From 9d93c859d7791fb16c216ff95265c58ba1000c5a Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Sat, 9 Sep 2017 09:15:08 +0100 Subject: [PATCH] build(aio): auto-link more code items We now parse all code blocks, after they have been rendered by dgeni and insert links to API docs that match "words" in the code. --- .../transforms/angular-api-package/index.js | 1 + .../post-processors/auto-link-code.js | 69 ++++++++++++++----- .../post-processors/auto-link-code.spec.js | 37 +++++++++- 3 files changed, 87 insertions(+), 20 deletions(-) diff --git a/aio/tools/transforms/angular-api-package/index.js b/aio/tools/transforms/angular-api-package/index.js index 1c7c78c9b9..b54c75dd2a 100644 --- a/aio/tools/transforms/angular-api-package/index.js +++ b/aio/tools/transforms/angular-api-package/index.js @@ -128,4 +128,5 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage]) convertToJsonProcessor.docTypes = convertToJsonProcessor.docTypes.concat(DOCS_TO_CONVERT); postProcessHtml.docTypes = convertToJsonProcessor.docTypes.concat(DOCS_TO_CONVERT); autoLinkCode.docTypes = DOCS_TO_CONVERT; + autoLinkCode.codeElements = ['code', 'code-example', 'code-pane']; }); diff --git a/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.js b/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.js index 79b5f31668..3e4982dac3 100644 --- a/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.js +++ b/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.js @@ -3,34 +3,69 @@ const is = require('hast-util-is-element'); const textContent = require('hast-util-to-string'); /** - * Automatically add in a link to the relevant document for simple - * code blocks, e.g. `MyClass` becomes - * `MyClass` + * Automatically add in a link to the relevant document for code blocks. + * E.g. `MyClass` becomes `MyClass` * - * @property docTypes an array of strings. Only docs that have one of these docTypes - * will be linked to. + * @property docTypes an array of strings. + * Only docs that have one of these docTypes will be linked to. * Usually set to the API exported docTypes, e.g. "class", "function", "directive", etc. + * + * @property codeElements an array of strings. + * Only text contained in these elements will be linked to. + * Usually set to "code" but also "code-example" for angular.io. */ module.exports = function autoLinkCode(getDocFromAlias) { autoLinkCodeImpl.docTypes = []; + autoLinkCodeImpl.codeElements = ['code']; return autoLinkCodeImpl; function autoLinkCodeImpl() { return (ast) => { - visit(ast, (node, ancestors) => { - if (is(node, 'code') && ancestors.every(ancestor => !is(ancestor, 'a'))) { - const docs = getDocFromAlias(textContent(node)); - if (docs.length === 1 && autoLinkCodeImpl.docTypes.indexOf(docs[0].docType) !== -1) { - const link = { - type: 'element', - tagName: 'a', - properties: { href: docs[0].path }, - children: node.children - }; - node.children = [link]; - } + visit(ast, 'element', (node, ancestors) => { + // Only interested in code elements that are not inside links + if (autoLinkCodeImpl.codeElements.some(elementType => is(node, elementType)) && + ancestors.every(ancestor => !is(ancestor, 'a'))) { + visit(node, 'text', (node, ancestors) => { + // Only interested in text nodes that are not inside links + if (ancestors.every(ancestor => !is(ancestor, 'a'))) { + + const parent = ancestors[ancestors.length-1]; + const index = parent.children.indexOf(node); + + // Can we convert the whole text node into a doc link? + const docs = getDocFromAlias(node.value); + if (foundValidDoc(docs)) { + parent.children.splice(index, 1, createLinkNode(docs[0], node.value)); + } else { + // Parse the text for words that we can convert to links + const nodes = textContent(node).split(/([A-Za-z0-9_]+)/) + .filter(word => word.length) + .map(word => { + const docs = getDocFromAlias(word); + return foundValidDoc(docs) ? + createLinkNode(docs[0], word) : // Create a link wrapping the text node. + { type: 'text', value: word }; // this is just text so push a new text node + }); + + // Replace the text node with the links and leftover text nodes + Array.prototype.splice.apply(parent.children, [index, 1].concat(nodes)); + } + } + }); } }); }; } + function foundValidDoc(docs) { + return docs.length === 1 && autoLinkCodeImpl.docTypes.indexOf(docs[0].docType) !== -1; + } + + function createLinkNode(doc, text) { + return { + type: 'element', + tagName: 'a', + properties: { href: doc.path, class: 'code-anchor' }, + children: [{ type: 'text', value: text }] + }; + } }; diff --git a/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.spec.js b/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.spec.js index eec9b0a930..de96946821 100644 --- a/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.spec.js +++ b/aio/tools/transforms/angular-base-package/post-processors/auto-link-code.spec.js @@ -9,7 +9,7 @@ describe('autoLinkCode post-processor', () => { const dgeni = new Dgeni([testPackage]); const injector = dgeni.configureInjector(); autoLinkCode = injector.get('autoLinkCode'); - autoLinkCode.docTypes = ['class', 'pipe']; + autoLinkCode.docTypes = ['class', 'pipe', 'function', 'const']; aliasMap = injector.get('aliasMap'); processor = injector.get('postProcessHtml'); processor.docTypes = ['test-doc']; @@ -20,14 +20,14 @@ describe('autoLinkCode post-processor', () => { aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' }); const doc = { docType: 'test-doc', renderedContent: 'MyClass' }; processor.$process([doc]); - expect(doc.renderedContent).toEqual('MyClass'); + expect(doc.renderedContent).toEqual('MyClass'); }); it('should insert an anchor into every code item that matches an alias of an API doc', () => { aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass', 'foo.MyClass'], path: 'a/b/myclass' }); const doc = { docType: 'test-doc', renderedContent: 'foo.MyClass' }; processor.$process([doc]); - expect(doc.renderedContent).toEqual('foo.MyClass'); + expect(doc.renderedContent).toEqual('foo.MyClass'); }); it('should ignore code items that do not match a link to an API doc', () => { @@ -43,4 +43,35 @@ describe('autoLinkCode post-processor', () => { processor.$process([doc]); expect(doc.renderedContent).toEqual('
MyClass
'); }); + + it('should ignore code items match an API doc but are not in the list of acceptable docTypes', () => { + aliasMap.addDoc({ docType: 'directive', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' }); + const doc = { docType: 'test-doc', renderedContent: 'MyClass' }; + processor.$process([doc]); + expect(doc.renderedContent).toEqual('MyClass'); + }); + + it('should insert anchors for individual text nodes within a code block', () => { + aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' }); + const doc = { docType: 'test-doc', renderedContent: 'MyClassMyClass' }; + processor.$process([doc]); + expect(doc.renderedContent).toEqual('MyClassMyClass'); + }); + + it('should insert anchors for words that match within text nodes in a code block', () => { + aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' }); + aliasMap.addDoc({ docType: 'function', id: 'myFunc', aliases: ['myFunc'], path: 'ng/myfunc' }); + aliasMap.addDoc({ docType: 'const', id: 'MY_CONST', aliases: ['MY_CONST'], path: 'ng/my_const' }); + const doc = { docType: 'test-doc', renderedContent: 'myFunc() {\n return new MyClass(MY_CONST);\n}' }; + processor.$process([doc]); + expect(doc.renderedContent).toEqual('myFunc() {\n return new MyClass(MY_CONST);\n}'); + }); + + it('should work with custom elements', () => { + autoLinkCode.codeElements = ['code-example']; + aliasMap.addDoc({ docType: 'class', id: 'MyClass', aliases: ['MyClass'], path: 'a/b/myclass' }); + const doc = { docType: 'test-doc', renderedContent: 'MyClass' }; + processor.$process([doc]); + expect(doc.renderedContent).toEqual('MyClass'); + }); });