[ADF-2764] Added tool to add links to type names in Markdown files (#3226)

* [ADF-2764] Added basic type linker tool

* [ADF-2764] Support for class links in body text

* [ADF-2764] Started support for multi-word class names (not working yet)

* [ADF-2764] Updated script to handle English component names
This commit is contained in:
Andy Stark
2018-04-24 14:30:51 +01:00
committed by Eugenio Romano
parent b580efb7f6
commit 8f27cd1758
4 changed files with 668 additions and 0 deletions

View File

@@ -8,6 +8,10 @@
"enhance": [ "enhance": [
"tsInfo", "tsInfo",
"toc" "toc"
],
"dev": [
"tsInfo",
"typeLinker"
] ]
} }
} }

View File

@@ -1,6 +1,7 @@
module.exports = { module.exports = {
"ngNameToDisplayName": ngNameToDisplayName, "ngNameToDisplayName": ngNameToDisplayName,
"dekebabifyName": dekebabifyName, "dekebabifyName": dekebabifyName,
"kebabifyClassName": kebabifyClassName,
"classTypes": ["component", "directive", "model", "pipe", "service", "widget"] "classTypes": ["component", "directive", "model", "pipe", "service", "widget"]
} }
@@ -12,8 +13,23 @@ function ngNameToDisplayName(ngName) {
} }
function displayNameToNgName(name) {
var noSpaceName = ngName.replace(/ ([a-zA-Z])/, "$1".toUpperCase());
return noSpaceName.substr(0, 1).toUpperCase() + noSpaceName.substr(1);
}
function dekebabifyName(name) { function dekebabifyName(name) {
var result = name.replace(/-/g, " "); var result = name.replace(/-/g, " ");
result = result.substr(0, 1).toUpperCase() + result.substr(1); result = result.substr(0, 1).toUpperCase() + result.substr(1);
return result; return result;
}
function kebabifyClassName(name) {
var result = name.replace(/(Component|Directive|Interface|Model|Pipe|Service|Widget)$/, match => {
return "." + match.toLowerCase();
});
result = result.replace(/([A-Z])/g, "-$1");
return result.substr(1).toLowerCase();
} }

View File

