foldkit-richtext-dom 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +228 -0
- package/dist/editor-bundle.d.mts +7535 -0
- package/dist/editor-bundle.d.mts.map +1 -0
- package/dist/editor-bundle.mjs +243 -0
- package/dist/editor-bundle.mjs.map +1 -0
- package/dist/editor.d.mts +849 -0
- package/dist/editor.d.mts.map +1 -0
- package/dist/editor.mjs +95 -0
- package/dist/editor.mjs.map +1 -0
- package/dist/events-BTw9SJ7r.d.mts +77 -0
- package/dist/events-BTw9SJ7r.d.mts.map +1 -0
- package/dist/events.d.mts +2 -0
- package/dist/events.mjs +305 -0
- package/dist/events.mjs.map +1 -0
- package/dist/host.d.mts +25 -0
- package/dist/host.d.mts.map +1 -0
- package/dist/host.mjs +52 -0
- package/dist/host.mjs.map +1 -0
- package/dist/html.d.mts +19 -0
- package/dist/html.d.mts.map +1 -0
- package/dist/html.mjs +234 -0
- package/dist/html.mjs.map +1 -0
- package/dist/index-6QEOehXh.d.mts +38 -0
- package/dist/index-6QEOehXh.d.mts.map +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +304 -0
- package/dist/index.mjs.map +1 -0
- package/dist/toolbar.d.mts +26 -0
- package/dist/toolbar.d.mts.map +1 -0
- package/dist/toolbar.mjs +23 -0
- package/dist/toolbar.mjs.map +1 -0
- package/dist/view.d.mts +9 -0
- package/dist/view.d.mts.map +1 -0
- package/dist/view.mjs +62 -0
- package/dist/view.mjs.map +1 -0
- package/package.json +89 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["/**\n * The DOM interpreter for an editable subtree: it renders a semantic Document\n * into an owned subtree, maps positions both ways, and patches only the nodes a\n * ChangeSet names. No Foldkit VDOM reaches inside this subtree, and the semantic\n * document stays authoritative — the DOM is never read as truth beyond mapping a\n * browser selection back to a semantic position.\n */\nimport * as RichText from 'foldkit-richtext'\n\nexport interface EditorDom {\n /** The owned subtree root. Everything inside belongs to this interpreter. */\n readonly root: HTMLElement\n /** Current element per block and run identity. */\n readonly elements: ReadonlyMap<RichText.NodeId, HTMLElement>\n /** The semantic document this subtree currently represents. */\n readonly content: RichText.Document\n /** The registry this subtree was rendered with, and is patched with again. */\n readonly rendering: RichText.Rendering\n}\n\nconst MARK_ATTRIBUTE = 'data-marks'\n\n/** The element a block's own type implies; a declared node kind is elsewhere. */\nconst blockTag = (block: RichText.Block): string => {\n if (block.type === 'Heading') return `h${block.level}`\n if (block.type === 'Unknown' || block.type === 'Node') return 'div'\n return 'p'\n}\n\n/** The element a renderer entry names, with its attributes; children come later. */\nconst renderElement = (owner: Document, entry: RichText.ElementRendering): HTMLElement => {\n const element = owner.createElement(entry.tag)\n for (const [name, value] of Object.entries(entry.attributes)) element.setAttribute(name, value)\n return element\n}\n\n/**\n * A run is one element carrying `data-run`, whatever the renderer wraps its text\n * in. Nesting the mark elements *inside* that element is what keeps a browser\n * selection mappable: `rangeToPosition` finds the run through `closest`, and the\n * text node stays deepest, so an offset is still an offset into the run's text.\n */\nconst renderRun = (\n owner: Document,\n run: RichText.Text,\n rendering: RichText.Rendering,\n): HTMLElement => {\n const element = owner.createElement('span')\n element.setAttribute('data-run', run.id)\n const { nest, unrendered } = RichText.runRendering(rendering, run)\n // Always a text node, even when empty: a caret inside an empty run has to be\n // addressable, and an element container has no semantic offset.\n let content: Node = owner.createTextNode(run.text)\n for (const entry of nest) {\n const wrapper = renderElement(owner, entry)\n wrapper.append(content)\n content = wrapper\n }\n element.append(content)\n // A name no entry renders still rides here on the run element, so a slice and\n // an HTML round trip keep a way to carry it.\n if (unrendered.length > 0) element.setAttribute(MARK_ATTRIBUTE, unrendered.join(' '))\n return element\n}\n\nconst renderBlock = (\n owner: Document,\n block: RichText.Block,\n elements: Map<RichText.NodeId, HTMLElement>,\n rendering: RichText.Rendering,\n): HTMLElement => {\n // A declared node kind renders as its entry (§121); every other block keeps the\n // tag its own type implies. The id attribute is the interpreter's, so it wins\n // over an entry that names it.\n const entry = block.type === 'Node' ? RichText.nodeRendering(rendering, block) : undefined\n const element =\n entry === undefined ? owner.createElement(blockTag(block)) : renderElement(owner, entry)\n element.setAttribute('data-block', block.id)\n if (block.type === 'Unknown') {\n // Preserved content is shown as a diagnostic placeholder, never executed.\n element.setAttribute('data-unknown', block.originalType)\n element.setAttribute('contenteditable', 'false')\n element.textContent = `[${block.originalType}]`\n elements.set(block.id, element)\n return element\n }\n // Always a text node, even when empty: a caret inside an empty run has to be\n // addressable, and an element container has no semantic offset.\n for (const run of block.children) {\n const runElement = renderRun(owner, run, rendering)\n element.append(runElement)\n elements.set(run.id, runElement)\n }\n // A node that accepts nested blocks renders them inside it, so a list keeps\n // its items and each nested block stays addressable by identity.\n if (block.type === 'Node' && block.blocks !== undefined) {\n for (const nested of block.blocks)\n element.append(renderBlock(owner, nested, elements, rendering))\n }\n elements.set(block.id, element)\n return element\n}\n\n/** Builds the owned subtree and the identity index it is patched through. */\nexport const mount = (\n owner: Document,\n content: RichText.Document,\n rendering: RichText.Rendering = RichText.noRendering,\n): EditorDom => {\n const root = owner.createElement('div')\n root.setAttribute('contenteditable', 'true')\n const elements = new Map<RichText.NodeId, HTMLElement>()\n for (const block of content.children) root.append(renderBlock(owner, block, elements, rendering))\n return { root, elements, content, rendering }\n}\n\n/**\n * Applies a ChangeSet: removed identities lose their elements, dirty identities\n * are re-rendered in place (or inserted, when they are new), and every\n * untouched element keeps its object identity, so the browser is not handed a\n * rebuilt tree on each keystroke.\n */\nconst childIds = (element: HTMLElement, attribute: string): ReadonlyArray<string> =>\n Array.from(element.children)\n .map(child => child.getAttribute(attribute))\n .filter((id): id is string => id !== null)\n\nconst sameIds = (present: ReadonlyArray<string | null>, wanted: ReadonlyArray<string>): boolean =>\n present.length === wanted.length && present.every((id, at) => id === wanted[at])\n\nexport const patch = (\n dom: EditorDom,\n content: RichText.Document,\n changeSet: RichText.ChangeSet,\n): EditorDom => {\n const elements = new Map(dom.elements)\n const root = dom.root\n for (const id of changeSet.removedNodes) {\n const element = elements.get(id)\n if (element === undefined) continue\n // Harmless when a dirty ancestor replaces the subtree anyway: removing a\n // node that is already detached is a no-op.\n element.remove()\n elements.delete(id)\n }\n const rebuilt = new Set<RichText.NodeId>()\n patchBlocks(root.ownerDocument, root, content.children, elements, dom.rendering, rebuilt)\n for (const id of changeSet.dirtyNodes) {\n const located = RichText.locateRun(content, id)\n if (located === undefined) continue\n const block = RichText.blockAtPath(content, located.path)\n if (block === undefined || rebuilt.has(block.id)) continue\n const parent = elements.get(block.id)\n if (parent === undefined) continue\n const fresh = renderRun(root.ownerDocument, located.run, dom.rendering)\n elements.get(id)?.remove()\n elements.set(id, fresh)\n // A run belongs where the document says it does inside its block.\n const next = block.children[located.index + 1]\n const nextElement = next === undefined ? undefined : elements.get(next.id)\n if (nextElement !== undefined) nextElement.before(fresh)\n else parent.append(fresh)\n }\n return { root, elements, content, rendering: dom.rendering }\n}\n\n/**\n * Patches one block list into its container. A block is rebuilt only when its\n * run list and its nested blocks are unchanged in shape, or it is new;\n * otherwise its element stays, which keeps every sibling's identity — and a\n * block that merely moved is moved, not re-rendered. A kept container needs no\n * recursion: its nested identities are unchanged, so any change inside them is a\n * run-level change the caller patches directly. Nested structural patching\n * arrives with the operations that can produce it (§116 slice 3).\n */\nconst patchBlocks = (\n owner: Document,\n container: HTMLElement,\n blocks: ReadonlyArray<RichText.Block>,\n elements: Map<RichText.NodeId, HTMLElement>,\n rendering: RichText.Rendering,\n rebuilt: Set<RichText.NodeId>,\n): void => {\n let previousElement: HTMLElement | undefined\n for (const [index, block] of blocks.entries()) {\n const existing = elements.get(block.id)\n const nested = block.type === 'Node' && block.blocks !== undefined ? block.blocks : []\n const sameStructure =\n existing !== undefined &&\n sameIds(\n childIds(existing, 'data-run'),\n block.children.map(run => run.id),\n ) &&\n sameIds(\n childIds(existing, 'data-block'),\n nested.map(child => child.id),\n )\n // Placement is relative to the previous block's element, which this loop has\n // already put in place, so an insert or a move lands in document order\n // whatever the surrounding elements are doing.\n const place = (element: HTMLElement): void => {\n if (previousElement !== undefined) previousElement.after(element)\n else {\n const nextId = blocks[index + 1]?.id\n const next = nextId === undefined ? undefined : elements.get(nextId)\n if (next !== undefined) next.before(element)\n else container.append(element)\n }\n }\n if (existing !== undefined && sameStructure) {\n if (container.children[index] !== existing) place(existing)\n previousElement = existing\n continue\n }\n const fresh = renderBlock(owner, block, elements, rendering)\n existing?.remove()\n place(fresh)\n rebuilt.add(block.id)\n previousElement = fresh\n }\n}\n\n/**\n * The text node a run's content starts in. Mark elements may wrap it, so this\n * descends rather than taking `firstChild`, and returns nothing when the run\n * holds no text at all.\n */\nconst textNodeOf = (element: HTMLElement): Text | undefined => {\n let node: Node | null = element.firstChild\n while (node instanceof Element) node = node.firstChild\n return node instanceof Text ? node : undefined\n}\n\n/** The DOM range for a semantic position, or undefined if it no longer resolves. */\nexport const positionToRange = (dom: EditorDom, position: RichText.Position): Range | undefined => {\n const element = dom.elements.get(position.node)\n if (element === undefined || element.hasAttribute('data-unknown')) return undefined\n const text = textNodeOf(element)\n const limit = text?.length ?? 0\n if (position.offset > limit) return undefined\n const range = dom.root.ownerDocument.createRange()\n if (text === undefined) {\n range.setStart(element, 0)\n range.collapse(true)\n return range\n }\n range.setStart(text, position.offset)\n range.collapse(true)\n return range\n}\n\nconst runIdFor = (dom: EditorDom, node: Node): RichText.NodeId | undefined => {\n const element = node instanceof Element ? node : node.parentElement\n const run = element?.closest('[data-run]')\n const id = run?.getAttribute('data-run')\n return id === null || id === undefined ? undefined : (id as RichText.NodeId)\n}\n\n/**\n * Maps a browser selection endpoint back to a semantic position. Only text-node\n * containers inside a run are supported; anything else (an element offset, a\n * range outside the owned subtree) has no semantic position and returns\n * undefined rather than guessing.\n */\nexport const rangeToPosition = (\n dom: EditorDom,\n node: Node,\n offset: number,\n): RichText.Position | undefined => {\n if (!(node instanceof Text)) return undefined\n const id = runIdFor(dom, node)\n if (id === undefined) return undefined\n const located = RichText.locateRun(dom.content, id)\n if (located === undefined) return undefined\n return { node: id, offset, affinity: offset === located.run.text.length ? 'after' : 'before' }\n}\n\n/** Plain text of the subtree, one line per text block: for assertions and fallback copy. */\nexport const toText = (dom: EditorDom): string => {\n const lines: Array<string> = []\n const walk = (container: Element): void => {\n for (const element of Array.from(container.children)) {\n if (!element.hasAttribute('data-block')) continue\n // A container contributes its nested blocks' lines, not one long line.\n if (Array.from(element.children).some(child => child.hasAttribute('data-block'))) {\n walk(element)\n continue\n }\n const line = (element.textContent ?? '').trim()\n if (line.length > 0) lines.push(line)\n }\n }\n walk(dom.root)\n return lines.join('\\n')\n}\n\nconst renderedText = (block: RichText.Block): string => {\n if (block.type === 'Unknown') return `[${block.originalType}]`\n const runs = block.children.map(run => run.text).join('')\n const nested =\n block.type === 'Node' && block.blocks !== undefined\n ? block.blocks.map(renderedText).join('')\n : ''\n return runs + nested\n}\n\n/** The chain of element names a run's text sits under, as `strong>em`. */\nconst runShape = (element: HTMLElement): string => {\n const tags: Array<string> = []\n let node: Node | null = element.firstChild\n while (node instanceof Element) {\n tags.push(node.tagName.toLowerCase())\n node = node.firstChild\n }\n return tags.join('>')\n}\n\n/**\n * Whether a run element still shows what the renderer produces for it: the same\n * mark elements, and `data-marks` carrying exactly the names no entry renders. A\n * browser can split a mark element or drop one without changing the text, and the\n * semantic document stays the authority, so recovery has to see that.\n */\nconst runMatches = (\n element: HTMLElement | undefined,\n rendering: RichText.Rendering,\n run: RichText.Text,\n): boolean => {\n if (element === undefined) return false\n const { nest, unrendered } = RichText.runRendering(rendering, run)\n const marks = unrendered.join(' ')\n const carried = element.getAttribute(MARK_ATTRIBUTE)\n if (marks.length === 0 ? carried !== null : carried !== marks) return false\n // `nest` is innermost first, so the chain from the outside is its reverse.\n const expected = nest\n .map(entry => entry.tag.toLowerCase())\n .reverse()\n .join('>')\n return runShape(element) === expected\n}\n\n/**\n * Recovery, not domain state (§31): makes the subtree match the document again\n * after something outside the semantic pipeline touched it — a cancelled IME\n * composition leaves text the document never had, and a browser extension can\n * mutate anything. Blocks whose rendered text already matches are left alone,\n * so this costs nothing in the normal case and returns the same `EditorDom`\n * when nothing was wrong.\n */\nexport const repair = (dom: EditorDom, content: RichText.Document): EditorDom => {\n const present = new Set<RichText.NodeId>()\n const dirtyNodes = new Set<RichText.NodeId>()\n const removedNodes = new Set<RichText.NodeId>()\n // A block the browser touched — stray text, a changed run or child list — is\n // dropped from the map as well as the DOM, so `patch` renders it fresh rather\n // than placing a stale element back where it was.\n const elements = new Map(dom.elements)\n const inspect = (blocks: ReadonlyArray<RichText.Block>): void => {\n for (const block of blocks) {\n present.add(block.id)\n for (const run of block.children) present.add(run.id)\n const nested = block.type === 'Node' && block.blocks !== undefined ? block.blocks : []\n for (const child of nested) present.add(child.id)\n const element = elements.get(block.id)\n const runs = block.type === 'Unknown' ? [] : block.children.map(run => run.id)\n const marksMatch =\n block.type === 'Unknown' ||\n block.children.every(run => runMatches(elements.get(run.id), dom.rendering, run))\n const shapeMatches =\n element !== undefined &&\n element.textContent === renderedText(block) &&\n marksMatch &&\n sameIds(childIds(element, 'data-run'), runs) &&\n sameIds(\n childIds(element, 'data-block'),\n nested.map(child => child.id),\n )\n if (!shapeMatches) {\n element?.remove()\n elements.delete(block.id)\n for (const run of runs) elements.delete(run)\n dirtyNodes.add(block.id)\n for (const run of runs) dirtyNodes.add(run)\n }\n inspect(nested)\n }\n }\n inspect(content.children)\n for (const id of elements.keys()) if (!present.has(id)) removedNodes.add(id)\n // A browser or extension can insert elements the map never knew about; sweep\n // the subtree for identities the document does not have.\n for (const element of Array.from(dom.root.querySelectorAll('[data-block], [data-run]'))) {\n const identity = element.getAttribute('data-block') ?? element.getAttribute('data-run') ?? ''\n if (identity.length > 0 && !present.has(identity as RichText.NodeId)) element.remove()\n }\n if (dirtyNodes.size === 0 && removedNodes.size === 0) return dom\n return patch(\n { root: dom.root, elements, content: dom.content, rendering: dom.rendering },\n content,\n {\n dirtyNodes,\n insertedNodes: new Set(),\n removedNodes,\n textChanged: new Set(),\n structureChanged: false,\n selectionChanged: false,\n },\n )\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as RichText from "foldkit-richtext";
|
|
2
|
+
import { Html, HtmlBuilder } from "foldkit/html";
|
|
3
|
+
//#region src/toolbar.d.ts
|
|
4
|
+
/** The editor state a toolbar reads: what a Bundle projects, without the rest. */
|
|
5
|
+
export interface ToolbarState {
|
|
6
|
+
readonly document: RichText.Document;
|
|
7
|
+
readonly selection: RichText.Selection | null;
|
|
8
|
+
readonly storedMarks: ReadonlyArray<string> | null;
|
|
9
|
+
}
|
|
10
|
+
export interface ToolbarOptions<Message> {
|
|
11
|
+
readonly state: ToolbarState;
|
|
12
|
+
/** The marks to offer, by name. Defaults to the three the package ships. */
|
|
13
|
+
readonly marks?: ReadonlyArray<string> | undefined;
|
|
14
|
+
/** How a mark becomes the application's Message, usually its editor wrapper. */
|
|
15
|
+
readonly toMessage: (mark: string) => Message;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A mark is active when the caret carries it, or — with no stored format — when
|
|
19
|
+
* every run the selection covers does (§119). Exported because a Mixins slot view
|
|
20
|
+
* renders its own buttons and needs the same rule.
|
|
21
|
+
*/
|
|
22
|
+
export declare const markActive: (state: ToolbarState, mark: string) => boolean;
|
|
23
|
+
/** Buttons for the editor's marks. The application decides where they go. */
|
|
24
|
+
export declare const marksToolbar: <Message>(options: ToolbarOptions<Message>) => (h: HtmlBuilder<Message>) => Html;
|
|
25
|
+
//#endregion
|
|
26
|
+
//# sourceMappingURL=toolbar.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolbar.d.mts","names":[],"sources":["../src/toolbar.ts"],"mappings":";;;;iBAYiB;WACN,UAAU,SAAS;WACnB,WAAW,SAAS;WACpB,aAAa;;iBAGP,eAAe;WACrB,OAAO;;WAEP,QAAQ;;WAER,YAAY,iBAAiB;;;;;;;qBAQ3B,aAAc,OAAO,cAAc;;qBAMnC,eACV,SAAS,SAAS,eAAe,cACjC,GAAG,YAAY,aAAW"}
|
package/dist/toolbar.mjs
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import * as RichText from "foldkit-richtext";
|
|
2
|
+
//#region src/toolbar.ts
|
|
3
|
+
/**
|
|
4
|
+
* A mark is active when the caret carries it, or — with no stored format — when
|
|
5
|
+
* every run the selection covers does (§119). Exported because a Mixins slot view
|
|
6
|
+
* renders its own buttons and needs the same rule.
|
|
7
|
+
*/
|
|
8
|
+
const markActive = (state, mark) => state.storedMarks !== null ? state.storedMarks.includes(mark) : RichText.marksInRange(state.document, state.selection).has(mark);
|
|
9
|
+
/** Buttons for the editor's marks. The application decides where they go. */
|
|
10
|
+
const marksToolbar = (options) => (h) => {
|
|
11
|
+
const marks = options.marks ?? RichText.shippedMarks.map((definition) => definition.name);
|
|
12
|
+
return h.div([h.DataAttribute("toolbar", "marks")], marks.map((mark) => h.button([
|
|
13
|
+
h.Type("button"),
|
|
14
|
+
h.DataAttribute("mark", mark),
|
|
15
|
+
h.AriaLabel(mark),
|
|
16
|
+
h.AriaPressed(markActive(options.state, mark) ? "true" : "false"),
|
|
17
|
+
h.OnClick(options.toMessage(mark))
|
|
18
|
+
], [mark])));
|
|
19
|
+
};
|
|
20
|
+
//#endregion
|
|
21
|
+
export { markActive, marksToolbar };
|
|
22
|
+
|
|
23
|
+
//# sourceMappingURL=toolbar.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolbar.mjs","names":[],"sources":["../src/toolbar.ts"],"sourcesContent":["/**\n * The marks toolbar (§35, §119): a button per mark, dispatching the same Messages\n * a chord does. It is the application's chrome, not the editor's — §29 gives the\n * editor the `contenteditable` subtree and the application everything around it —\n * so this renders a value the application places beside the editor's host, and it\n * never touches the DOM the editor owns. A Mixins slot family wraps these buttons\n * when an application needs to restyle or replace parts.\n */\nimport type { Html, HtmlBuilder } from 'foldkit/html'\nimport * as RichText from 'foldkit-richtext'\n\n/** The editor state a toolbar reads: what a Bundle projects, without the rest. */\nexport interface ToolbarState {\n readonly document: RichText.Document\n readonly selection: RichText.Selection | null\n readonly storedMarks: ReadonlyArray<string> | null\n}\n\nexport interface ToolbarOptions<Message> {\n readonly state: ToolbarState\n /** The marks to offer, by name. Defaults to the three the package ships. */\n readonly marks?: ReadonlyArray<string> | undefined\n /** How a mark becomes the application's Message, usually its editor wrapper. */\n readonly toMessage: (mark: string) => Message\n}\n\n/**\n * A mark is active when the caret carries it, or — with no stored format — when\n * every run the selection covers does (§119). Exported because a Mixins slot view\n * renders its own buttons and needs the same rule.\n */\nexport const markActive = (state: ToolbarState, mark: string): boolean =>\n state.storedMarks !== null\n ? state.storedMarks.includes(mark)\n : RichText.marksInRange(state.document, state.selection).has(mark)\n\n/** Buttons for the editor's marks. The application decides where they go. */\nexport const marksToolbar =\n <Message>(options: ToolbarOptions<Message>) =>\n (h: HtmlBuilder<Message>): Html => {\n const marks = options.marks ?? RichText.shippedMarks.map(definition => definition.name)\n return h.div(\n [h.DataAttribute('toolbar', 'marks')],\n marks.map(mark =>\n h.button(\n [\n h.Type('button'),\n h.DataAttribute('mark', mark),\n h.AriaLabel(mark),\n h.AriaPressed(markActive(options.state, mark) ? 'true' : 'false'),\n h.OnClick(options.toMessage(mark)),\n ],\n [mark],\n ),\n ),\n )\n }\n"],"mappings":";;;;;;;AA+BA,MAAa,cAAc,OAAqB,SAC9C,MAAM,gBAAgB,OAClB,MAAM,YAAY,SAAS,IAAI,IAC/B,SAAS,aAAa,MAAM,UAAU,MAAM,SAAS,CAAC,CAAC,IAAI,IAAI;;AAGrE,MAAa,gBACD,aACT,MAAkC;CACjC,MAAM,QAAQ,QAAQ,SAAS,SAAS,aAAa,KAAI,eAAc,WAAW,IAAI;CACtF,OAAO,EAAE,IACP,CAAC,EAAE,cAAc,WAAW,OAAO,CAAC,GACpC,MAAM,KAAI,SACR,EAAE,OACA;EACE,EAAE,KAAK,QAAQ;EACf,EAAE,cAAc,QAAQ,IAAI;EAC5B,EAAE,UAAU,IAAI;EAChB,EAAE,YAAY,WAAW,QAAQ,OAAO,IAAI,IAAI,SAAS,OAAO;EAChE,EAAE,QAAQ,QAAQ,UAAU,IAAI,CAAC;CACnC,GACA,CAAC,IAAI,CACP,CACF,CACF;AACF"}
|
package/dist/view.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as RichText from "foldkit-richtext";
|
|
2
|
+
import { Html } from "foldkit/html";
|
|
3
|
+
//#region src/view.d.ts
|
|
4
|
+
/** One element per block, ready to place in any Foldkit view. */
|
|
5
|
+
export declare const renderBlocks: (blocks: ReadonlyArray<RichText.Block>, renderer?: RichText.Rendering) => ReadonlyArray<Html>;
|
|
6
|
+
/** The whole document as a `div` of block elements. */
|
|
7
|
+
export declare const renderDocument: (document: RichText.Document, renderer?: RichText.Rendering) => Html;
|
|
8
|
+
//#endregion
|
|
9
|
+
//# sourceMappingURL=view.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view.d.mts","names":[],"sources":["../src/view.ts"],"mappings":";;;;qBAgFa,eACX,QAAQ,cAAc,SAAS,QAC/B,WAAU,SAAS,cAClB,cAAc;;qBAGJ,iBACX,UAAU,SAAS,UACnB,WAAU,SAAS,cAClB"}
|
package/dist/view.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as RichText from "foldkit-richtext";
|
|
2
|
+
import { inertHtml } from "foldkit/html";
|
|
3
|
+
//#region src/view.ts
|
|
4
|
+
/**
|
|
5
|
+
* The read-only renderer (§33): a Document becomes ordinary Foldkit `Html`, so
|
|
6
|
+
* SSR, static pages, CMS visitor rendering, and email generation all reuse one
|
|
7
|
+
* interpreter. It dispatches nothing — `inertHtml` has no Message universe — and
|
|
8
|
+
* it never touches the DOM directly, so it is the counterpart to the editable
|
|
9
|
+
* adapter rather than a second editor.
|
|
10
|
+
*
|
|
11
|
+
* Marks and node kinds render through the same registry as the HTML serializer
|
|
12
|
+
* (§121), so a declared Link becomes a real `<a href>` here too instead of a
|
|
13
|
+
* name.
|
|
14
|
+
*/
|
|
15
|
+
const HEADINGS = {
|
|
16
|
+
1: inertHtml.h1,
|
|
17
|
+
2: inertHtml.h2,
|
|
18
|
+
3: inertHtml.h3,
|
|
19
|
+
4: inertHtml.h4,
|
|
20
|
+
5: inertHtml.h5,
|
|
21
|
+
6: inertHtml.h6
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Foldkit types one builder per tag name and publishes no builder for an
|
|
25
|
+
* arbitrary tag, so this narrows a tag the renderer named to the builder of that
|
|
26
|
+
* name. The core has already refused a malformed name; a tag Foldkit cannot build
|
|
27
|
+
* is reported rather than silently swapped for another element. Attribute
|
|
28
|
+
* builders are capitalized and tags are not, which is why the name must be
|
|
29
|
+
* lowercase to be looked up.
|
|
30
|
+
*/
|
|
31
|
+
const ELEMENTS = inertHtml;
|
|
32
|
+
const elementFor = (tag) => {
|
|
33
|
+
const builder = tag === tag.toLowerCase() ? ELEMENTS[tag] : void 0;
|
|
34
|
+
if (builder === void 0) throw new Error(`RichText.renderDocument: no element for "<${tag}>"`);
|
|
35
|
+
return builder;
|
|
36
|
+
};
|
|
37
|
+
const renderElement = (element, children) => {
|
|
38
|
+
const attributes = Object.entries(element.attributes).map(([key, value]) => inertHtml.Attribute(key, value));
|
|
39
|
+
return elementFor(element.tag)(attributes, children);
|
|
40
|
+
};
|
|
41
|
+
const renderRun = (renderer, run) => {
|
|
42
|
+
const { nest, unrendered } = RichText.runRendering(renderer, run);
|
|
43
|
+
let node = run.text;
|
|
44
|
+
for (const element of nest) node = renderElement(element, [node]);
|
|
45
|
+
return unrendered.length === 0 ? node : inertHtml.span([inertHtml.DataAttribute("marks", unrendered.join(" "))], [node]);
|
|
46
|
+
};
|
|
47
|
+
const renderBlock = (renderer, block) => {
|
|
48
|
+
if (block.type === "Unknown") return inertHtml.div([inertHtml.DataAttribute("unknown", block.originalType)], [`[${block.originalType}]`]);
|
|
49
|
+
const children = [...block.children.map((run) => renderRun(renderer, run)), ...block.type === "Node" && block.blocks !== void 0 ? renderBlocks(block.blocks, renderer) : []];
|
|
50
|
+
const element = block.type === "Node" ? RichText.nodeRendering(renderer, block) : void 0;
|
|
51
|
+
if (element !== void 0) return renderElement(element, children);
|
|
52
|
+
if (block.type === "Node") return inertHtml.div([inertHtml.DataAttribute("node", block.kind)], children);
|
|
53
|
+
return block.type === "Heading" ? HEADINGS[block.level]([], children) : inertHtml.p([], children);
|
|
54
|
+
};
|
|
55
|
+
/** One element per block, ready to place in any Foldkit view. */
|
|
56
|
+
const renderBlocks = (blocks, renderer = RichText.noRendering) => blocks.map((block) => renderBlock(renderer, block));
|
|
57
|
+
/** The whole document as a `div` of block elements. */
|
|
58
|
+
const renderDocument = (document, renderer = RichText.noRendering) => inertHtml.div([], renderBlocks(document.children, renderer));
|
|
59
|
+
//#endregion
|
|
60
|
+
export { renderBlocks, renderDocument };
|
|
61
|
+
|
|
62
|
+
//# sourceMappingURL=view.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view.mjs","names":["h"],"sources":["../src/view.ts"],"sourcesContent":["/**\n * The read-only renderer (§33): a Document becomes ordinary Foldkit `Html`, so\n * SSR, static pages, CMS visitor rendering, and email generation all reuse one\n * interpreter. It dispatches nothing — `inertHtml` has no Message universe — and\n * it never touches the DOM directly, so it is the counterpart to the editable\n * adapter rather than a second editor.\n *\n * Marks and node kinds render through the same registry as the HTML serializer\n * (§121), so a declared Link becomes a real `<a href>` here too instead of a\n * name.\n */\nimport { inertHtml as h, type Html } from 'foldkit/html'\nimport * as RichText from 'foldkit-richtext'\n\n/** A rendered child is either an element or literal text. */\ntype Child = Html | string\n\ntype ElementFunction = typeof h.span\ntype ElementAttributes = NonNullable<Parameters<ElementFunction>[0]>\n\nconst HEADINGS = { 1: h.h1, 2: h.h2, 3: h.h3, 4: h.h4, 5: h.h5, 6: h.h6 } as const\n\n/**\n * Foldkit types one builder per tag name and publishes no builder for an\n * arbitrary tag, so this narrows a tag the renderer named to the builder of that\n * name. The core has already refused a malformed name; a tag Foldkit cannot build\n * is reported rather than silently swapped for another element. Attribute\n * builders are capitalized and tags are not, which is why the name must be\n * lowercase to be looked up.\n */\nconst ELEMENTS = h as unknown as Readonly<Record<string, ElementFunction>>\n\nconst elementFor = (tag: string): ElementFunction => {\n const builder = tag === tag.toLowerCase() ? ELEMENTS[tag] : undefined\n if (builder === undefined) throw new Error(`RichText.renderDocument: no element for \"<${tag}>\"`)\n return builder\n}\n\nconst renderElement = (\n element: RichText.ElementRendering,\n children: ReadonlyArray<Child>,\n): Html => {\n const attributes: ElementAttributes = Object.entries(element.attributes).map(([key, value]) =>\n h.Attribute(key, value),\n )\n return elementFor(element.tag)(attributes, children)\n}\n\nconst renderRun = (renderer: RichText.Rendering, run: RichText.Text): Child => {\n const { nest, unrendered } = RichText.runRendering(renderer, run)\n let node: Child = run.text\n for (const element of nest) node = renderElement(element, [node])\n return unrendered.length === 0\n ? node\n : h.span([h.DataAttribute('marks', unrendered.join(' '))], [node])\n}\n\nconst renderBlock = (renderer: RichText.Rendering, block: RichText.Block): Html => {\n if (block.type === 'Unknown') {\n // Preserved content renders as a diagnostic placeholder, never executed.\n return h.div([h.DataAttribute('unknown', block.originalType)], [`[${block.originalType}]`])\n }\n // A node that accepts nested blocks renders them inside it, so a list keeps\n // its items; a text block holds only its runs.\n const children: ReadonlyArray<Child> = [\n ...block.children.map(run => renderRun(renderer, run)),\n ...(block.type === 'Node' && block.blocks !== undefined\n ? renderBlocks(block.blocks, renderer)\n : []),\n ]\n const element = block.type === 'Node' ? RichText.nodeRendering(renderer, block) : undefined\n if (element !== undefined) return renderElement(element, children)\n if (block.type === 'Node') {\n // The kind is addressable so a stylesheet or a renderer can reach it.\n return h.div([h.DataAttribute('node', block.kind)], children)\n }\n return block.type === 'Heading' ? HEADINGS[block.level]([], children) : h.p([], children)\n}\n\n/** One element per block, ready to place in any Foldkit view. */\nexport const renderBlocks = (\n blocks: ReadonlyArray<RichText.Block>,\n renderer: RichText.Rendering = RichText.noRendering,\n): ReadonlyArray<Html> => blocks.map(block => renderBlock(renderer, block))\n\n/** The whole document as a `div` of block elements. */\nexport const renderDocument = (\n document: RichText.Document,\n renderer: RichText.Rendering = RichText.noRendering,\n): Html => h.div([], renderBlocks(document.children, renderer))\n"],"mappings":";;;;;;;;;;;;;;AAoBA,MAAM,WAAW;CAAE,GAAGA,UAAE;CAAI,GAAGA,UAAE;CAAI,GAAGA,UAAE;CAAI,GAAGA,UAAE;CAAI,GAAGA,UAAE;CAAI,GAAGA,UAAE;AAAG;;;;;;;;;AAUxE,MAAM,WAAWA;AAEjB,MAAM,cAAc,QAAiC;CACnD,MAAM,UAAU,QAAQ,IAAI,YAAY,IAAI,SAAS,OAAO,KAAA;CAC5D,IAAI,YAAY,KAAA,GAAW,MAAM,IAAI,MAAM,6CAA6C,IAAI,GAAG;CAC/F,OAAO;AACT;AAEA,MAAM,iBACJ,SACA,aACS;CACT,MAAM,aAAgC,OAAO,QAAQ,QAAQ,UAAU,CAAC,CAAC,KAAK,CAAC,KAAK,WAClFA,UAAE,UAAU,KAAK,KAAK,CACxB;CACA,OAAO,WAAW,QAAQ,GAAG,CAAC,CAAC,YAAY,QAAQ;AACrD;AAEA,MAAM,aAAa,UAA8B,QAA8B;CAC7E,MAAM,EAAE,MAAM,eAAe,SAAS,aAAa,UAAU,GAAG;CAChE,IAAI,OAAc,IAAI;CACtB,KAAK,MAAM,WAAW,MAAM,OAAO,cAAc,SAAS,CAAC,IAAI,CAAC;CAChE,OAAO,WAAW,WAAW,IACzB,OACAA,UAAE,KAAK,CAACA,UAAE,cAAc,SAAS,WAAW,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC;AACrE;AAEA,MAAM,eAAe,UAA8B,UAAgC;CACjF,IAAI,MAAM,SAAS,WAEjB,OAAOA,UAAE,IAAI,CAACA,UAAE,cAAc,WAAW,MAAM,YAAY,CAAC,GAAG,CAAC,IAAI,MAAM,aAAa,EAAE,CAAC;CAI5F,MAAM,WAAiC,CACrC,GAAG,MAAM,SAAS,KAAI,QAAO,UAAU,UAAU,GAAG,CAAC,GACrD,GAAI,MAAM,SAAS,UAAU,MAAM,WAAW,KAAA,IAC1C,aAAa,MAAM,QAAQ,QAAQ,IACnC,CAAC,CACP;CACA,MAAM,UAAU,MAAM,SAAS,SAAS,SAAS,cAAc,UAAU,KAAK,IAAI,KAAA;CAClF,IAAI,YAAY,KAAA,GAAW,OAAO,cAAc,SAAS,QAAQ;CACjE,IAAI,MAAM,SAAS,QAEjB,OAAOA,UAAE,IAAI,CAACA,UAAE,cAAc,QAAQ,MAAM,IAAI,CAAC,GAAG,QAAQ;CAE9D,OAAO,MAAM,SAAS,YAAY,SAAS,MAAM,MAAM,CAAC,CAAC,GAAG,QAAQ,IAAIA,UAAE,EAAE,CAAC,GAAG,QAAQ;AAC1F;;AAGA,MAAa,gBACX,QACA,WAA+B,SAAS,gBAChB,OAAO,KAAI,UAAS,YAAY,UAAU,KAAK,CAAC;;AAG1E,MAAa,kBACX,UACA,WAA+B,SAAS,gBAC/BA,UAAE,IAAI,CAAC,GAAG,aAAa,SAAS,UAAU,QAAQ,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foldkit-richtext-dom",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The DOM interpreter for a foldkit-richtext editable subtree.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"foldkit",
|
|
7
|
+
"effect",
|
|
8
|
+
"rich-text",
|
|
9
|
+
"editor",
|
|
10
|
+
"contenteditable",
|
|
11
|
+
"dom",
|
|
12
|
+
"typescript"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"author": "Patrick Glenn <doeixd@gmail.com>",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/doeixd/foldkit-plus.git",
|
|
23
|
+
"directory": "packages/richtext-dom"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/doeixd/foldkit-plus/tree/main/packages/richtext-dom#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/doeixd/foldkit-plus/issues"
|
|
28
|
+
},
|
|
29
|
+
"sideEffects": false,
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"types": "./dist/index.d.mts",
|
|
33
|
+
"import": "./dist/index.mjs"
|
|
34
|
+
},
|
|
35
|
+
"./editor": {
|
|
36
|
+
"types": "./dist/editor.d.mts",
|
|
37
|
+
"import": "./dist/editor.mjs"
|
|
38
|
+
},
|
|
39
|
+
"./editor-bundle": {
|
|
40
|
+
"types": "./dist/editor-bundle.d.mts",
|
|
41
|
+
"import": "./dist/editor-bundle.mjs"
|
|
42
|
+
},
|
|
43
|
+
"./events": {
|
|
44
|
+
"types": "./dist/events.d.mts",
|
|
45
|
+
"import": "./dist/events.mjs"
|
|
46
|
+
},
|
|
47
|
+
"./host": {
|
|
48
|
+
"types": "./dist/host.d.mts",
|
|
49
|
+
"import": "./dist/host.mjs"
|
|
50
|
+
},
|
|
51
|
+
"./html": {
|
|
52
|
+
"types": "./dist/html.d.mts",
|
|
53
|
+
"import": "./dist/html.mjs"
|
|
54
|
+
},
|
|
55
|
+
"./toolbar": {
|
|
56
|
+
"types": "./dist/toolbar.d.mts",
|
|
57
|
+
"import": "./dist/toolbar.mjs"
|
|
58
|
+
},
|
|
59
|
+
"./view": {
|
|
60
|
+
"types": "./dist/view.d.mts",
|
|
61
|
+
"import": "./dist/view.mjs"
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
"files": [
|
|
65
|
+
"dist",
|
|
66
|
+
"README.md",
|
|
67
|
+
"LICENSE"
|
|
68
|
+
],
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"foldkit-richtext": "0.1.0"
|
|
71
|
+
},
|
|
72
|
+
"peerDependencies": {
|
|
73
|
+
"effect": "^4.0.0-rc.116",
|
|
74
|
+
"foldkit": "^0.163.0",
|
|
75
|
+
"foldkit-bundle": "^0.3.0"
|
|
76
|
+
},
|
|
77
|
+
"devDependencies": {
|
|
78
|
+
"effect": "4.0.0-rc.116",
|
|
79
|
+
"foldkit": "0.163.0",
|
|
80
|
+
"jsdom": "26.1.0",
|
|
81
|
+
"tsdown": "^0.23.0",
|
|
82
|
+
"typescript": "5.9.3",
|
|
83
|
+
"foldkit-bundle": "0.3.0"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "tsdown",
|
|
87
|
+
"typecheck": "tsc -b"
|
|
88
|
+
}
|
|
89
|
+
}
|