Improve documentation support with markdown

Rework documentation URLs as inline markdown.

Redesign documentations with markdown text.

Redesign way to document scripts/categories and present the
documentation.

Documentation is showed in an expandable box instead of tooltip. This is
to allow writing longer documentation (tooltips are meant to be used for
short text) and have better experience on mobile.

If a node (script/category) has documentation it's now shown with single
information icon (ℹ) aligned to right.

Add support for rendering documentation as markdown. It automatically
converts plain URLs to URLs with display names (e.g.
https://docs.microsoft.com/..) will be rendered automatically like
"docs.microsoft.com - Windows 11 Privacy...".
This commit is contained in:
undergroundwires
2022-09-25 23:25:43 +02:00
parent 924b326244
commit 6067bdb24e
41 changed files with 973 additions and 265 deletions

View File

@@ -3,7 +3,7 @@ import { expect } from 'chai';
import type { CategoryData, CategoryOrScriptData } from '@/application/collections/';
import { CategoryFactoryType, parseCategory } from '@/application/Parser/CategoryParser';
import { parseScript } from '@/application/Parser/Script/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { parseDocs } from '@/application/Parser/DocumentationParser';
import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub';
import { ScriptDataStub } from '@tests/unit/shared/Stubs/ScriptDataStub';
import { CategoryCollectionParseContextStub } from '@tests/unit/shared/Stubs/CategoryCollectionParseContextStub';
@@ -157,14 +157,14 @@ describe('CategoryParser', () => {
it('returns expected docs', () => {
// arrange
const url = 'https://privacy.sexy';
const expected = parseDocUrls({ docs: url });
const expected = parseDocs({ docs: url });
const category = new CategoryDataStub()
.withDocs(url);
// act
const actual = new TestBuilder()
.withData(category)
.parseCategory()
.documentationUrls;
.docs;
// assert
expect(actual).to.deep.equal(expected);
});

View File

@@ -1,26 +1,60 @@
import 'mocha';
import { expect } from 'chai';
import type { DocumentableData } from '@/application/collections/';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { parseDocs } from '@/application/Parser/DocumentationParser';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('DocumentationParser', () => {
describe('parseDocUrls', () => {
describe('throws when absent', () => {
describe('parseDocs', () => {
describe('throws when node is missing', () => {
itEachAbsentObjectValue((absentValue) => {
// arrange
const expectedError = 'missing documentable';
// act
const act = () => parseDocUrls(absentValue);
const act = () => parseDocs(absentValue);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when single documentation is missing', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const expectedError = 'missing documentation';
const node: DocumentableData = { docs: ['non empty doc 1', absentValue] };
// act
const act = () => parseDocs(node);
// assert
expect(act).to.throw(expectedError);
});
});
describe('throws when type is unexpected', () => {
// arrange
const expectedTypeError = 'docs field (documentation) must be an array of strings';
const wrongTypedValue = 22 as never;
const testCases: ReadonlyArray<{ name: string, node: DocumentableData }> = [
{
name: 'given docs',
node: { docs: wrongTypedValue },
},
{
name: 'single doc',
node: { docs: ['non empty doc 1', wrongTypedValue] },
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
// act
const act = () => parseDocs(testCase.node);
// assert
expect(act).to.throw(expectedTypeError);
});
}
});
it('returns empty when empty', () => {
// arrange
const empty: DocumentableData = { };
// act
const actual = parseDocUrls(empty);
const actual = parseDocs(empty);
// assert
expect(actual).to.have.lengthOf(0);
});
@@ -30,7 +64,7 @@ describe('DocumentationParser', () => {
const expected = [url];
const sut: DocumentableData = { docs: url };
// act
const actual = parseDocUrls(sut);
const actual = parseDocs(sut);
// assert
expect(actual).to.deep.equal(expected);
});
@@ -39,7 +73,7 @@ describe('DocumentationParser', () => {
const expected = ['https://privacy.sexy', 'https://github.com/undergroundwires/privacy.sexy'];
const sut: DocumentableData = { docs: expected };
// act
const actual = parseDocUrls(sut);
const actual = parseDocs(sut);
// assert
expect(actual).to.deep.equal(expected);
});

View File

@@ -2,7 +2,7 @@ import 'mocha';
import { expect } from 'chai';
import type { ScriptData } from '@/application/collections/';
import { parseScript, ScriptFactoryType } from '@/application/Parser/Script/ScriptParser';
import { parseDocUrls } from '@/application/Parser/DocumentationParser';
import { parseDocs } from '@/application/Parser/DocumentationParser';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { ICategoryCollectionParseContext } from '@/application/Parser/Script/ICategoryCollectionParseContext';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
@@ -36,13 +36,13 @@ describe('ScriptParser', () => {
const docs = ['https://expected-doc1.com', 'https://expected-doc2.com'];
const script = ScriptDataStub.createWithCode()
.withDocs(docs);
const expected = parseDocUrls(script);
const expected = parseDocs(script);
// act
const actual = new TestBuilder()
.withData(script)
.parseScript();
// assert
expect(actual.documentationUrls).to.deep.equal(expected);
expect(actual.docs).to.deep.equal(expected);
});
describe('level', () => {
describe('accepts absent level', () => {