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:
@@ -36,10 +36,14 @@ function collectUniqueUrls(app: IApplication): string[] {
|
||||
return app
|
||||
.collections
|
||||
.flatMap((a) => a.getAllScripts())
|
||||
.flatMap((script) => script.documentationUrls)
|
||||
.flatMap((script) => script.docs?.flatMap((doc) => parseUrls(doc)))
|
||||
.filter((url, index, array) => array.indexOf(url) === index);
|
||||
}
|
||||
|
||||
function parseUrls(text: string): string[] {
|
||||
return text?.match(/\bhttps?:\/\/\S+/gi) ?? [];
|
||||
}
|
||||
|
||||
function printUrls(statuses: IUrlStatus[]): string {
|
||||
/* eslint-disable prefer-template */
|
||||
return '\n'
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { parseApplication } from '@/application/Parser/ApplicationParser';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { createRenderer } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/MarkdownRenderer';
|
||||
|
||||
describe('MarkdownRenderer', () => {
|
||||
describe('can render all docs', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
for (const node of collectAllDocumentableNodes()) {
|
||||
it(`${node.nodeLabel}`, () => {
|
||||
// act
|
||||
const html = renderer.render(node.docs);
|
||||
// assert
|
||||
expect(isValidHtml(html));
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
interface IDocumentableNode {
|
||||
nodeLabel: string
|
||||
docs: string
|
||||
}
|
||||
function* collectAllDocumentableNodes(): Generator<IDocumentableNode> {
|
||||
const app = parseApplication();
|
||||
for (const collection of app.collections) {
|
||||
const documentableNodes = [
|
||||
...collection.getAllScripts(),
|
||||
...collection.getAllCategories(),
|
||||
];
|
||||
for (const node of documentableNodes) {
|
||||
const documentable: IDocumentableNode = {
|
||||
nodeLabel: `${OperatingSystem[collection.os]} | ${node.name} (${node.id})`,
|
||||
docs: node.docs.join('\n'),
|
||||
};
|
||||
yield documentable;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isValidHtml(value: string): boolean {
|
||||
const doc = new window.DOMParser().parseFromString(value, 'text/html');
|
||||
return Array.from(doc.body.childNodes).some((node) => node.nodeType === 1);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -92,15 +92,15 @@ describe('Script', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
describe('documentationUrls', () => {
|
||||
describe('docs', () => {
|
||||
it('sets as expected', () => {
|
||||
// arrange
|
||||
const expected = ['doc1', 'doc2'];
|
||||
// act
|
||||
const sut = new ScriptBuilder()
|
||||
.withDocumentationUrls(expected)
|
||||
.withDocs(expected)
|
||||
.build();
|
||||
const actual = sut.documentationUrls;
|
||||
const actual = sut.docs;
|
||||
// assert
|
||||
expect(actual).to.equal(expected);
|
||||
});
|
||||
@@ -115,7 +115,7 @@ class ScriptBuilder {
|
||||
|
||||
private level = RecommendationLevel.Standard;
|
||||
|
||||
private documentationUrls: readonly string[];
|
||||
private docs: readonly string[] = undefined;
|
||||
|
||||
public withCodes(code: string, revertCode = ''): ScriptBuilder {
|
||||
this.code = new ScriptCodeStub()
|
||||
@@ -139,8 +139,8 @@ class ScriptBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDocumentationUrls(urls: readonly string[]): ScriptBuilder {
|
||||
this.documentationUrls = urls;
|
||||
public withDocs(urls: readonly string[]): ScriptBuilder {
|
||||
this.docs = urls;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ class ScriptBuilder {
|
||||
return new Script(
|
||||
this.name,
|
||||
this.code,
|
||||
this.documentationUrls,
|
||||
this.docs,
|
||||
this.level,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ function isReversible(category: ICategory): boolean {
|
||||
function expectSameCategory(node: INode, category: ICategory): void {
|
||||
expect(node.type).to.equal(NodeType.Category, getErrorMessage('type'));
|
||||
expect(node.id).to.equal(getCategoryNodeId(category), getErrorMessage('id'));
|
||||
expect(node.documentationUrls).to.equal(category.documentationUrls, getErrorMessage('documentationUrls'));
|
||||
expect(node.docs).to.equal(category.docs, getErrorMessage('docs'));
|
||||
expect(node.text).to.equal(category.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(isReversible(category), getErrorMessage('isReversible'));
|
||||
expect(node.children).to.have.lengthOf(category.scripts.length || category.subCategories.length, getErrorMessage('name'));
|
||||
@@ -110,7 +110,7 @@ function expectSameCategory(node: INode, category: ICategory): void {
|
||||
function expectSameScript(node: INode, script: IScript): void {
|
||||
expect(node.type).to.equal(NodeType.Script, getErrorMessage('type'));
|
||||
expect(node.id).to.equal(getScriptNodeId(script), getErrorMessage('id'));
|
||||
expect(node.documentationUrls).to.equal(script.documentationUrls, getErrorMessage('documentationUrls'));
|
||||
expect(node.docs).to.equal(script.docs, getErrorMessage('docs'));
|
||||
expect(node.text).to.equal(script.name, getErrorMessage('name'));
|
||||
expect(node.isReversible).to.equal(script.canRevert(), getErrorMessage('canRevert'));
|
||||
expect(node.children).to.equal(undefined);
|
||||
|
||||
@@ -12,7 +12,7 @@ describe('NodePredicateFilter', () => {
|
||||
data: {
|
||||
text: 'script-text',
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
docs: [],
|
||||
isReversible: false,
|
||||
},
|
||||
states: undefined,
|
||||
@@ -22,7 +22,7 @@ describe('NodePredicateFilter', () => {
|
||||
id: 'script',
|
||||
text: 'script-text',
|
||||
isReversible: false,
|
||||
documentationUrls: [],
|
||||
docs: [],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
};
|
||||
@@ -54,7 +54,7 @@ function getExistingNode(): ILiquorTreeExistingNode {
|
||||
data: {
|
||||
text: 'script-text',
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
docs: [],
|
||||
isReversible: false,
|
||||
},
|
||||
states: undefined,
|
||||
|
||||
@@ -32,16 +32,16 @@ describe('NodeStateUpdater', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('a'), getScriptNode('b')],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('c')],
|
||||
},
|
||||
],
|
||||
@@ -56,16 +56,16 @@ describe('NodeStateUpdater', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('a'), getScriptNode('b')],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('c')],
|
||||
},
|
||||
],
|
||||
@@ -80,16 +80,16 @@ describe('NodeStateUpdater', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('a'), getScriptNode('b')],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('c')],
|
||||
},
|
||||
],
|
||||
@@ -128,16 +128,16 @@ describe('NodeStateUpdater', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('a'), getScriptNode('b')],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('c')],
|
||||
},
|
||||
],
|
||||
@@ -152,16 +152,16 @@ describe('NodeStateUpdater', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('a'), getScriptNode('b')],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('c')],
|
||||
},
|
||||
],
|
||||
@@ -176,16 +176,16 @@ describe('NodeStateUpdater', () => {
|
||||
// arrange
|
||||
const node = {
|
||||
id: '1',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('a'), getScriptNode('b')],
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
data: { type: NodeType.Category, documentationUrls: [], isReversible: false },
|
||||
data: { type: NodeType.Category, docs: [], isReversible: false },
|
||||
children: [getScriptNode('c')],
|
||||
},
|
||||
],
|
||||
@@ -204,7 +204,7 @@ describe('NodeStateUpdater', () => {
|
||||
id: scriptNodeId,
|
||||
data: {
|
||||
type: NodeType.Script,
|
||||
documentationUrls: [],
|
||||
docs: [],
|
||||
isReversible: false,
|
||||
},
|
||||
children: [],
|
||||
|
||||
@@ -33,20 +33,20 @@ function getNode(): INode {
|
||||
text: 'parentcategory',
|
||||
isReversible: true,
|
||||
type: NodeType.Category,
|
||||
documentationUrls: ['parentcategory-url1', 'parentcategory-url2'],
|
||||
docs: ['parentcategory-doc1', 'parentcategory-doc2'],
|
||||
children: [
|
||||
{
|
||||
id: '2',
|
||||
text: 'subcategory',
|
||||
isReversible: true,
|
||||
documentationUrls: ['subcategory-url1', 'subcategory-url2'],
|
||||
docs: ['subcategory-doc1', 'subcategory-doc2'],
|
||||
type: NodeType.Category,
|
||||
children: [
|
||||
{
|
||||
id: 'script1',
|
||||
text: 'cool script 1',
|
||||
isReversible: true,
|
||||
documentationUrls: ['script1url1', 'script1url2'],
|
||||
docs: ['script1-doc1', 'script1-doc2'],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
},
|
||||
@@ -54,7 +54,7 @@ function getNode(): INode {
|
||||
id: 'script2',
|
||||
text: 'cool script 2',
|
||||
isReversible: true,
|
||||
documentationUrls: ['script2url1', 'script2url2'],
|
||||
docs: ['script2-doc1', 'script2-doc2'],
|
||||
children: [],
|
||||
type: NodeType.Script,
|
||||
}],
|
||||
@@ -66,7 +66,7 @@ function getExpectedExistingNodeData(node: INode): ILiquorTreeNodeData {
|
||||
return {
|
||||
text: node.text,
|
||||
type: node.type,
|
||||
documentationUrls: node.documentationUrls,
|
||||
docs: node.docs,
|
||||
isReversible: node.isReversible,
|
||||
};
|
||||
}
|
||||
@@ -74,7 +74,7 @@ function getExpectedExistingNodeData(node: INode): ILiquorTreeNodeData {
|
||||
function getExpectedNewNodeData(node: INode): ICustomLiquorTreeData {
|
||||
return {
|
||||
type: node.type,
|
||||
documentationUrls: node.documentationUrls,
|
||||
docs: node.docs,
|
||||
isReversible: node.isReversible,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { createRenderer } from '@/presentation/components/Scripts/View/ScriptsTree/SelectableTree/Node/Documentation/MarkdownRenderer';
|
||||
|
||||
describe('MarkdownRenderer', () => {
|
||||
describe('createRenderer', () => {
|
||||
it('can create', () => {
|
||||
// arrange & act
|
||||
const renderer = createRenderer();
|
||||
// assert
|
||||
expect(renderer !== undefined);
|
||||
});
|
||||
it('opens URLs in new tab', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
const markdown = '[undergroundwires.dev](https://undergroundwires.dev)';
|
||||
// act
|
||||
const htmlString = renderer.render(markdown);
|
||||
// assert
|
||||
const html = parseHtml(htmlString);
|
||||
const aElement = html.getElementsByTagName('a')[0];
|
||||
const href = aElement.getAttribute('target');
|
||||
expect(href).to.equal('_blank');
|
||||
});
|
||||
it('does not convert single linebreak to <br>', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
const markdown = 'Text with\nSingle\nLinebreaks';
|
||||
// act
|
||||
const htmlString = renderer.render(markdown);
|
||||
// assert
|
||||
const html = parseHtml(htmlString);
|
||||
const totalBrElements = html.getElementsByTagName('br').length;
|
||||
expect(totalBrElements).to.equal(0);
|
||||
});
|
||||
it('creates links for plain URL', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
const expectedUrl = 'https://privacy.sexy/';
|
||||
const markdown = `Visit ${expectedUrl} now!`;
|
||||
// act
|
||||
const htmlString = renderer.render(markdown);
|
||||
// assert
|
||||
const html = parseHtml(htmlString);
|
||||
const aElement = html.getElementsByTagName('a')[0];
|
||||
const href = aElement.getAttribute('href');
|
||||
expect(href).to.equal(expectedUrl);
|
||||
});
|
||||
it('it generates beautiful labels for auto-linkified URL', () => {
|
||||
// arrange
|
||||
const renderer = createRenderer();
|
||||
const url = 'https://privacy.sexy';
|
||||
const expectedText = 'privacy.sexy';
|
||||
const markdown = `Visit ${url} now!`;
|
||||
// act
|
||||
const htmlString = renderer.render(markdown);
|
||||
// assert
|
||||
const html = parseHtml(htmlString);
|
||||
const aElement = html.getElementsByTagName('a')[0];
|
||||
expect(aElement.text).to.equal(expectedText);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function parseHtml(htmlString: string): Document {
|
||||
const parser = new window.DOMParser();
|
||||
const htmlDoc = parser.parseFromString(htmlString, 'text/html');
|
||||
return htmlDoc;
|
||||
}
|
||||
@@ -39,7 +39,7 @@ describe('ReverterFactory', () => {
|
||||
id: nodeId,
|
||||
text: 'text',
|
||||
isReversible: false,
|
||||
documentationUrls: [],
|
||||
docs: [],
|
||||
children: [],
|
||||
type,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { CategoryData, CategoryOrScriptData, DocumentationUrlsData } from '@/application/collections/';
|
||||
import type { CategoryData, CategoryOrScriptData, DocumentationData } from '@/application/collections/';
|
||||
import { ScriptDataStub } from './ScriptDataStub';
|
||||
|
||||
export class CategoryDataStub implements CategoryData {
|
||||
@@ -6,7 +6,7 @@ export class CategoryDataStub implements CategoryData {
|
||||
|
||||
public category = 'category name';
|
||||
|
||||
public docs?: DocumentationUrlsData;
|
||||
public docs?: DocumentationData;
|
||||
|
||||
public withChildren(children: readonly CategoryOrScriptData[]) {
|
||||
this.children = children;
|
||||
@@ -18,7 +18,7 @@ export class CategoryDataStub implements CategoryData {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDocs(docs: DocumentationUrlsData) {
|
||||
public withDocs(docs: DocumentationData) {
|
||||
this.docs = docs;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
|
||||
|
||||
public readonly scripts = new Array<IScript>();
|
||||
|
||||
public readonly documentationUrls = new Array<string>();
|
||||
public readonly docs = new Array<string>();
|
||||
|
||||
public constructor(id: number) {
|
||||
super(id);
|
||||
|
||||
@@ -33,7 +33,7 @@ export class ScriptDataStub implements ScriptData {
|
||||
|
||||
public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase();
|
||||
|
||||
public docs = ['hello.com'];
|
||||
public docs?: readonly string[] = ['hello.com'];
|
||||
|
||||
private constructor() { /* use static methods for constructing */ }
|
||||
|
||||
@@ -42,7 +42,7 @@ export class ScriptDataStub implements ScriptData {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withDocs(docs: string[]): ScriptDataStub {
|
||||
public withDocs(docs: readonly string[]): ScriptDataStub {
|
||||
this.docs = docs;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
|
||||
revert: `REM revert-code (${this.id})`,
|
||||
};
|
||||
|
||||
public readonly documentationUrls = new Array<string>();
|
||||
public readonly docs = new Array<string>();
|
||||
|
||||
public level? = RecommendationLevel.Standard;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user