var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) { function accept(f) { if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected"); return f; } var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value"; var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null; var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {}); var _, done = false; for (var i = decorators.length - 1; i >= 0; i--) { var context = {}; for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p]; for (var p in contextIn.access) context.access[p] = contextIn.access[p]; context.addInitializer = function (f) { if (done) throw new TypeError("Cannot add initializers after decoration has completed"); extraInitializers.push(accept(f || null)); }; var result = (0, decorators[i])(kind === "accessor" ? { get: descriptor.get, set: descriptor.set } : descriptor[key], context); if (kind === "accessor") { if (result === void 0) continue; if (result === null || typeof result !== "object") throw new TypeError("Object expected"); if (_ = accept(result.get)) descriptor.get = _; if (_ = accept(result.set)) descriptor.set = _; if (_ = accept(result.init)) initializers.unshift(_); } else if (_ = accept(result)) { if (kind === "field") initializers.unshift(_); else descriptor[key] = _; } } if (target) Object.defineProperty(target, contextIn.name, descriptor); done = true; }; var __runInitializers = (this && this.__runInitializers) || function (thisArg, initializers, value) { var useValue = arguments.length > 2; for (var i = 0; i < initializers.length; i++) { value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg); } return useValue ? value : void 0; }; import MarkdownIt from "markdown-it"; import { ContextAwareRendererComponent } from "../components.js"; import { MarkdownEvent, RendererEvent } from "../events.js"; import { Option } from "../../utils/index.js"; import { highlight, isLoadedLanguage, isSupportedLanguage } from "../../utils/highlighter.js"; import { assertNever, escapeHtml, i18n, JSX } from "#utils"; import { anchorIcon } from "./default/partials/anchor-icon.js"; import { Reflection, ReflectionKind, } from "../../models/index.js"; function getFriendlyFullName(target) { if (target instanceof Reflection) { return target.getFriendlyFullName(); } if (target.parent) { return target.name; } const parts = [target.name]; let current = target; while (current.parent) { parts.unshift(current.name); current = current.parent; } return parts.join("."); } /** * Implements markdown and relativeURL helpers for templates. * @internal */ let MarkedPlugin = (() => { let _classSuper = ContextAwareRendererComponent; let _lightTheme_decorators; let _lightTheme_initializers = []; let _lightTheme_extraInitializers = []; let _darkTheme_decorators; let _darkTheme_initializers = []; let _darkTheme_extraInitializers = []; let _markdownItOptions_decorators; let _markdownItOptions_initializers = []; let _markdownItOptions_extraInitializers = []; let _markdownLinkExternal_decorators; let _markdownLinkExternal_initializers = []; let _markdownLinkExternal_extraInitializers = []; let _validation_decorators; let _validation_initializers = []; let _validation_extraInitializers = []; return class MarkedPlugin extends _classSuper { static { const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0; _lightTheme_decorators = [Option("lightHighlightTheme")]; _darkTheme_decorators = [Option("darkHighlightTheme")]; _markdownItOptions_decorators = [Option("markdownItOptions")]; _markdownLinkExternal_decorators = [Option("markdownLinkExternal")]; _validation_decorators = [Option("validation")]; __esDecorate(this, null, _lightTheme_decorators, { kind: "accessor", name: "lightTheme", static: false, private: false, access: { has: obj => "lightTheme" in obj, get: obj => obj.lightTheme, set: (obj, value) => { obj.lightTheme = value; } }, metadata: _metadata }, _lightTheme_initializers, _lightTheme_extraInitializers); __esDecorate(this, null, _darkTheme_decorators, { kind: "accessor", name: "darkTheme", static: false, private: false, access: { has: obj => "darkTheme" in obj, get: obj => obj.darkTheme, set: (obj, value) => { obj.darkTheme = value; } }, metadata: _metadata }, _darkTheme_initializers, _darkTheme_extraInitializers); __esDecorate(this, null, _markdownItOptions_decorators, { kind: "accessor", name: "markdownItOptions", static: false, private: false, access: { has: obj => "markdownItOptions" in obj, get: obj => obj.markdownItOptions, set: (obj, value) => { obj.markdownItOptions = value; } }, metadata: _metadata }, _markdownItOptions_initializers, _markdownItOptions_extraInitializers); __esDecorate(this, null, _markdownLinkExternal_decorators, { kind: "accessor", name: "markdownLinkExternal", static: false, private: false, access: { has: obj => "markdownLinkExternal" in obj, get: obj => obj.markdownLinkExternal, set: (obj, value) => { obj.markdownLinkExternal = value; } }, metadata: _metadata }, _markdownLinkExternal_initializers, _markdownLinkExternal_extraInitializers); __esDecorate(this, null, _validation_decorators, { kind: "accessor", name: "validation", static: false, private: false, access: { has: obj => "validation" in obj, get: obj => obj.validation, set: (obj, value) => { obj.validation = value; } }, metadata: _metadata }, _validation_initializers, _validation_extraInitializers); if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata }); } #lightTheme_accessor_storage = __runInitializers(this, _lightTheme_initializers, void 0); get lightTheme() { return this.#lightTheme_accessor_storage; } set lightTheme(value) { this.#lightTheme_accessor_storage = value; } #darkTheme_accessor_storage = (__runInitializers(this, _lightTheme_extraInitializers), __runInitializers(this, _darkTheme_initializers, void 0)); get darkTheme() { return this.#darkTheme_accessor_storage; } set darkTheme(value) { this.#darkTheme_accessor_storage = value; } #markdownItOptions_accessor_storage = (__runInitializers(this, _darkTheme_extraInitializers), __runInitializers(this, _markdownItOptions_initializers, void 0)); get markdownItOptions() { return this.#markdownItOptions_accessor_storage; } set markdownItOptions(value) { this.#markdownItOptions_accessor_storage = value; } #markdownLinkExternal_accessor_storage = (__runInitializers(this, _markdownItOptions_extraInitializers), __runInitializers(this, _markdownLinkExternal_initializers, void 0)); get markdownLinkExternal() { return this.#markdownLinkExternal_accessor_storage; } set markdownLinkExternal(value) { this.#markdownLinkExternal_accessor_storage = value; } #validation_accessor_storage = (__runInitializers(this, _markdownLinkExternal_extraInitializers), __runInitializers(this, _validation_initializers, void 0)); get validation() { return this.#validation_accessor_storage; } set validation(value) { this.#validation_accessor_storage = value; } parser = __runInitializers(this, _validation_extraInitializers); renderedRelativeLinks = []; /** * This needing to be here really feels hacky... probably some nicer way to do this. * Revisit in 0.28. */ renderContext = null; lastHeaderSlug = ""; constructor(owner) { super(owner); this.owner.on(MarkdownEvent.PARSE, this.onParseMarkdown.bind(this)); this.owner.on(RendererEvent.END, this.onEnd.bind(this)); } /** * Highlight the syntax of the given text using Shiki. * * @param text The text that should be highlighted. * @param lang The language that should be used to highlight the string. * @return A html string with syntax highlighting. */ getHighlighted(text, lang) { lang = lang || "typescript"; lang = lang.toLowerCase(); if (!isSupportedLanguage(lang)) { this.application.logger.warn(i18n.unsupported_highlight_language_0_not_highlighted_in_comment_for_1(lang, getFriendlyFullName(this.page?.model || { name: "(unknown)" }))); return text; } if (!isLoadedLanguage(lang)) { this.application.logger.warn(i18n.unloaded_language_0_not_highlighted_in_comment_for_1(lang, getFriendlyFullName(this.page?.model || { name: "(unknown)" }))); return text; } return highlight(text, lang); } /** * Parse the given markdown string and return the resulting html. * * @param input The markdown string that should be parsed. * @returns The resulting html string. */ parseMarkdown(input, page, context) { let markdown = input; if (typeof markdown !== "string") { markdown = this.displayPartsToMarkdown(page, context, markdown); } this.renderContext = context; const event = new MarkdownEvent(page, markdown, markdown); this.owner.trigger(MarkdownEvent.PARSE, event); this.renderContext = null; return event.parsedText; } displayPartsToMarkdown(page, context, parts) { const useHtml = !!this.markdownItOptions["html"]; const result = []; for (const part of parts) { switch (part.kind) { case "text": case "code": result.push(part.text); break; case "inline-tag": switch (part.tag) { case "@label": case "@inheritdoc": // Shouldn't happen break; // Not rendered. case "@link": case "@linkcode": case "@linkplain": { if (part.target) { let url; let kindClass; if (typeof part.target === "string") { url = part.target === "#" ? undefined : part.target; } else if ("id" in part.target) { // No point in trying to resolve a ReflectionSymbolId at this point, we've already // tried and failed during the resolution step. Warnings related to those broken links // have already been emitted. kindClass = ReflectionKind.classString(part.target.kind); if (context.router.hasUrl(part.target)) { url = context.urlTo(part.target); } // If we don't have a URL the user probably linked to some deeply nested property // which doesn't get an assigned URL. We'll walk upwards until we find a reflection // which has a URL and link to that instead. if (typeof url === "undefined") { // Walk upwards to find something we can link to. let target = part.target.parent; while (!context.router.hasUrl(target)) { target = target.parent; } // We know we'll always end up with a URL here eventually as the // project always has a URL. url = context.urlTo(target); if (this.validation.rewrittenLink) { this.application.logger.warn(i18n .reflection_0_links_to_1_with_text_2_but_resolved_to_3(page.model.getFriendlyFullName(), part.target.getFriendlyFullName(), part.text, target.getFriendlyFullName())); } } // If the url goes to this page, render as `#` // to go to the top of the page. if (url == "") { url = "#"; } } if (useHtml) { const text = part.tag === "@linkcode" ? `${part.text}` : part.text; result.push(url ? `${text}` : part.text); } else { const text = part.tag === "@linkcode" ? "`" + part.text + "`" : part.text; result.push(url ? `[${text}](${url})` : text); } } else { result.push(part.text); } break; } default: // Hmm... probably want to be able to render these somehow, so custom inline tags can be given // special rendering rules. Future capability. For now, just render their text. result.push(`{${part.tag} ${part.text}}`); break; } break; case "relative-link": switch (typeof part.target) { case "number": { const refl = page.project.files.resolve(part.target, page.model.project); let url; if (typeof refl === "object") { url = context.urlTo(refl); } else { const fileName = page.project.files.getName(part.target); if (fileName) { url = context.relativeURL(`media/${fileName}`); } } if (typeof url !== "undefined") { if (part.targetAnchor) { url += "#" + part.targetAnchor; if (typeof refl === "object") { this.renderedRelativeLinks.push({ source: this.page.model, target: refl, link: part, }); } } result.push(url); break; } } // fall through case "undefined": result.push(part.text); break; } break; default: assertNever(part); } } return result.join(""); } onEnd() { for (const { source, target, link } of this.renderedRelativeLinks) { const slugger = this.owner.router.getSlugger(target); if (!slugger.hasAnchor(link.targetAnchor)) { this.application.logger.warn(i18n.reflection_0_links_to_1_but_anchor_does_not_exist_try_2(getFriendlyFullName(source), link.text, slugger .getSimilarAnchors(link.targetAnchor) .map((a) => link.text.replace(/#.*/, "#" + a)) .join("\n\t"))); } } // In case we're in watch mode this.renderedRelativeLinks = []; } /** * Triggered before the renderer starts rendering a project. * * @param event An event object describing the current render operation. */ onBeginRenderer(event) { super.onBeginRenderer(event); this.setupParser(); } getSlugger() { return this.owner.router.getSlugger(this.page.model); } /** * Creates an object with options that are passed to the markdown parser. * * @returns The options object for the markdown parser. */ setupParser() { this.parser = MarkdownIt({ ...this.markdownItOptions, highlight: (code, lang) => { code = this.getHighlighted(code, lang || "ts"); code = code.replace(/\n$/, "") + "\n"; if (!lang) { return `
${code}
\n`; } return `
${code}
\n`; }, }); githubAlertMarkdownPlugin(this.parser); const loader = this.application.options.getValue("markdownItLoader"); loader(this.parser); // Add anchor links for headings in readme, and add them to the "On this page" section this.parser.renderer.rules["heading_open"] = (tokens, idx) => { const token = tokens[idx]; const content = getTokenTextContent(tokens[idx + 1]); const level = token.markup.length; const slug = this.getSlugger().slug(content); this.lastHeaderSlug = slug; this.page.pageHeadings.push({ link: `#${slug}`, text: content, level, }); return `<${token.tag} id="${slug}" class="tsd-anchor-link">`; }; this.parser.renderer.rules["heading_close"] = (tokens, idx) => { return `${JSX.renderElement(anchorIcon(this.renderContext, this.lastHeaderSlug))}`; }; // Rewrite anchor links inline in a readme file to links targeting the `md:` prefixed anchors // that TypeDoc creates. this.parser.renderer.rules["link_open"] = (tokens, idx, options, _env, self) => { const token = tokens[idx]; const href = token.attrGet("href"); if (href) { // Note: This doesn't catch @link tags to reflections as those // will be relative links. This will likely have to change with // the introduction of support for customized routers whenever // that becomes a real thing. if (this.markdownLinkExternal && /^https?:\/\//i.test(href) && !(href + "/").startsWith(this.hostedBaseUrl)) { token.attrSet("target", "_blank"); const classes = token.attrGet("class")?.split(" ") || []; classes.push("external"); token.attrSet("class", classes.join(" ")); } token.attrSet("href", href); } return self.renderToken(tokens, idx, options); }; this.parser.renderer.rules["alert_open"] = (tokens, idx) => { const icon = this.renderContext.icons[tokens[idx].attrGet("icon")]; const iconHtml = JSX.renderElement(icon()); return `
${iconHtml}${tokens[idx].attrGet("alert")}
`; }; } /** * Triggered when {@link MarkedPlugin} parses a markdown string. * * @param event */ onParseMarkdown(event) { event.parsedText = this.parser.render(event.parsedText); } }; })(); export { MarkedPlugin }; function getTokenTextContent(token) { if (token.children) { return token.children.map(getTokenTextContent).join(""); } return token.content; } const kindNames = ["note", "tip", "important", "warning", "caution"]; const iconNames = ["alertNote", "alertTip", "alertImportant", "alertWarning", "alertCaution"]; const kindTranslations = [ () => i18n.alert_note(), () => i18n.alert_tip(), () => i18n.alert_important(), () => i18n.alert_warning(), () => i18n.alert_caution(), ]; function githubAlertMarkdownPlugin(md) { md.core.ruler.after("block", "typedoc-github-alert-plugin", (state) => { const bqStarts = []; for (let i = 0; i < state.tokens.length; ++i) { const token = state.tokens[i]; if (token.type === "blockquote_open") { bqStarts.push(i); } else if (token.type === "blockquote_close") { if (bqStarts.length === 1) { checkForAlert(state.tokens, bqStarts[0], i); } bqStarts.pop(); } } }); } function checkForAlert(tokens, start, end) { let alertKind = -1; // Search for the first "inline" token. That will be the blockquote text. for (let i = start; i < end; ++i) { if (tokens[i].type === "inline") { // Check for `[!NOTE]` const kindString = tokens[i].content.match(/^\[!(\w+)\]/); const kindIndex = kindNames.indexOf(kindString?.[1].toLowerCase() || ""); if (kindIndex !== -1) { tokens[i].content = tokens[i].content.substring(kindString[0].length); alertKind = kindIndex; } break; } } // If we found an alert, then replace the blockquote_open and blockquote_close tokens with // alert_open and alert_close tokens that can be rendered specially. if (alertKind === -1) return; tokens[start].type = "alert_open"; tokens[start].tag = "div"; tokens[start].attrPush(["class", `tsd-alert tsd-alert-${kindNames[alertKind]}`]); tokens[start].attrPush(["alert", kindTranslations[alertKind]()]); tokens[start].attrPush(["icon", iconNames[alertKind]]); tokens[end].type = "alert_close"; tokens[end].tag = "div"; }