import { language as bash } from "@twinkleplop/bash"; import { language as css } from "@twinkleplop/css"; import { language as html } from "@twinkleplop/html"; import { language as javascript } from "@twinkleplop/javascript"; import { language as json } from "@twinkleplop/json"; import { language as markdown } from "@twinkleplop/markdown"; import { language as rust } from "@twinkleplop/rust"; import { language as toml } from "@twinkleplop/toml"; import { language as typescript } from "@twinkleplop/typescript"; import { language as yaml } from "@twinkleplop/yaml"; import type { Build } from "bongo"; import type { Fence } from "@bongo/markdown"; const NAMES: Record = { bash: "Shell", css: "CSS", diff: "Diff", html: "HTML", javascript: "JavaScript", js: "JavaScript", json: "JSON", jsonc: "JSON", markdown: "Markdown", md: "Markdown", pkl: "Pkl", rs: "Rust", rust: "Rust", scss: "SCSS", sh: "Shell", toml: "TOML", ts: "TypeScript", typescript: "TypeScript", vue: "Vue", xml: "XML", yaml: "YAML", yml: "YAML", }; const ESCAPE: Record = { "&": "&", "<": "<", ">": ">", '"': """, }; const escape = (text: string) => text.replace(/[&<>"]/g, (char) => ESCAPE[char]); const JSON_LIKE = new Set(["json", "jsonc", "json5"]); /** * Twinkleplop tags a JSON key and a string value alike, so an object comes out * one flat colour. A string that a `:` follows is a key: tag it as a property, * which is what the YAML and TOML grammars already do. */ function keys(html: string, lang: string): string { if (!JSON_LIKE.has(lang)) return html; return html.replace( /((?:(?!<\/span>)[\s\S])*)<\/span>(:<\/span>)/g, '$1$2', ); } const HIGHLIGHTERS: Record string> = { bash: bash(), css: css(), html: html(), javascript: javascript(), json: json(), markdown: markdown(), rust: rust(), toml: toml(), typescript: typescript(), yaml: yaml(), }; /** * The twinkleplop highlighters for each fence language the docs use. Vue and * SCSS borrow the HTML and CSS grammars, as tidy itself treats them. */ export const languages = { ...HIGHLIGHTERS, scss: "css", less: "css", vue: "html", xml: "html", pkl: "typescript", }; /** * Give each code block a title bar with its file name or language, and a copy * button. A block with no language gets a plain frame, as terminal transcripts * do. */ export const fence: Fence = (html, block) => { if (!block.lang) { return `
${escape(block.code)}
`; } // The name of a file says more than the name of its language, and its // extension usually says the language anyway. let label = block.filename ? `${escape(block.filename)}` : `${NAMES[block.lang] ?? escape(block.lang)}`; return `
${label}
${keys(html, block.lang)}
`; }; // A code span this short never wraps, so an option name keeps its dashes. const SHORT = 32; /** * Let wide tables scroll, and mark each short code span and each table cell of * code spans alone, such as `--eol, --quote`, which stay on one line. */ export const transform: Build.Transform = (text, ctx) => ctx.kind === "page" ? text .replace( /[\s\S]*?<\/table>/g, (table) => `
${table}
`, ) .replace( /', ) .replace(/([^<]*)<\/code>/g, (span, inner: string) => inner.length > SHORT ? span : `${inner}`, ) : text; /** * Highlight source as a bare `
` block, for the panes
 * of the home page, which have their own frame.
 */
export function highlight(code: string, lang: string): string {
  let fn = HIGHLIGHTERS[lang];
  if (!fn) throw new Error(`No highlighter for "${lang}"`);
  return keys(fn(code), lang);
}
((?:[^<]*<\/code>(?:, )?){2,})<\/td>/g, '$1