@@ -0,0 +1,289 @@
"use strict";
exports.__esModule = true;
var path = require("path");
var fs = require("fs");
var typedoc_1 = require("typedoc");
var unist = require("../unistHelpers");
var ngHelpers = require("../ngHelpers");
var includedNodeTypes = [
"root", "paragraph", "inlineCode", "list", "listItem",
"table", "tableRow", "tableCell", "emphasis", "strong",
"link", "text"
];
var docFolder = path.resolve("..", "docs");
var adfLibNames = ["core", "content-services", "insights", "process-services"];
function initPhase(aggData) {
aggData.docFiles = {};
aggData.nameLookup = new SplitNameLookup();
adfLibNames.forEach(function (libName) {
var libFolderPath = path.resolve(docFolder, libName);
var files = fs.readdirSync(libFolderPath);
files.forEach(function (file) {
if (path.extname(file) === ".md") {
var relPath = libFolderPath.substr(libFolderPath.indexOf("docs") + 5).replace(/\\/, "/") + "/" + file;
var compName = path.basename(file, ".md");
aggData.docFiles[compName] = relPath;
}
});
});
var classes = aggData.projData.getReflectionsByKind(typedoc_1.ReflectionKind.Class);
classes.forEach(function (currClass) {
if (currClass.name.match(/(Component|Directive|Interface|Model|Pipe|Service|Widget)$/)) {
aggData.nameLookup.addName(currClass.name);
}
});
//console.log(JSON.stringify(aggData.nameLookup));
}
exports.initPhase = initPhase;
function readPhase(tree, pathname, aggData) { }
exports.readPhase = readPhase;
function aggPhase(aggData) {
}
exports.aggPhase = aggPhase;
function updatePhase(tree, pathname, aggData) {
traverseMDTree(tree);
return true;
function traverseMDTree(node) {
if (!includedNodeTypes.includes(node.type)) {
return;
}
if (node.type === "inlineCode") {
var link = resolveTypeLink(aggData, node.value);
if (link) {
convertNodeToTypeLink(node, node.value, link);
}
}
else if (node.type === "link") {
if (node.children && ((node.children[0].type === "inlineCode") ||
(node.children[0].type === "text"))) {
var link = resolveTypeLink(aggData, node.children[0].value);
if (link) {
convertNodeToTypeLink(node, node.children[0].value, link);
}
}
}
else if (node.type === "paragraph") {
node.children.forEach(function (child, index) {
if (child.type === "text") {
var newNodes = handleLinksInBodyText(aggData, child.value);
(_a = node.children).splice.apply(_a, [index, 1].concat(newNodes));
}
else {
traverseMDTree(child);
}
var _a;
});
}
else if (node.children) {
node.children.forEach(function (child) {
traverseMDTree(child);
});
}
}
}
exports.updatePhase = updatePhase;
var SplitNameNode = /** @class */ (function () {
function SplitNameNode(key, value) {
if (key === void 0) { key = ""; }
if (value === void 0) { value = ""; }
this.key = key;
this.value = value;
this.children = {};
}
SplitNameNode.prototype.addChild = function (child) {
this.children[child.key] = child;
};
return SplitNameNode;
}());
var SplitNameMatchElement = /** @class */ (function () {
function SplitNameMatchElement(node, textPos) {
this.node = node;
this.textPos = textPos;
}
return SplitNameMatchElement;
}());
var SplitNameMatchResult = /** @class */ (function () {
function SplitNameMatchResult(value, startPos) {
this.value = value;
this.startPos = startPos;
}
return SplitNameMatchResult;
}());
var SplitNameMatcher = /** @class */ (function () {
function SplitNameMatcher(root) {
this.root = root;
this.reset();
}
/* Returns all names that match when this word is added. */
SplitNameMatcher.prototype.nextWord = function (word, textPos) {
var result = [];
this.matches.push(new SplitNameMatchElement(this.root, textPos));
for (var i = this.matches.length - 1; i >= 0; i--) {
if (this.matches[i].node.children) {
var child = this.matches[i].node.children[word];
if (child) {
if (child.value) {
/* Using unshift to add the match to the array means that
* the longest matches will appear first in the array.
* User can then just use the first array element if only
* the longest match is needed.
*/
result.unshift(new SplitNameMatchResult(child.value, this.matches[i].textPos));
this.matches.splice(i, 1);
}
else {
this.matches[i] = new SplitNameMatchElement(child, this.matches[i].textPos);
}
}
else {
this.matches.splice(i, 1);
}
}
else {
this.matches.splice(i, 1);
}
}
if (result === []) {
return null;
}
else {
return result;
}
};
SplitNameMatcher.prototype.reset = function () {
this.matches = [];
};
return SplitNameMatcher;
}());
var SplitNameLookup = /** @class */ (function () {
function SplitNameLookup() {
this.root = new SplitNameNode();
}
SplitNameLookup.prototype.addName = function (name) {
var spacedName = name.replace(/([A-Z])/g, " $1");
var segments = spacedName.trim().toLowerCase().split(" ");
var currNode = this.root;
segments.forEach(function (segment, index) {
var value = "";
if (index == (segments.length - 1)) {
value = name;
}
var childNode = currNode.children[segment];
if (!childNode) {
childNode = new SplitNameNode(segment, value);
currNode.addChild(childNode);
}
currNode = childNode;
});
};
return SplitNameLookup;
}());
var WordScanner = /** @class */ (function () {
function WordScanner(text) {
this.text = text;
this.separators = " \n\r\t.;:";
this.index = 0;
this.nextSeparator = 0;
this.next();
}
WordScanner.prototype.finished = function () {
return this.index >= this.text.length;
};
WordScanner.prototype.next = function () {
this.advanceIndex();
this.advanceNextSeparator();
this.current = this.text.substring(this.index, this.nextSeparator);
};
WordScanner.prototype.advanceNextSeparator = function () {
for (var i = this.index; i < this.text.length; i++) {
if (this.separators.indexOf(this.text[i]) !== -1) {
this.nextSeparator = i;
return;
}
}
this.nextSeparator = this.text.length;
};
WordScanner.prototype.advanceIndex = function () {
for (var i = this.nextSeparator; i < this.text.length; i++) {
if (this.separators.indexOf(this.text[i]) === -1) {
this.index = i;
return;
}
}
this.index = this.text.length;
};
return WordScanner;
}());
function handleLinksInBodyText(aggData, text) {
var result = [];
var currTextStart = 0;
var matcher = new SplitNameMatcher(aggData.nameLookup.root);
for (var scanner = new WordScanner(text); !scanner.finished(); scanner.next()) {
var word = scanner.current
.replace(/'s$/, "")
.replace(/^[;:,\."']+/g, "")
.replace(/[;:,\."']+$/g, "");
var link = resolveTypeLink(aggData, word);
var matchStart = void 0;
if (!link) {
var match = matcher.nextWord(word.toLowerCase(), scanner.index);
if (match && match[0]) {
link = resolveTypeLink(aggData, match[0].value);
matchStart = match[0].startPos;
}
}
else {
matchStart = scanner.index;
}
if (link) {
var linkText = text.substring(matchStart, scanner.nextSeparator);
var linkNode = unist.makeLink(unist.makeText(linkText), link);
var prevText = text.substring(currTextStart, matchStart);
result.push(unist.makeText(prevText));
result.push(linkNode);
currTextStart = scanner.nextSeparator;
matcher.reset();
}
}
var remainingText = text.substring(currTextStart, text.length);
if (remainingText) {
result.push(unist.makeText(remainingText));
}
return result;
}
function resolveTypeLink(aggData, text) {
var possTypeName = cleanTypeName(text);
var ref = aggData.projData.findReflectionByName(possTypeName);
if (ref && isLinkable(ref.kind)) {
var kebabName = ngHelpers.kebabifyClassName(possTypeName);
var possDocFile = aggData.docFiles[kebabName];
var url = "../../lib/" + ref.sources[0].fileName;
if (possDocFile) {
url = "../" + possDocFile;
}
return url;
}
else {
return "";
}
}
function cleanTypeName(text) {
var matches = text.match(/[a-zA-Z0-9_]+<([a-zA-Z0-9_]+)>/);
if (matches) {
return matches[1];
}
else {
return text;
}
}
function isLinkable(kind) {
return (kind === typedoc_1.ReflectionKind.Class) ||
(kind === typedoc_1.ReflectionKind.Interface) ||
(kind === typedoc_1.ReflectionKind.Enum);
}
function convertNodeToTypeLink(node, text, url) {
var linkDisplayText = unist.makeInlineCode(text);
node.type = "link";
node.url = url;
node.children = [linkDisplayText];
}

View File

@@ -0,0 +1,359 @@
import * as path from "path";
import * as fs from "fs";
import * as remark from "remark";
import * as stringify from "remark-stringify";
import * as frontMatter from "remark-frontmatter";
import {
Application,
ProjectReflection,
Reflection,
DeclarationReflection,
SignatureReflection,
ParameterReflection,
ReflectionKind,
TraverseProperty,
Decorator
} from "typedoc";
import { CommentTag } from "typedoc/dist/lib/models";
import * as unist from "../unistHelpers";
import * as ngHelpers from "../ngHelpers";
const includedNodeTypes = [
"root", "paragraph", "inlineCode", "list", "listItem",
"table", "tableRow", "tableCell", "emphasis", "strong",
"link", "text"
];
const docFolder = path.resolve("..", "docs");
const adfLibNames = ["core", "content-services", "insights", "process-services"];
export function initPhase(aggData) {
aggData.docFiles = {};
aggData.nameLookup = new SplitNameLookup();
adfLibNames.forEach(libName => {
let libFolderPath = path.resolve(docFolder, libName);
let files = fs.readdirSync(libFolderPath);
files.forEach(file => {
if (path.extname(file) === ".md") {
let relPath = libFolderPath.substr(libFolderPath.indexOf("docs") + 5).replace(/\\/, "/") + "/" + file;
let compName = path.basename(file, ".md");
aggData.docFiles[compName] = relPath;
}
});
});
let classes = aggData.projData.getReflectionsByKind(ReflectionKind.Class);
classes.forEach(currClass => {
if (currClass.name.match(/(Component|Directive|Interface|Model|Pipe|Service|Widget)$/)) {
aggData.nameLookup.addName(currClass.name);
}
});
//console.log(JSON.stringify(aggData.nameLookup));
}
export function readPhase(tree, pathname, aggData) {}
export function aggPhase(aggData) {
}
export function updatePhase(tree, pathname, aggData) {
traverseMDTree(tree);
return true;
function traverseMDTree(node) {
if (!includedNodeTypes.includes(node.type)) {
return;
}
if (node.type === "inlineCode") {
let link = resolveTypeLink(aggData, node.value);
if (link) {
convertNodeToTypeLink(node, node.value, link);
}
} else if (node.type === "link") {
if (node.children && (
(node.children[0].type === "inlineCode") ||
(node.children[0].type === "text")
)) {
let link = resolveTypeLink(aggData, node.children[0].value);
if (link) {
convertNodeToTypeLink(node, node.children[0].value, link);
}
}
} else if (node.type === "paragraph") {
node.children.forEach((child, index) => {
if (child.type === "text") {
let newNodes = handleLinksInBodyText(aggData, child.value);
node.children.splice(index, 1, ...newNodes);
} else {
traverseMDTree(child);
}
});
} else if (node.children) {
node.children.forEach(child => {
traverseMDTree(child);
});
}
}
}
class SplitNameNode {
children: {};
constructor(public key: string = "", public value: string = "") {
this.children = {};
}
addChild(child: SplitNameNode) {
this.children[child.key] = child;
}
}
class SplitNameMatchElement {
constructor(public node: SplitNameNode, public textPos: number) {}
}
class SplitNameMatchResult {
constructor(public value: string, public startPos: number) {}
}
class SplitNameMatcher {
matches: SplitNameMatchElement[];
constructor(public root: SplitNameNode) {
this.reset();
}
/* Returns all names that match when this word is added. */
nextWord(word: string, textPos: number): SplitNameMatchResult[] {
let result = [];
this.matches.push(new SplitNameMatchElement(this.root, textPos));
for (let i = this.matches.length - 1; i >= 0; i--) {
if (this.matches[i].node.children) {
let child = this.matches[i].node.children[word];
if (child) {
if (child.value) {
/* Using unshift to add the match to the array means that
* the longest matches will appear first in the array.
* User can then just use the first array element if only
* the longest match is needed.
*/
result.unshift(new SplitNameMatchResult(child.value, this.matches[i].textPos));
this.matches.splice(i, 1);
} else {
this.matches[i] = new SplitNameMatchElement(child, this.matches[i].textPos);
}
} else {
this.matches.splice(i, 1);
}
} else {
this.matches.splice(i, 1);
}
}
if (result === []) {
return null;
} else {
return result;
}
}
reset() {
this.matches = [];
}
}
class SplitNameLookup {
root: SplitNameNode;
constructor() {
this.root = new SplitNameNode();
}
addName(name: string) {
let spacedName = name.replace(/([A-Z])/g, " $1");
let segments = spacedName.trim().toLowerCase().split(" ");
let currNode = this.root;
segments.forEach((segment, index) => {
let value = "";
if (index == (segments.length - 1)) {
value = name;
}
let childNode = currNode.children[segment];
if (!childNode) {
childNode = new SplitNameNode(segment, value);
currNode.addChild(childNode);
}
currNode = childNode;
});
}
}
class WordScanner {
separators: string;
index: number;
nextSeparator: number;
current: string;
constructor(public text: string) {
this.separators = " \n\r\t.;:";
this.index = 0;
this.nextSeparator = 0;
this.next();
}
finished() {
return this.index >= this.text.length;
}
next(): void {
this.advanceIndex();
this.advanceNextSeparator();
this.current = this.text.substring(this.index, this.nextSeparator);
}
advanceNextSeparator() {
for (let i = this.index; i < this.text.length; i++) {
if (this.separators.indexOf(this.text[i]) !== -1) {
this.nextSeparator = i;
return;
}
}
this.nextSeparator = this.text.length;
}
advanceIndex() {
for (let i = this.nextSeparator; i < this.text.length; i++) {
if (this.separators.indexOf(this.text[i]) === -1) {
this.index = i;
return;
}
}
this.index = this.text.length;
}
}
function handleLinksInBodyText(aggData, text: string): Node[] {
let result = [];
let currTextStart = 0;
let matcher = new SplitNameMatcher(aggData.nameLookup.root);
for (let scanner = new WordScanner(text); !scanner.finished(); scanner.next()) {
let word = scanner.current
.replace(/'s$/, "")
.replace(/^[;:,\."']+/g, "")
.replace(/[;:,\."']+$/g, "");
let link = resolveTypeLink(aggData, word);
let matchStart;
if (!link) {
let match = matcher.nextWord(word.toLowerCase(), scanner.index);
if (match && match[0]) {
link = resolveTypeLink(aggData, match[0].value);
matchStart = match[0].startPos;
}
} else {
matchStart = scanner.index
}
if (link) {
let linkText = text.substring(matchStart, scanner.nextSeparator);
let linkNode = unist.makeLink(unist.makeText(linkText), link);
let prevText = text.substring(currTextStart, matchStart);
result.push(unist.makeText(prevText));
result.push(linkNode);
currTextStart = scanner.nextSeparator;
matcher.reset();
}
}
let remainingText = text.substring(currTextStart, text.length);
if (remainingText) {
result.push(unist.makeText(remainingText));
}
return result;
}
function resolveTypeLink(aggData, text): string {
let possTypeName = cleanTypeName(text);
let ref: Reflection = aggData.projData.findReflectionByName(possTypeName);
if (ref && isLinkable(ref.kind)) {
let kebabName = ngHelpers.kebabifyClassName(possTypeName);
let possDocFile = aggData.docFiles[kebabName];
let url = "../../lib/" + ref.sources[0].fileName;
if (possDocFile) {
url = "../" + possDocFile;
}
return url;
} else {
return "";
}
}
function cleanTypeName(text) {
let matches = text.match(/[a-zA-Z0-9_]+<([a-zA-Z0-9_]+)>/);
if (matches) {
return matches[1];
} else {
return text;
}
}
function isLinkable(kind: ReflectionKind) {
return (kind === ReflectionKind.Class) ||
(kind === ReflectionKind.Interface) ||
(kind === ReflectionKind.Enum);
}
function convertNodeToTypeLink(node, text, url) {
let linkDisplayText = unist.makeInlineCode(text);
node.type = "link";
node.url = url;
node.children = [linkDisplayText];
}