char-slop/berry

multipurpose self-modifiable agentic harness

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

Charlotte Somrender tildes literally in markdown7692781

main
3.5 KiB95 linesraw
1import { Marked, type Token, type TokensList } from "marked";
2import { code } from "./code.tsx";
3
4const escape = (text: string) =>
5  text.replace(
6    /[&<>"']/g,
7    (char) => `&${{ "&": "amp", "<": "lt", ">": "gt", '"': "quot", "'": "#39" }[char]};`,
8  );
9
10// model output is untrusted: raw html shows as text, and links only go where links should
11const marked = new Marked({
12  gfm: true,
13  tokenizer: {
14    del: () => undefined,
15  },
16  renderer: {
17    html: ({ text }) => escape(text),
18    link({ href, tokens }) {
19      const text = this.parser.parseInline(tokens);
20      if (!/^(https?:|mailto:|#|\/)/i.test(href)) return text;
21      return `<a href="${escape(href)}" target="_blank" rel="noopener noreferrer">${text}</a>`;
22    },
23  },
24});
25
26/** One top-level block's nodes; its code is highlighted only once `settled`, since a fence being written still grows. */
27function draw(token: Token, links: TokensList["links"], settled: boolean): ChildNode[] {
28  const template = document.createElement("template");
29  template.innerHTML = marked.parser(Object.assign([token], { links }));
30  for (const block of template.content.querySelectorAll("pre > code")) {
31    const source = block.textContent ?? "";
32    if (!settled) {
33      // a growing fence redraws whole on every revision, so it stays one text node until it settles
34      const pre = document.createElement("pre");
35      pre.className = "code bare";
36      const text = document.createElement("span");
37      text.className = "code-text";
38      text.textContent = source.replace(/\n$/, "");
39      pre.append(text);
40      block.parentElement!.replaceWith(pre);
41      continue;
42    }
43    const language = block.className.match(/language-(\S+)/)?.[1];
44    block.parentElement!.replaceWith(
45      code(source, { gutter: false, ...(language ? { language } : {}) }),
46    );
47  }
48  return [...template.content.childNodes];
49}
50
51type Drawn = { raw: string; settled: boolean; nodes: ChildNode[] };
52
53/**
54 * Markdown that may still be being written: each `update` draws again only the top-level blocks that changed, which
55 * while it streams is the last. `final` says no more is coming.
56 */
57export function markdownView() {
58  const element = document.createElement("div");
59  element.className = "markdown";
60  let drawn: Drawn[] = [];
61  let shown: { text: string; final: boolean } | null = null;
62  let links = "";
63  return {
64    element,
65    update(text: string, final: boolean) {
66      if (shown?.text === text && shown.final === final) return;
67      shown = { text, final };
68      const tokens = marked.lexer(text);
69      // a reference link resolves against definitions anywhere in the text, even those written later
70      const known = JSON.stringify(tokens.links);
71      if (known !== links) {
72        links = known;
73        drawn = [];
74        element.replaceChildren();
75      }
76      const next = tokens.map((token, index): Drawn => {
77        const settled = final || index < tokens.length - 1;
78        const before = drawn[index];
79        if (before?.raw === token.raw && before.settled === settled) return before;
80        return { raw: token.raw, settled, nodes: draw(token, tokens.links, settled) };
81      });
82      let first = 0;
83      while (first < next.length && next[first] === drawn[first]) first++;
84      for (const { nodes } of drawn.slice(first)) for (const node of nodes) node.remove();
85      element.append(...next.slice(first).flatMap(({ nodes }) => nodes));
86      drawn = next;
87    },
88  };
89}
90
91export function markdown(text: string): HTMLElement {
92  const view = markdownView();
93  view.update(text, true);
94  return view.element;
95}