char-slop/berry

multipurpose self-modifiable agentic harness

git clone https://git.t4t.associates/char-slop/berry

Charlotte Sombetter code line breakingbc405c3

main
3.6 KiB92 linesraw
1import { highlightedLines } from "./highlight.ts";
2import { detectLanguage, languageNamed } from "./languages.ts";
3
4// as bare jsx text, a newline would collapse
5const NEWLINE = "\n";
6
7const waiting = new WeakMap<Element, () => void>();
8let visibility: IntersectionObserver | undefined;
9
10export type CodeOptions = {
11  /** Picks the grammar by file name. */
12  readonly path?: string;
13  /** A grammar or an alias such as `ts`; wins over `path`. */
14  readonly language?: string;
15  /** What each line's gutter shows: line numbers by default, `false` for no gutter. */
16  readonly gutter?: ((index: number) => string) | false;
17};
18
19const grammarOf = ({ path, language }: Pick<CodeOptions, "path" | "language">) =>
20  language ? languageNamed(language) ?? language : path ? detectLanguage(path) : null;
21
22/** `source`'s lines highlighted, the grammar picked as `code` picks it; `null` without one. */
23export async function highlight(
24  source: string,
25  options: Pick<CodeOptions, "path" | "language">,
26): Promise<Node[][] | null> {
27  const grammar = grammarOf(options);
28  return grammar === null ? null : await highlightedLines(grammar, source);
29}
30
31/**
32 * Source as sorcery shows it: each line shares a grid row with its gutter, so wrapped lines don't displace the
33 * gutter below. Plain at first, then highlighted once the worker answers.
34 */
35export function code(
36  source: string,
37  { path, language, gutter = (i) => String(i + 1) }: CodeOptions = {},
38): HTMLElement {
39  const lines = source.split("\n");
40  if (lines.length > 1 && lines.at(-1) === "") lines.pop();
41  // the newline belongs inside the line's own text: a grid item, firefox's copy adds a break after each
42  const pre = (
43    <pre class={gutter ? "code" : "code bare"}>
44      {lines.map((line, i) => (
45        <span class="code-line">
46          {gutter ? <span class="ln">{gutter(i)}</span> : undefined}
47          <span class="code-text">{line}{i < lines.length - 1 ? NEWLINE : ""}</span>
48        </span>
49      ))}
50    </pre>
51  ) as HTMLPreElement;
52  const grammar = grammarOf({ path, language });
53  if (grammar !== null) {
54    if (!visibility) {
55      const observer = visibility = new IntersectionObserver((entries) => {
56        for (const { target, isIntersecting } of entries) {
57          if (!isIntersecting || !target.isConnected) continue;
58          const highlight = waiting.get(target);
59          observer.unobserve(target);
60          waiting.delete(target);
61          highlight?.();
62        }
63      });
64      // code is built detached; observe only while mounted so discarded views aren't retained
65      new MutationObserver((records) => {
66        for (const record of records) {
67          for (const node of [...record.addedNodes, ...record.removedNodes]) {
68            if (!(node instanceof Element)) continue;
69            const blocks = [node, ...node.querySelectorAll("pre.code")];
70            for (const block of blocks) {
71              if (!waiting.has(block)) continue;
72              if (block.isConnected) observer.observe(block);
73              else observer.unobserve(block);
74            }
75          }
76        }
77      }).observe(document, { childList: true, subtree: true });
78    }
79    waiting.set(pre, () => {
80      highlightedLines(grammar, lines.join("\n"))
81        .then((highlighted) => {
82          if (highlighted === null) return;
83          const targets = pre.querySelectorAll(".code-text");
84          for (const [i, line] of highlighted.entries()) {
85            targets[i]?.replaceChildren(...line, ...(i < highlighted.length - 1 ? [NEWLINE] : []));
86          }
87        })
88        .catch((error) => console.warn("arborium:", error));
89    });
90  }
91  return pre;
92}