UNPKG

alinea

Version:
435 lines (433 loc) 13.6 kB
import { YMap, YText, YXmlElement, YXmlFragment, YXmlHook, YXmlText } from "../../chunks/chunk-QUIANN6B.js"; import "../../chunks/chunk-AJJSW27C.js"; import "../../chunks/chunk-NZLE2WMY.js"; // src/core/shape/RichTextShape.ts import { Entry } from "../Entry.js"; import { MediaFile } from "../media/MediaTypes.js"; import { BlockNode, ElementNode, LinkMark, Mark, Node, TextNode } from "../TextDoc.js"; import { entries, fromEntries, keys } from "../util/Objects.js"; import { RecordShape } from "./RecordShape.js"; import { ScalarShape } from "./ScalarShape.js"; var RichTextElements = /* @__PURE__ */ ((RichTextElements2) => { RichTextElements2["h1"] = "h1"; RichTextElements2["h2"] = "h2"; RichTextElements2["h3"] = "h3"; RichTextElements2["h4"] = "h4"; RichTextElements2["h5"] = "h5"; RichTextElements2["h6"] = "h6"; RichTextElements2["p"] = "p"; RichTextElements2["b"] = "b"; RichTextElements2["i"] = "i"; RichTextElements2["ul"] = "ul"; RichTextElements2["ol"] = "ol"; RichTextElements2["li"] = "li"; RichTextElements2["a"] = "a"; RichTextElements2["blockquote"] = "blockquote"; RichTextElements2["hr"] = "hr"; RichTextElements2["br"] = "br"; RichTextElements2["small"] = "small"; RichTextElements2["sup"] = "sup"; RichTextElements2["sub"] = "sub"; RichTextElements2["table"] = "table"; RichTextElements2["tbody"] = "tbody"; RichTextElements2["td"] = "td"; RichTextElements2["th"] = "th"; RichTextElements2["tr"] = "tr"; return RichTextElements2; })(RichTextElements || {}); function serialize(item) { if (item instanceof YXmlHook) { return []; } if (item instanceof YXmlText) { const delta = item.toDelta(); return delta.map((d) => { const text = { [Node.type]: "text", [TextNode.text]: d.insert }; if (d.attributes) { text.marks = Object.keys(d.attributes).map((type) => { const attrs2 = d.attributes[type]; const mark = { [Mark.type]: type }; if (attrs2) for (const [key, value] of Object.entries(attrs2)) { if (typeof value !== "string") continue; if (key.startsWith("data-")) mark[`_${key.slice("data-".length)}`] = value; else mark[key] = value; } return mark; }); } return text; }); } const res = { [Node.type]: item.nodeName }; if (typeof item.getAttributes !== "function") return res; const attrs = item.getAttributes(); if (attrs && Object.keys(attrs).length) Object.assign(res, attrs); const children = item.toArray(); if (children.length) { res.content = children.flatMap(serialize); } return res; } function unserializeMarks(marks) { return Object.fromEntries( marks.map((mark) => { const { [Mark.type]: type, ...attrs } = mark; const res = Object.fromEntries( Object.entries(attrs).map(([key, value]) => { if (key.startsWith("_")) return [`data-${key.slice(1)}`, value]; return [key, value]; }) ); return [type, res]; }) ); } function unserialize(nodes) { const result = []; for (const node of nodes) { if (Node.isText(node)) { const text = node[TextNode.text]; const marks = node[TextNode.marks]; const type = new YXmlText(); if (text) type.insert(0, text, marks && unserializeMarks(marks)); result.push(type); } else if (Node.isElement(node)) { const { [Node.type]: type, [ElementNode.content]: content, ...attrs } = node; const element = new YXmlElement(type); for (const key in attrs) { const val = attrs[key]; if (val) element.setAttribute(key, val); } if (content) element.insert(0, unserialize(content)); result.push(element); } else if (Node.isBlock(node)) { const element = new YXmlElement(node[Node.type]); element.setAttribute(BlockNode.id, node[BlockNode.id]); result.push(element); } } return result; } var linkInfoFields = { id: Entry.id, url: Entry.url, location: MediaFile.location }; var RichTextShape = class { constructor(label, shapes, initialValue, searchable) { this.label = label; this.shapes = shapes; this.initialValue = initialValue; this.searchable = searchable; this.blocks = shapes ? fromEntries( entries(shapes).map(([key, value]) => { return [ key, new RecordShape(value.label, { [Node.type]: new ScalarShape("Type"), [BlockNode.id]: new ScalarShape("Id"), ...value.shapes }) ]; }) ) : {}; } blocks; create() { return this.initialValue ?? []; } toXml(rows) { return unserialize(rows); } toV1(value) { if (!Array.isArray(value)) return []; return value.map(this.normalizeRow); } normalizeRow = (row) => { if (Node.type in row) return row; const { type, ...data } = row; if (type === "text") { const updated = { [Node.type]: "text", [TextNode.text]: data.text }; if (!data.marks) return updated; return { ...updated, [TextNode.marks]: data.marks.map((mark) => { const { type: type2, attrs } = mark; if (type2 !== "link") return { [Mark.type]: type2, ...attrs }; const { "data-id": id, "data-entry": entry, "data-type": link, ...rest2 } = attrs; const res2 = {}; if (type2) res2[Mark.type] = type2; if (id) res2[LinkMark.id] = id; if (entry) res2[LinkMark.entry] = entry; if (link) res2[LinkMark.link] = link; for (const [key, value] of entries(rest2)) if (typeof value === "string") res2[key] = rest2[key]; return res2; }) }; } const shape = this.blocks[type]; if (shape) { return { [Node.type]: type, [BlockNode.id]: data.id, ...shape.toV1(data) }; } const { content, ...rest } = data; if (type === "heading" && rest.textAlign === "left") rest.textAlign = void 0; const res = { [Node.type]: type, ...rest }; if (content) res[ElementNode.content] = content.map(this.normalizeRow); return res; }; toY(value) { const map = new YMap(); const text = new YXmlFragment(); map.set("$text", text); const types = this.blocks; if (!Array.isArray(value)) return map; for (const node of value) { if (!Node.isBlock(node)) continue; const type = types[node[Node.type]]; map.set(node[BlockNode.id], type.toY(node)); } text.insert(0, this.toXml(value)); return map; } fromY(map) { if (!map) return []; const text = map.get("$text"); const types = this.blocks ?? {}; const content = text?.toArray()?.flatMap(serialize) || []; const [first] = content; const isEmpty = content.length === 1 && Node.isElement(first) && first[Node.type] === "paragraph" && first[ElementNode.content]?.length === 0; if (isEmpty) return []; return content.map((node) => { if (Node.isBlock(node)) { const shape = types[node[Node.type]]; if (shape) return { [Node.type]: node[Node.type], [BlockNode.id]: node[BlockNode.id], ...shape.fromY(map.get(node[BlockNode.id])) }; } if (Node.isElement(node)) { if (node[Node.type] === "heading") { if (node.textAlign === "left") node.textAlign = void 0; } } return node; }); } applyY(value, parent, key) { const current = parent.get(key); if (!current || !value) return void parent.set(key, this.toY(value)); const blocks = value.filter(Node.isBlock); const currentKeys = new Set( [...current.keys()].filter((key2) => key2 !== "$text") ); const valueKeys = new Set(blocks.map((row) => row[BlockNode.id])); const removed = [...currentKeys].filter((key2) => !valueKeys.has(key2)); const added = [...valueKeys].filter((key2) => !currentKeys.has(key2)); const changed = [...valueKeys].filter((key2) => currentKeys.has(key2)); for (const id of removed) current.delete(id); for (const id of added) { const row = blocks.find((row2) => row2[BlockNode.id] === id); if (!row) continue; const type = row[Node.type]; const rowType = this.blocks[type]; if (!rowType) continue; current.set(id, rowType.toY(row)); } for (const id of changed) { const row = blocks.find((row2) => row2[BlockNode.id] === id); if (!row) continue; const type = row[Node.type]; const currentRow = current.get(id); if (!currentRow) continue; const currentType = currentRow.get(Node.type); if (currentType !== type) { current.delete(id); current.set(id, this.blocks[type].toY(row)); continue; } const rowType = this.blocks[type]; if (!rowType) continue; rowType.applyY(row, current, id); } function syncText(source, target) { const { text = "", marks = [] } = target; const str = YText.prototype.toString.call(source); if (text === str) { source.format(0, source.length, unserializeMarks(marks)); } else { source.delete(0, source.length); source.insert(0, text, unserializeMarks(marks)); } } const syncElement = (source, target) => { const { [Node.type]: type, content, ...attrs } = target; const keysToHandle = keys(attrs); for (const key2 of keys(attrs)) source.setAttribute(key2, attrs[key2]); for (const key2 of keys(source.getAttributes())) if (!keysToHandle.includes(key2)) source.removeAttribute(key2); syncNodes(source, content ?? []); }; const syncBlock = (source, target) => { source.setAttribute(BlockNode.id, target[BlockNode.id]); }; const syncNodes = (source, value2) => { let i = 0; for (; i < value2.length; i++) { const row = value2[i]; const node = source.get(i); if (!node) { source.insert(i, this.toXml([row])); continue; } const typeA = node instanceof YXmlText ? "text" : node.nodeName; const typeB = row[Node.type]; if (typeA !== typeB) { source.delete(i); source.insert(i, this.toXml([row])); continue; } if (Node.isText(row)) { syncText(node, row); } else if (Node.isElement(row)) { syncElement(node, row); } else if (Node.isBlock(row)) { syncBlock(node, row); } } while (source.length > i) source.delete(i); }; syncNodes(current.get("$text"), value); } init(parent, key) { if (!parent.has(key)) parent.set(key, this.toY(this.create())); } watch(parent, key) { const map = parent.get(key); return (fun) => { const listener = (events, tx) => { if (tx.origin === "self") return; fun(); }; map.observeDeep(listener); return () => map.unobserveDeep(listener); }; } mutator(parent, key) { const map = parent.get(key); return { map, fragment: map.get("$text"), insert: (id, block) => { if (!this.blocks) throw new Error("No types defined"); const shape = this.blocks[block]; map.set( id, shape.toY({ ...shape.create(), [Node.type]: block, [BlockNode.id]: id }) ); } }; } async applyLinks(doc, loader) { if (!Array.isArray(doc)) return; const links = /* @__PURE__ */ new Map(); iterMarks(doc, (mark) => { if (mark[Mark.type] !== "link") return; const entryId = mark[LinkMark.entry]; if (typeof entryId === "string") links.set(mark, entryId); }); async function loadLinks() { const linkIds = Array.from(new Set(links.values())); const entries2 = await loader.resolveLinks(linkInfoFields, linkIds); const info = new Map( entries2.map((entry) => { return [entry.id, entry]; }) ); for (const [mark, entryId] of links) { const type = mark[LinkMark.link]; const data = info.get(entryId); if (data) mark.href = type === "file" ? data.location : data.url; } } await Promise.all( [loadLinks()].concat( doc.flatMap((row) => { if (!this.blocks || !Node.isBlock(row)) return []; const shape = this.blocks[row[Node.type]]; if (!shape) return []; return [shape.applyLinks(row, loader)]; }) ) ); } searchableText(value) { const res = ""; if (!this.searchable) return res; if (!Array.isArray(value)) return res; return value.reduce((acc, node) => { return acc + this.textOf(node); }, ""); } textOf(node) { if (Node.isText(node)) { return node.text ? ` ${node.text}` : ""; } if (Node.isElement(node) && node.content) { return node.content.reduce((acc, node2) => { return acc + this.textOf(node2); }, ""); } if (Node.isBlock(node)) { const shape = this.blocks[node[Node.type]]; if (shape) return shape.searchableText(node); } return ""; } }; function iterMarks(doc, fn) { for (const row of doc) { if (Node.isText(row)) row.marks?.forEach(fn); else if (Node.isElement(row) && row.content) iterMarks(row.content, fn); } } export { RichTextElements, RichTextShape };