typedoc
Version:
Create api documentation for TypeScript projects.
243 lines (242 loc) • 8.79 kB
JavaScript
import { HtmlAttributeParser, ParserState } from "../../utils/html.js";
import { TokenSyntaxKind } from "./lexer.js";
import MarkdownIt from "markdown-it";
const MdHelpers = new MarkdownIt().helpers;
/**
* This is incredibly unfortunate. The comment lexer owns the responsibility
* for splitting up text into text/code, this is totally fine for HTML links
* but for markdown links, ``[`code`](./link)`` is valid, so we need to keep
* track of state across calls to {@link textContent}.
*/
export class TextParserReentryState {
withinLinkLabel = false;
lastPartWasNewline = false;
checkState(token) {
switch (token.kind) {
case TokenSyntaxKind.Code:
if (/\n\s*\n/.test(token.text)) {
this.withinLinkLabel = false;
}
break;
case TokenSyntaxKind.NewLine:
if (this.lastPartWasNewline) {
this.withinLinkLabel = false;
}
break;
}
this.lastPartWasNewline = token.kind === TokenSyntaxKind.NewLine;
}
}
/**
* Look for relative links within a piece of text and add them to the {@link FileRegistry}
* so that they can be correctly resolved during rendering.
*/
export function textContent(sourcePath, token, i18n, warning, outContent, files, atNewLine, reentry) {
let lastPartEnd = 0;
const data = {
sourcePath,
token,
pos: 0, // relative to the token
i18n,
warning,
files: files,
atNewLine,
};
function addRef(ref) {
outContent.push({
kind: "text",
text: token.text.slice(lastPartEnd, ref.pos),
});
const link = {
kind: "relative-link",
text: token.text.slice(ref.pos, ref.end),
target: ref.target,
targetAnchor: ref.targetAnchor,
};
outContent.push(link);
lastPartEnd = ref.end;
data.pos = ref.end;
if (!ref.target) {
warning(i18n.relative_path_0_is_not_a_file_and_will_not_be_copied_to_output(token.text.slice(ref.pos, ref.end)), {
kind: TokenSyntaxKind.Text,
// ref.pos is relative to the token, but this pos is relative to the file.
pos: token.pos + ref.pos,
text: token.text.slice(ref.pos, ref.end),
});
}
}
while (data.pos < token.text.length) {
const link = checkMarkdownLink(data, reentry);
if (link) {
addRef(link);
continue;
}
const reference = checkReference(data);
if (reference) {
addRef(reference);
continue;
}
const tagLink = checkTagLink(data);
if (tagLink) {
addRef(tagLink);
continue;
}
data.atNewLine = token.text[data.pos] === "\n";
++data.pos;
}
if (lastPartEnd !== token.text.length) {
outContent.push({ kind: "text", text: token.text.slice(lastPartEnd) });
}
}
/**
* Links are inline text with the form `[ text ]( url title )`.
*
* Images are just links with a leading `!` and lack of support for `[ref]` referring to a path
* defined elsewhere, we don't care about that distinction here as we'll only replace the path
* piece of the image.
*
* Reference: https://github.com/markdown-it/markdown-it/blob/14.1.0/lib/rules_inline/link.mjs
* Reference: https://github.com/markdown-it/markdown-it/blob/14.1.0/lib/rules_inline/image.mjs
*
*/
function checkMarkdownLink(data, reentry) {
const { token, sourcePath, files } = data;
let searchStart;
if (reentry.withinLinkLabel) {
searchStart = data.pos;
reentry.withinLinkLabel = false;
}
else if (token.text[data.pos] === "[") {
searchStart = data.pos + 1;
}
else {
return;
}
const labelEnd = findLabelEnd(token.text, searchStart);
if (labelEnd === -1) {
// This markdown link might be split across multiple display parts
// [ `text` ](link)
// ^^ text
// ^^^^^^ code
// ^^^^^^^^ text
reentry.withinLinkLabel = true;
return;
}
if (token.text[labelEnd] === "]" && token.text[labelEnd + 1] === "(") {
const link = MdHelpers.parseLinkDestination(token.text, labelEnd + 2, token.text.length);
if (link.ok) {
// Only make a relative-link display part if it's actually a relative link.
// Discard protocol:// links, unix style absolute paths, and windows style absolute paths.
if (isRelativePath(link.str)) {
const { target, anchor } = files.register(sourcePath, link.str) || { target: undefined, anchor: undefined };
return {
pos: labelEnd + 2,
end: link.pos,
target,
targetAnchor: anchor,
};
}
// This was a link, skip ahead to ensure we don't happen to parse
// something else as a link within the link.
data.pos = link.pos - 1;
}
}
}
/**
* Reference definitions are blocks with the form `[label]: link title`
* Reference: https://github.com/markdown-it/markdown-it/blob/14.1.0/lib/rules_block/reference.mjs
*
* Note: This may include false positives where TypeDoc recognizes a reference block that markdown
* does not if users start lines with something that looks like a reference block without fully
* separating it from an above paragraph. For a first cut, this is good enough.
*/
function checkReference(data) {
const { atNewLine, pos, token, files, sourcePath } = data;
if (atNewLine) {
let lookahead = pos;
while (/[ \t]/.test(token.text[lookahead])) {
++lookahead;
}
if (token.text[lookahead] === "[") {
while (lookahead < token.text.length &&
/[^\n\]]/.test(token.text[lookahead])) {
++lookahead;
}
if (token.text.startsWith("]:", lookahead)) {
lookahead += 2;
while (/[ \t]/.test(token.text[lookahead])) {
++lookahead;
}
const link = MdHelpers.parseLinkDestination(token.text, lookahead, token.text.length);
if (link.ok) {
if (isRelativePath(link.str)) {
const { target, anchor } = files.register(sourcePath, link.str) || { target: undefined, anchor: undefined };
return {
pos: lookahead,
end: link.pos,
target,
targetAnchor: anchor,
};
}
data.pos = link.pos - 1;
}
}
}
}
}
/**
* Looks for `<a href="./relative">` and `<img src="./relative">`
*/
function checkTagLink(data) {
const { pos, token } = data;
if (token.text.startsWith("<img ", pos)) {
data.pos += 4;
return checkAttribute(data, "src");
}
if (token.text.startsWith("<a ", pos)) {
data.pos += 3;
return checkAttribute(data, "href");
}
}
function checkAttribute(data, attr) {
const parser = new HtmlAttributeParser(data.token.text, data.pos);
while (parser.state !== ParserState.END) {
if (parser.state === ParserState.BeforeAttributeValue &&
parser.currentAttributeName === attr) {
parser.step();
if (isRelativePath(parser.currentAttributeValue)) {
data.pos = parser.pos;
const { target, anchor } = data.files.register(data.sourcePath, parser.currentAttributeValue) || { target: undefined, anchor: undefined };
return {
pos: parser.currentAttributeValueStart,
end: parser.currentAttributeValueEnd,
target,
targetAnchor: anchor,
};
}
return;
}
parser.step();
}
}
function isRelativePath(link) {
// Lots of edge cases encoded right here!
// Originally, this attempted to match protocol://, but...
// `mailto:example@example.com` is not a relative path
// `C:\foo` is not a relative path
// `/etc/passwd` is not a relative path
// `#anchor` is not a relative path
return !/^[a-z]+:|^\/|^#/i.test(link);
}
function findLabelEnd(text, pos) {
while (pos < text.length) {
switch (text[pos]) {
case "\n":
case "]":
case "[":
return pos;
}
++pos;
}
return -1;
}