UNPKG

alinea

Version:

[![npm](https://img.shields.io/npm/v/alinea.svg)](https://npmjs.org/package/alinea) [![install size](https://packagephobia.com/badge?p=alinea)](https://packagephobia.com/result?p=alinea)

335 lines (333 loc) 9.64 kB
import { YMap, YText, YXmlElement, YXmlFragment, YXmlHook, YXmlText } from "../../chunks/chunk-OYP4EJOA.js"; import "../../chunks/chunk-O6EXLFU2.js"; import "../../chunks/chunk-U5RRZUYZ.js"; // src/core/shape/RichTextShape.ts import { Entry } from "../Entry.js"; import { Hint } from "../Hint.js"; import { TextNode } from "../TextDoc.js"; import { entries, fromEntries, keys } from "../util/Objects.js"; import { RecordShape } from "./RecordShape.js"; import { ScalarShape } from "./ScalarShape.js"; function serialize(item) { if (item instanceof YXmlHook) { return []; } if (item instanceof YXmlText) { const delta = item.toDelta(); return delta.map((d) => { const text = { type: "text", text: d.insert }; if (d.attributes) { text.marks = Object.keys(d.attributes).map((type) => { const attrs2 = d.attributes[type]; const mark = { type }; if (attrs2 && Object.keys(attrs2).length) mark.attrs = attrs2; return mark; }); } return text; }); } const res = { type: item.nodeName }; const attrs = item.getAttributes(); if (attrs && Object.keys(attrs).length) Object.assign(res, attrs); const children = item.toArray(); if (children.length) { res.content = children.map(serialize).flat(); } return res; } function unserializeMarks(marks) { return Object.fromEntries(marks.map((mark) => [mark.type, { ...mark.attrs }])); } function unserialize(node) { switch (node.type) { case "text": { const { text, marks } = node; const type = new YXmlText(); if (text) type.insert(0, text, marks && unserializeMarks(marks)); return type; } default: { const { type, 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, content.map(unserialize)); return element; } } } var linkInfoFields = void 0; var RichTextShape = class { constructor(label, shapes, initialValue) { this.label = label; this.shapes = shapes; this.initialValue = initialValue; this.values = shapes ? fromEntries( entries(shapes).map(([key, value]) => { return [ key, new RecordShape(value.label, { type: new ScalarShape("Type"), ...value.properties }) ]; }) ) : {}; } values; innerTypes(parents) { if (!this.shapes) return []; return entries(this.shapes).flatMap(([name, shape]) => { const info = { name, shape, parents }; const inner = shape.innerTypes(parents.concat(name)); if (Hint.isDefinitionName(name)) return [info, ...inner]; return inner; }); } create() { return this.initialValue ?? []; } typeOfChild(yValue, child) { const block = yValue.get(child); const type = block && block.get("type"); const value = type && this.values && this.values[type]; if (value) return value; throw new Error(`Type of block "${child}" not found`); } toXml(rows) { const types = this.values; return rows.map((row) => { return row.type in types ? { type: row.type, id: row.id } : row; }).map(unserialize); } toY(value) { const map = new YMap(); const text = new YXmlFragment(); map.set("$text", text); const types = this.values; if (!Array.isArray(value)) return map; for (const node of value) { const type = types[node.type]; if (type && "id" in node) map.set(node.id, type.toY(node)); } text.insert(0, this.toXml(value)); return map; } fromY(value) { if (!value) return []; const text = value.get("$text"); const types = this.values ?? {}; const content = text?.toArray()?.map(serialize)?.flat() || []; const isEmpty = content.length === 1 && content[0].type === "paragraph" && content[0].content?.length === 0; if (isEmpty) return []; return content.map((node) => { const type = types[node.type]; if (type && "id" in node) { return { id: node.id, type: node.type, ...type.fromY(value.get(node.id)) }; } 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( (row) => this.values?.[row.type] && "id" in row ); const currentKeys = new Set( [...current.keys()].filter((key2) => key2 !== "$text") ); const valueKeys = new Set(blocks.map((row) => row.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.id === id); if (!row) continue; const type = row.type; const rowType = this.values[type]; if (!rowType) continue; current.set(id, rowType.toY(row)); } for (const id of changed) { const row = blocks.find((row2) => row2.id === id); if (!row) continue; const type = row.type; const currentRow = current.get(id); if (!currentRow) continue; const currentType = currentRow.get("type"); if (currentType !== type) { current.delete(id); current.set(id, this.values[type].toY(row)); continue; } const rowType = this.values[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 { type, content, ...attrs } = target; const isBlock = type in this.values; const keysToHandle = isBlock ? ["id"] : keys(attrs); for (const key2 of keysToHandle) source.setAttribute(key2, attrs[key2]); if (isBlock) return; for (const key2 of keys(source.getAttributes())) if (!keysToHandle.includes(key2)) source.removeAttribute(key2); syncNodes(source, content ?? []); }; 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.type; if (typeA !== typeB) { source.delete(i); source.insert(i, this.toXml([row])); continue; } if (typeA === "text") { syncText(node, row); continue; } syncElement(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) { return () => () => { }; } mutator(parent, key, readOnly) { let map = parent.get(key); if (!map) { parent.set(key, this.toY([])); map = parent.get(key); } return { readOnly, map, fragment: map.get("$text"), insert: (id, block) => { if (!this.values) throw new Error("No types defined"); const shape = this.values[block]; const row = { ...shape.create(), id, type: block }; map.set(id, shape.toY(row)); } }; } async applyLinks(doc, loader) { if (!Array.isArray(doc)) return; const links = /* @__PURE__ */ new Map(); iterMarks(doc, (mark) => { if (mark.type !== "link") return; const id = mark.attrs["data-entry"]; if (id) links.set(mark, id); }); async function loadLinks() { const linkIds = Array.from(new Set(links.values())); linkInfoFields ??= { url: Entry.url, // This is MediaFile.location - but we're avoiding circular imports here location: Entry.data.get("location") }; const entries2 = await loader.resolveLinks(linkInfoFields, linkIds); const info = new Map(linkIds.map((id, i) => [id, entries2[i]])); for (const [mark, id] of links) { const type = mark.attrs["data-type"]; const data = info.get(id); if (data) mark.attrs["href"] = type === "file" ? data.location : data.url; } } await Promise.all( [loadLinks()].concat( doc.flatMap((row) => { const subType = this.values?.[row.type]; if (!subType) return []; return [subType.applyLinks(row, loader)]; }) ) ); } }; function iterMarks(doc, fn) { for (const row of doc) { if (row.marks) row.marks.forEach(fn); if (!TextNode.isElement(row)) continue; if (row.content) iterMarks(row.content, fn); } } export { RichTextShape };