alinea
Version:
[](https://npmjs.org/package/alinea) [](https://packagephobia.com/result?p=alinea)
335 lines (333 loc) • 9.64 kB
JavaScript
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
};