json-joy
Version:
Collection of libraries for building collaborative editing apps.
203 lines (202 loc) • 7.53 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.importStyle = exports.importHtml = exports.textFromHtml = exports.htmlToHast = exports.fromHtml = exports.fromHast = exports.fromJsonMl = exports.toViewRange = void 0;
const html_1 = require("very-small-parser/lib/html");
const fromHast_1 = require("very-small-parser/lib/html/json-ml/fromHast");
const slice_1 = require("../slice");
const constants_1 = require("../slice/constants");
const constants_2 = require("../rga/constants");
const toPlainText_1 = require("very-small-parser/lib/toPlainText");
const walk_1 = require("very-small-parser/lib/html/json-ml/walk");
const fromBase64_1 = require("@jsonjoy.com/base64/lib/fromBase64");
/**
* @todo Implement HTML normalization function, ensure that:
*
* - <blockquote> and <p> nodes are treated correctly, especially when sole node
* is nested.
* - list nodes are treated correctly.
* - <svg> nodes are converted to Base64 and inlined as data URL images.
*/
/**
* Flattens a {@link PeritextMlNode} tree structure into a {@link ViewRange}
* flat string with annotation ranges.
*/
class ViewRangeBuilder {
constructor() {
this.text = '';
this.slices = [];
}
build0(node, path) {
const skipWhitespace = path.length < 2;
if (typeof node === 'string') {
if (skipWhitespace && !node.trim())
return false;
this.text += node;
return false;
}
const [type, attr] = node;
const start = this.text.length;
const length = node.length;
const inline = !!attr?.inline;
const hasType = type === 0 || !!type;
const firstChild = node[2];
const isFirstChildInline = firstChild && (typeof firstChild === 'string' || firstChild[1]?.inline);
if (hasType && !inline && isFirstChildInline) {
this.text += '\n';
const header = (constants_1.SliceStacking.Marker << constants_1.SliceHeaderShift.Stacking) +
(constants_2.Anchor.Before << constants_1.SliceHeaderShift.X1Anchor) +
(constants_2.Anchor.Before << constants_1.SliceHeaderShift.X2Anchor);
const slice = [header, start, start, path.length ? [...path, type] : type];
const data = attr?.data;
if (data)
slice.push(data);
this.slices.push(slice);
}
for (let i = 2; i < length; i++)
this.build0(node[i], type === '' ? path : [...path, type]);
if (hasType && inline) {
let end = 0, header = 0;
if (inline) {
end = this.text.length;
const stacking = attr?.stacking ?? constants_1.SliceStacking.Many;
header =
(stacking << constants_1.SliceHeaderShift.Stacking) +
(constants_2.Anchor.Before << constants_1.SliceHeaderShift.X1Anchor) +
(constants_2.Anchor.After << constants_1.SliceHeaderShift.X2Anchor);
const slice = [header, start, end, type];
const data = attr?.data;
if (data)
slice.push(data);
this.slices.push(slice);
}
}
return false;
}
build(node) {
this.build0(node, []);
const view = [this.text, 0, this.slices];
return view;
}
}
const toViewRange = (node) => new ViewRangeBuilder().build(node);
exports.toViewRange = toViewRange;
// HTML elements to completely ignore.
const IGNORE_TAGS = new Set(['meta', 'style', 'script', 'link', 'head']);
// HTML elements to rewrite as different block elements.
const BLOCK_TAGS_REWRITE = new Map([
['html', ''],
['body', ''],
['div', ''],
]);
// HTML elements to rewrite as different inline elements.
const INLINE_TAGS_REWRITE = new Map([['span', '']]);
const fromJsonMl = (jsonml, registry) => {
if (typeof jsonml === 'string')
return jsonml;
let tag = jsonml[0];
let inlineHtmlTag = false;
if (typeof tag === 'string') {
tag = tag.toLowerCase();
if (IGNORE_TAGS.has(tag))
return '';
const mapped = BLOCK_TAGS_REWRITE.get(tag);
if (mapped !== undefined)
tag = mapped;
else {
const mapped = INLINE_TAGS_REWRITE.get(tag);
if (mapped !== undefined) {
tag = mapped;
inlineHtmlTag = true;
}
}
}
const length = jsonml.length;
const node = [tag, null];
for (let i = 2; i < length; i++) {
const peritextNode = (0, exports.fromJsonMl)(jsonml[i], registry);
if (!peritextNode)
continue;
node.push(peritextNode);
}
const res = registry.fromHtml(jsonml);
if (res) {
node[0] = res[0];
node[1] = res[1];
}
else {
if (typeof tag === 'string')
node[0] = slice_1.SliceTypeName[tag] ?? tag;
const attr = jsonml[1] || {};
let data = null;
if (attr['data-attr'] !== void 0) {
try {
data = JSON.parse(attr['data-attr']);
}
catch { }
}
const inline = inlineHtmlTag || attr['data-inline'] === 'true';
if (data || inline)
node[1] = { data, inline };
}
if (typeof node[0] === 'number' && node[0] < 0)
(node[1] || (node[1] = {})).inline = true;
if (node.length < 3 && (node[1] || {}).inline)
return '';
return node;
};
exports.fromJsonMl = fromJsonMl;
const fromHast = (hast, registry) => {
const jsonml = (0, fromHast_1.fromHast)(hast);
return (0, exports.fromJsonMl)(jsonml, registry);
};
exports.fromHast = fromHast;
const fromHtml = (html, registry) => {
const hast = html_1.html.parsef(html);
return (0, exports.fromHast)(hast, registry);
};
exports.fromHtml = fromHtml;
const htmlToHast = (html) => html_1.html.parsef(html);
exports.htmlToHast = htmlToHast;
const textFromHtml = (html) => {
const hast = html_1.html.parsef(html);
return (0, toPlainText_1.toPlainText)(hast);
};
exports.textFromHtml = textFromHtml;
const getExportData = (html) => {
const attrName = 'data-json-joy-peritext';
const maybeHasPeritextExport = html.includes(attrName);
const hast = html_1.html.parsef(html);
const jsonml = (0, fromHast_1.fromHast)(hast);
if (maybeHasPeritextExport) {
const iterator = (0, walk_1.walk)(jsonml);
let node;
while ((node = iterator())) {
if (node && typeof node === 'object') {
const [tag, attr] = node;
if (attr?.[attrName]) {
const jsonBase64 = attr[attrName];
const buffer = (0, fromBase64_1.fromBase64)(jsonBase64);
const json = new TextDecoder().decode(buffer);
const data = JSON.parse(json);
return [void 0, data];
}
}
}
}
return [jsonml];
};
const importHtml = (html, registry) => {
const [jsonml, data] = getExportData(html);
if (data?.style)
return [void 0, data.style];
if (data?.view)
return [data.view];
const node = (0, exports.fromJsonMl)(jsonml, registry);
return [(0, exports.toViewRange)(node)];
};
exports.importHtml = importHtml;
const importStyle = (html) => {
const [, data] = getExportData(html);
return data?.style;
};
exports.importStyle = importStyle;
;