markdown-code-example-inserter
Version:
Syncs code examples with markdown documentation.
100 lines (99 loc) • 3.97 kB
JavaScript
import { InvalidNodeError } from '../errors/invalid-node.error.js';
import { linkCommentTriggerPhrase, startsWithTriggerPhraseRegExp } from '../trigger-phrase.js';
import { isCodeBlock, isCommentNode, parseMarkdownContents } from './parse-markdown.js';
import { walk } from './walk.js';
function isExampleLinkComment(input) {
return isCommentNode(input) && input.value.trim().startsWith(linkCommentTriggerPhrase);
}
export function extractIndent(line, node) {
if (typeof node.value === 'string' && line.trim().startsWith(node.value)) {
return line.slice(0,
// column is 1 indexed so we must remove 1 from it
node.position.start.column - 1);
}
return '';
}
export function extractLinks(markdownFileContents) {
const parsedRoot = parseMarkdownContents(markdownFileContents);
const markdownLines = markdownFileContents.toString().split('\n');
const commentData = [];
let lastNode;
let lastHtmlNode;
walk(parsedRoot, 'markdown', (node, language) => {
const lastComment = commentData[commentData.length - 1];
if (language === 'markdown' && isHtmlNode(node)) {
assertFullyPositionedNode(node);
const htmlLine = markdownLines[
// line is 1 indexed
node.position.start.line - 1];
if (!htmlLine) {
throw new InvalidNodeError(node, `this Html node's position.start.line is not actually a valid line number from the file it's in`);
}
lastHtmlNode = {
htmlNode: node,
indent: extractIndent(htmlLine, node),
};
}
else if (language === 'html' && isExampleLinkComment(node)) {
if (!lastHtmlNode) {
throw new InvalidNodeError(node, 'encountered html node without first encountering html root node');
}
assertFullyPositionedNode(node);
const newNode = offsetNodePosition(node, lastHtmlNode.htmlNode);
node = newNode;
commentData.push({ comment: newNode, indent: lastHtmlNode.indent });
}
else if (language === 'markdown' &&
lastComment &&
lastNode === lastComment.comment &&
isCodeBlock(node)) {
assertFullyPositionedNode(node);
lastComment.codeBlock = node;
}
lastNode = node;
});
return commentData.map((comment) => {
assertFullyPositionedNode(comment.comment);
return {
node: comment.comment,
indent: comment.indent,
linkPath: comment.comment.value
.trim()
.replace(startsWithTriggerPhraseRegExp, '')
.trim(),
linkedCodeBlock: comment.codeBlock,
};
});
}
function offsetNodePosition(needsOffset, offsetBase) {
return {
...needsOffset,
position: {
...needsOffset.position,
start: {
...needsOffset.position.start,
line: offsetBase.position.start.line + needsOffset.position.start.line - 1,
offset: offsetBase.position.start.offset + needsOffset.position.start.offset,
},
end: {
...needsOffset.position.end,
line: offsetBase.position.start.line + needsOffset.position.end.line - 1,
offset: offsetBase.position.start.offset + needsOffset.position.end.offset,
},
},
};
}
function isHtmlNode(input) {
return input.type === 'html';
}
export function assertFullyPositionedNode(node) {
if (!node.position) {
throw new InvalidNodeError(node, 'missing position property');
}
if (node.position.end.offset == undefined) {
throw new InvalidNodeError(node, 'missing end position offset');
}
if (node.position.start.offset == undefined) {
throw new InvalidNodeError(node, 'missing start position offset');
}
}