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:
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user