renderPage

tcmts.site.renderPage · #vg2zfj5f · term · runs anywhere

One page, Markdown md, as plain, text-file-looking HTML: headings numbered like sections of a text file, code highlighted, two-column tables as lists of terms, images (IMAGES) in the page. Links to other pages go to their files; a link to something not on the site is shown as its path if elsewhere says it's a file of the repository, and is a problem otherwise.

import { esc, hrefFor, IMAGES, LANGS, plain, resolvePath, slug } from "#codebase/tcmts/site";
import type { Highlighter, Page, Rendered } from "#codebase/tcmts/site";
import { Marked } from "marked";
import type { Tokens } from "marked";

export function renderPage(
  page: Page,
  pages: Page[],
  md: string,
  hl: Highlighter,
  elsewhere: (src: string) => boolean,
  live: boolean,
  codeBrowser = !live,
): Rendered {
  const bySrc = new Map(pages.map((p) => [p.src, p]));
  const dir = page.src.slice(0, Math.max(0, page.src.lastIndexOf("/")));
  const r: Rendered = { html: "", title: page.label, toc: [], ids: [], links: [], problems: [] };
  let h2 = 0;
  let h3 = 0;
  const marked = new Marked({
    gfm: true,
    // `{@link tcmts.a.b}` (or `{@linkcode …}`, or with its own text after the name): a definition, by name (tcm
    // checked it when the page was saved, and it follows renames); on a site with the code browser (`codeBrowser`), its page
    // there, which isn't the app's (`rel="external"`).
    // Not in code, where it's written to be read as it is.
    extensions: [
      {
        name: "tcmLink",
        level: "inline",
        start: (src: string) => src.indexOf("{@link"),
        tokenizer(src: string) {
          const m = /^\{@link(code|plain)?\s+([\w$.-]+)(?:\s*\|\s*|\s+)?([^}]*)\}/.exec(src);
          return m
            ? {
                type: "tcmLink",
                raw: m[0],
                code: m[1] === "code",
                name: m[2]!,
                label: m[3]!.trim(),
              }
            : undefined;
        },
        renderer(token) {
          const { code, name, label } = token as unknown as {
            code: boolean;
            name: string;
            label: string;
          };
          const shown = label ? esc(label) : esc(name.slice(name.lastIndexOf(".") + 1));
          const inner = code || !label ? `<code>${shown}</code>` : shown;
          if (!codeBrowser || !name.startsWith("tcmts.")) return inner;
          const to = `code/tcm/${name.slice("tcmts.".length).replaceAll(".", "/")}.html`;
          return `<a href="${esc(to)}" rel="external" title="${esc(name)}">${inner}</a>`;
        },
      },
    ],
    renderer: {
      heading({ tokens, depth, text }: Tokens.Heading) {
        const inner = this.parser.parseInline(tokens);
        const id = slug(plain(text));
        r.ids.push(id);
        if (depth === 1) {
          r.title = plain(text);
          return `<h1 id="${id}">${page.n ? `${page.n}. ` : ""}${inner}</h1>\n<div class="rule" aria-hidden="true"></div>\n`;
        }
        const num = depth === 2 ? `${page.n}.${++h2}` : `${page.n}.${h2}.${++h3}`;
        if (depth === 2) h3 = 0;
        if (depth === 2) r.toc.push({ id, text: plain(text), num });
        return `<h${depth} id="${id}">${page.n ? `${num} ` : ""}${inner}</h${depth}>\n`;
      },
      link({ href, tokens }: Tokens.Link) {
        const text = this.parser.parseInline(tokens);
        if (/^[a-z]+:/i.test(href)) return `<a href="${esc(href)}">${text}</a>`;
        const [target, anchor] = href.split("#") as [string, string | undefined];
        // a link within the docs: to the page it names, or to an anchor on this one
        const resolved = target ? resolvePath(dir, target) : page.src;
        const to = bySrc.get(resolved);
        if (!to) {
          // a file in the repository but not on the site (source code, AGENTS.md): shown as its path
          if (elsewhere(resolved)) return `${text} <span class="path">(${esc(resolved)})</span>`;
          r.problems.push(`link to ${href}: no such page`);
          return text;
        }
        r.links.push({ file: to.file, ...(anchor ? { anchor } : {}), text: href });
        return `<a href="${hrefFor(to.file, live)}${anchor ? `#${anchor}` : ""}">${text}</a>`;
      },
      // an image (`![what it shows](images/tcm.svg)`, one of IMAGES): in the page, at its width; pressed, it's the
      // page's target, which shows it full size over the page, with a link (to nothing) that closes it
      image({ href, text }: Tokens.Image) {
        const src = resolvePath(dir, href);
        const svg = IMAGES[src];
        if (svg === undefined) {
          r.problems.push(`image ${href}: no such image (tcmts.site.IMAGES)`);
          return esc(text);
        }
        const id = `image-${slug(src.slice(src.lastIndexOf("/") + 1).replace(/\.svg$/, ""))}`;
        r.ids.push(id);
        const url = `data:image/svg+xml,${svg.replace(/[\n%#"&]/g, encodeURIComponent)}`;
        return (
          `<a class="image" id="${id}" href="#${id}" title="Full size"><img src="${url}" alt="${esc(text)}"></a>` +
          `<a class="image-close" href="#${id}-closed">[close]</a>`
        );
      },
      code({ text, lang }: Tokens.Code) {
        if (lang && LANGS.includes(lang))
          return (
            hl.codeToHtml(text, {
              lang,
              themes: { light: "github-light", dark: "github-dark" },
              defaultColor: false,
            }) + "\n"
          );
        return `<pre${lang ? ` data-lang="${esc(lang)}"` : ""}>${esc(text)}</pre>\n`;
      },
      table(token: Tokens.Table) {
        const cell = (c: Tokens.TableCell) => this.parser.parseInline(c.tokens);
        // a two-column table is a list of terms and what they mean: shown the man-page way, the term on its line
        // and the description indented under it, so long terms (commands with all their flags) wrap freely
        if (token.header.length === 2) {
          const rows = token.rows
            .map((row) => `<dt>${cell(row[0]!)}</dt><dd>${cell(row[1]!)}</dd>`)
            .join("\n");
          return `<dl>\n${rows}\n</dl>\n`;
        }
        const head = `<tr>${token.header.map((c) => `<th>${cell(c)}</th>`).join("")}</tr>`;
        const body = token.rows
          .map((row) => `<tr>${row.map((c) => `<td>${cell(c)}</td>`).join("")}</tr>`)
          .join("\n");
        return `<div class="table"><table>\n<thead>${head}</thead>\n<tbody>${body}</tbody>\n</table></div>\n`;
      },
    },
  });
  r.html = marked.parse(md) as string;
  return r;
}

uses

used by