Something went wrong. Try again.
A proposal for resolving a short, human-readable name of the form @handle/name into loaded, attributable web content, using AT Protocol iden atproto-web-names.szzt.app
Something went wrong. Try again.
9.7 kB · 196 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197import { Marked, Renderer } from "marked";import { load } from "cheerio";
export const escapeHtml = value => String(value).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'",})[c]);
export function slug(text) { return text.toLowerCase().replace(/[`*_]/g, "").replace(/[^\p{L}\p{N}\s-]/gu, "") .trim().replace(/[\s-]+/g, "-");}
/** Only transform Markdown prose tokens: examples and code must remain verbatim. */export function convert(source, publication) { const markdown = new Marked({ gfm: true }); const tokens = markdown.lexer(source); // This source uses ATX headings. Catch missing blank lines before dividers // instead of silently turning preceding prose into a setext heading. const unexpectedHeading = tokens.find(t => t.type === "heading" && !t.raw.startsWith("#")); if (unexpectedHeading) throw new Error(`Unexpected setext heading; check divider whitespace: ${unexpectedHeading.text}`); const sections = []; const references = new Map(); const legacyIds = new Map(); const ids = new Set(["abstract", "sotd", "conformance"]); const unresolved = new Set(); const diagrams = []; let topNumber = 0;
for (const token of tokens) { if (token.type !== "heading" || token.depth === 1) continue; const match = /^(?:Appendix )?([A-C]|\d+)((?:\.\d+)*)\.?(?:\s+)(.+)$/u.exec(token.text); const key = match ? match[1] + match[2] : null; const title = match ? match[3] : token.text; let id = ({ Abstract: "abstract", "Status of this document": "sotd", Conformance: "conformance" })[title]; if (!id) { const base = slug(title.replace(/\s*\(Sections?[^)]*\)/g, "")); id = base; if (ids.has(id)) id = `${sections.findLast(s => s.depth === 2)?.id ?? "section"}-${base}`; if (ids.has(id)) throw new Error(`Duplicate section ID: ${id}`); } ids.add(id); if (key && token.depth === 2 && /^\d/.test(key)) topNumber++; const number = key && /^\d/.test(key) ? `${topNumber}${match[2]}` : key; if (key && key !== number) throw new Error(`Source section ${key} must be numbered ${number} to match the rendered document.`); const section = { key, title, id, number, depth: token.depth, token }; sections.push(section); if (key) references.set(key, section); legacyIds.set(slug(token.text), id); }
function linkReference(key) { const target = references.get(key); if (!target) { unresolved.add(key); return escapeHtml(key); } return `<a href="#${target.id}" class="source-section-ref" data-source-section="${key}">${target.number}</a>`; }
function sectionReferences(text, linked = true) { return text.replace(/\b(Sections?\s+|Appendix\s+)([A-C](?:\.\d+)*|\d+(?:\.\d+)*)(?:\s*[–,]\s*(?:[A-C](?:\.\d+)*|\d+(?:\.\d+)*))*/g, whole => { const prefix = /^(Sections?\s+|Appendix\s+)/.exec(whole)[0]; return prefix + whole.slice(prefix.length).replace(/[A-C](?:\.\d+)*|\d+(?:\.\d+)*/g, key => linked ? linkReference(key) : (references.get(key)?.number ?? key)); }); } const prose = text => sectionReferences(text).replace(/\bRFC (2119|8174)\b/g, "[[!RFC$1]]");
class SpecRenderer extends Renderer { text(token) { // Tokens with children contain inline markup: let Marked visit the children. return token.tokens ? this.parser.parseInline(token.tokens) : prose(super.text(token)); } link(token) { const href = token.href.startsWith("#") ? `#${legacyIds.get(token.href.slice(1)) ?? token.href.slice(1)}` : token.href; return `<a href="${escapeHtml(href)}"${token.title ? ` title="${escapeHtml(token.title)}"` : ""}>${this.parser.parseInline(token.tokens)}</a>`; } code(token) { if (token.lang === "mermaid") { const name = `lifecycle${diagrams.length ? `-${diagrams.length + 1}` : ""}`; diagrams.push({ name, source: sectionReferences(token.text, false) }); return `<figure id="fig-${name}"><img src="assets/${name}.svg" alt="Resolution and loading lifecycle: verify identity and records; reject invalid proofs; redirect asserted targets; load verified bytes into a derived origin."><figcaption>Resolution and loading lifecycle. <a href="assets/${name}.svg">Open the full-size diagram</a>.</figcaption></figure>\n`; } // Disallow autolinks in examples. ReSpec adds highlighting at build time. const lang = /^[a-z0-9-]+$/i.test(token.lang ?? "") ? token.lang : "nohighlight"; const text = token.lang ? token.text : sectionReferences(token.text, false); return `<pre class="${lang} nolinks"><code>${escapeHtml(text)}\n</code></pre>\n`; } } const renderer = new SpecRenderer(); markdown.use({ renderer: { text: renderer.text, link: renderer.link, code: renderer.code } }); const render = list => markdown.parser(list); const html = []; const stack = []; let current; let body = []; let titlePreamble = []; let skip = false;
const flush = () => { if (!current) return; if (current.title === "Table of contents") { body = []; return; } // ReSpec supplies the equivalent BCP 14 paragraph; retain the document-specific rules. if (current.id === "conformance") { const firstParagraph = body.findIndex(t => t.type === "paragraph"); if (firstParagraph < 0 || !body[firstParagraph].text.includes("RFC 2119 and RFC 8174")) { throw new Error("Conformance preamble changed; review conversion before removing it."); } body.splice(firstParagraph, 1); } html.push(render(body)); body = []; };
for (const token of tokens) { if (token.type === "heading") { if (token.depth === 1) { skip = true; continue; } flush(); current = sections.find(s => s.token === token); skip = current.title === "Table of contents"; if (skip) continue; while (stack.length && stack.at(-1) >= token.depth) { html.push("</section>"); stack.pop(); } const classes = []; if (current.id === "conformance") classes.push("introductory"); if (/^[A-C]/.test(current.key ?? "")) classes.push("appendix", "informative"); html.push(`<section id="${current.id}"${classes.length ? ` class="${classes.join(" ")}"` : ""}>`); stack.push(token.depth); // ReSpec supplies the abstract and SotD headings itself. if (!["abstract", "sotd"].includes(current.id)) html.push(`<h${token.depth}>${markdown.parseInline(current.title)}</h${token.depth}>`); if (current.id === "sotd") html.push(render(titlePreamble)); } else if (!current) { if (skip && token.type !== "space") titlePreamble.push(token); } else if (!skip && token.type !== "hr") { body.push(token); } } flush(); while (stack.length) { html.push("</section>"); stack.pop(); }
const $ = load(`<body>${html.join("\n")}</body>`); // Promote only the explicitly defined vocabulary in the Terminology section. const terms = []; $("#terminology p > strong:first-child").each((_, el) => { const label = $(el).text(); if (!label.endsWith(".")) return; const names = label.slice(0, -1).split(" / "); const replacement = names.map(name => { const id = `dfn-${slug(name)}`; terms.push({ name, id }); return `<dfn id="${id}">${escapeHtml(name)}</dfn>`; }).join(" / ") + "."; $(el).replaceWith(replacement); });
$("#terminology").children("h2").after(`<nav class="term-index" aria-label="Defined terms"><p><strong>Definitions:</strong> ${terms.map(({ name, id }) => `<a href="#${id}">${escapeHtml(name)}</a>`).join(" · ")}</p></nav>`);
const config = { specStatus: publication.specStatus, shortName: publication.shortName, ...(publication.latestVersion ? { latestVersion: publication.latestVersion } : {}), subtitle: publication.subtitle, editors: publication.editors, ...(publication.publishDate ? { publishDate: publication.publishDate } : {}), maxTocLevel: 3, xref: false, localBiblio: { RFC2119: { title: "Key words for use in RFCs to Indicate Requirement Levels", href: "https://www.rfc-editor.org/rfc/rfc2119", authors: ["S. Bradner"], status: "BCP 14", publisher: "IETF", date: "March 1997" }, RFC8174: { title: "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", href: "https://www.rfc-editor.org/rfc/rfc8174", authors: ["B. Leiba"], status: "BCP 14", publisher: "IETF", date: "May 2017" }, }, }; const document = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(publication.title)}</title><link rel="stylesheet" href="assets/spec.css"><script class="remove">var respecConfig = ${JSON.stringify(config).replaceAll("<", "\\u003c")};</script><script class="remove" defer src="/node_modules/respec/builds/respec-w3c.js"></script></head><body><p class="copyright">${escapeHtml(publication.rightsNotice)}</p>${$("body").html()}</body></html>`; return { html: document, diagrams, report: { preview: publication.preview, source: "spec.md", sectionCount: sections.filter(s => s.key).length, sections: sections.filter(s => s.key).map(({ key, number, title, id }) => ({ sourceNumber: key, number, title, id })), definitionCount: terms.length, unresolvedSourceReferences: [...unresolved].sort(), publicationPlaceholders: [ ...(publication.editors.some(e => /pending/i.test(e.name)) ? ["Editor attribution"] : []), ...(/not yet selected/i.test(publication.rightsNotice) ? ["Document license/rights notice"] : []), ], }, };}