import { d as Extension, P as Plugin, a as PluginKey, Z as combineTransactionSteps, _ as getChangedRanges, $ as findChildrenInRange, a0 as Slice, F as Fragment, S as Selection, a1 as SelectionRange, T as TextSelection, a2 as keydownHandler, D as DecorationSet, A as Decoration, b as NodeSelection, a3 as Transform, a4 as AllSelection, R as Mark, N as Node3, Q as DOMParser$1, a5 as ReplaceStep, a6 as findParentNode, a7 as Mapping, a8 as DOMSerializer, q as callOrReturn, s as getExtensionField, m as mergeAttributes, a9 as index_default, aa as index_default$1, ab as index_default$2, ac as index_default$3, ad as index_default$4, o as dropPoint, ae as getMarkRange, Y as posToDOMRect, x as redo$1, y as undo$1, v as history, af as findChildren, ag as closeHistory, u as gapCursor, ah as getSchema, ai as createDocument, E as Editor, aj as Node$1, ak as keymap, i as isNodeSelection, O as extensions_exports, J as Text, L as Link, al as findParentNodeClosestToPos, am as selectionToInsertionEnd, an as ReplaceAroundStep, } from "./index-Bs--Ol2m.js"; import { g as getDefaultExportFromCjs } from "./_commonjsHelpers-B85MJLTf.js"; import { importShared } from "./__federation_fn_import-hlt2XzeI.js"; import { j as jsxRuntimeExports } from "./jsx-runtime-CvJTHeKY.js"; const scriptRel = /* @__PURE__ */ (function detectScriptRel() { const relList = typeof document !== "undefined" && document.createElement("link").relList; return relList && relList.supports && relList.supports("modulepreload") ? "modulepreload" : "preload"; })(); const assetsURL = function (dep) { return "/" + dep; }; const seen = {}; const __vitePreload = function preload(baseModule, deps, importerUrl) { let promise = Promise.resolve(); if (true && deps && deps.length > 0) { let allSettled2 = function (promises) { return Promise.all( promises.map((p) => Promise.resolve(p).then( (value) => ({ status: "fulfilled", value }), (reason) => ({ status: "rejected", reason }), ), ), ); }; document.getElementsByTagName("link"); const cspNonceMeta = document.querySelector("meta[property=csp-nonce]"); const cspNonce = cspNonceMeta?.nonce || cspNonceMeta?.getAttribute("nonce"); promise = allSettled2( deps.map((dep) => { dep = assetsURL(dep); if (dep in seen) return; seen[dep] = true; const isCss = dep.endsWith(".css"); const cssSelector = isCss ? '[rel="stylesheet"]' : ""; if (document.querySelector(`link[href="${dep}"]${cssSelector}`)) { return; } const link = document.createElement("link"); link.rel = isCss ? "stylesheet" : scriptRel; if (!isCss) { link.as = "script"; } link.crossOrigin = ""; link.href = dep; if (cspNonce) { link.setAttribute("nonce", cspNonce); } document.head.appendChild(link); if (isCss) { return new Promise((res, rej) => { link.addEventListener("load", res); link.addEventListener("error", () => rej(new Error(`Unable to preload CSS for ${dep}`))); }); } }), ); } function handlePreloadError(err) { const e = new Event("vite:preloadError", { cancelable: true, }); e.payload = err; window.dispatchEvent(e); if (!e.defaultPrevented) { throw err; } } return promise.then((res) => { for (const item of res || []) { if (item.status !== "rejected") continue; handlePreloadError(item.reason); } return baseModule().catch(handlePreloadError); }); }; // Unique ID creation requires a high quality random # generator. In the browser we therefore // require the crypto API and do not support built-in fallback to lower quality random number // generators (like Math.random()). var getRandomValues$1; var rnds8 = new Uint8Array(16); function rng() { // lazy load so that environments that need to polyfill have a chance to do so if (!getRandomValues$1) { // getRandomValues needs to be invoked in a context where "this" is a Crypto implementation. Also, // find the complete implementation of crypto (msCrypto) on IE11. getRandomValues$1 = (typeof crypto !== "undefined" && crypto.getRandomValues && crypto.getRandomValues.bind(crypto)) || (typeof msCrypto !== "undefined" && typeof msCrypto.getRandomValues === "function" && msCrypto.getRandomValues.bind(msCrypto)); if (!getRandomValues$1) { throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported"); } } return getRandomValues$1(rnds8); } const REGEX = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i; function validate(uuid) { return typeof uuid === "string" && REGEX.test(uuid); } /** * Convert array of 16 byte values to UUID string format of the form: * XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX */ var byteToHex = []; for (var i$2 = 0; i$2 < 256; ++i$2) { byteToHex.push((i$2 + 0x100).toString(16).substr(1)); } function stringify$2(arr) { var offset = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0; // Note: Be careful editing this code! It's been tuned for performance // and works in ways you may not expect. See https://github.com/uuidjs/uuid/pull/434 var uuid = ( byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]] ).toLowerCase(); // Consistency check for valid UUID. If this throws, it's likely due to one // of the following: // - One or more input array values don't map to a hex octet (leading to // "undefined" in the uuid) // - Invalid input values for the RFC `version` or `variant` fields if (!validate(uuid)) { throw TypeError("Stringified UUID is invalid"); } return uuid; } function v4(options, buf, offset) { options = options || {}; var rnds = options.random || (options.rng || rng)(); // Per 4.4, set bits for version and `clock_seq_hi_and_reserved` rnds[6] = (rnds[6] & 0x0f) | 0x40; rnds[8] = (rnds[8] & 0x3f) | 0x80; // Copy bytes to buffer, if provided return stringify$2(rnds); } function pt(t, o = JSON.stringify) { const r = {}; return t.filter((n) => { const e = o(n); return Object.prototype.hasOwnProperty.call(r, e) ? false : (r[e] = true); }); } function dt$1(t) { const o = t.filter((n, e) => t.indexOf(n) !== e); return pt(o); } const Q$2 = Extension.create({ name: "uniqueID", // we’ll set a very high priority to make sure this runs first // and is compatible with `appendTransaction` hooks of other extensions priority: 1e4, addOptions() { return { attributeName: "id", types: [], setIdAttribute: false, generateID: () => { if (typeof window < "u" && window.__TEST_OPTIONS) { const t = window.__TEST_OPTIONS; return (t.mockID === void 0 ? (t.mockID = 0) : t.mockID++, t.mockID.toString()); } return v4(); }, filterTransaction: null, }; }, addGlobalAttributes() { return [ { types: this.options.types, attributes: { [this.options.attributeName]: { default: null, parseHTML: (t) => t.getAttribute(`data-${this.options.attributeName}`), renderHTML: (t) => { const o = { [`data-${this.options.attributeName}`]: t[this.options.attributeName], }; return this.options.setIdAttribute ? { ...o, id: t[this.options.attributeName], } : o; }, }, }, }, ]; }, // check initial content for missing ids // onCreate() { // // Don’t do this when the collaboration extension is active // // because this may update the content, so Y.js tries to merge these changes. // // This leads to empty block nodes. // // See: https://github.com/ueberdosis/tiptap/issues/2400 // if ( // this.editor.extensionManager.extensions.find( // (extension) => extension.name === "collaboration" // ) // ) { // return; // } // const { view, state } = this.editor; // const { tr, doc } = state; // const { types, attributeName, generateID } = this.options; // const nodesWithoutId = findChildren(doc, (node) => { // return ( // types.includes(node.type.name) && node.attrs[attributeName] === null // ); // }); // nodesWithoutId.forEach(({ node, pos }) => { // tr.setNodeMarkup(pos, undefined, { // ...node.attrs, // [attributeName]: generateID(), // }); // }); // tr.setMeta("addToHistory", false); // view.dispatch(tr); // }, addProseMirrorPlugins() { let t = null, o = false; return [ new Plugin({ key: new PluginKey("uniqueID"), appendTransaction: (r, n, e) => { const s = r.some((g) => g.docChanged) && !n.doc.eq(e.doc), i = this.options.filterTransaction && r.some((g) => { var C, d; return !((d = (C = this.options).filterTransaction) != null && d.call(C, g)); }); if (!s || i) return; const { tr: l } = e, { types: a, attributeName: c, generateID: h } = this.options, f = combineTransactionSteps(n.doc, r), { mapping: p } = f; if ( (getChangedRanges(f).forEach(({ newRange: g }) => { const C = findChildrenInRange(e.doc, g, (y) => a.includes(y.type.name)), d = C.map(({ node: y }) => y.attrs[c]).filter((y) => y !== null), w = dt$1(d); C.forEach(({ node: y, pos: m }) => { var z; const b = (z = l.doc.nodeAt(m)) == null ? void 0 : z.attrs[c]; if (b === null) { const U = n.doc.type.createAndFill().content; if (n.doc.content.findDiffStart(U) === null) { const q = JSON.parse(JSON.stringify(e.doc.toJSON())); if (((q.content[0].content[0].attrs.id = "initialBlockId"), JSON.stringify(q.content) === JSON.stringify(U.toJSON()))) { l.setNodeMarkup(m, void 0, { ...y.attrs, [c]: "initialBlockId", }); return; } } l.setNodeMarkup(m, void 0, { ...y.attrs, [c]: h(), }); return; } const { deleted: k } = p.invert().mapResult(m); k && w.includes(b) && l.setNodeMarkup(m, void 0, { ...y.attrs, [c]: h(), }); }); }), !!l.steps.length) ) return (l.setMeta("uniqueID", true), l); }, // we register a global drag handler to track the current drag source element view(r) { const n = (e) => { let s; t = !((s = r.dom.parentElement) === null || s === void 0) && s.contains(e.target) ? r.dom.parentElement : null; }; return ( window.addEventListener("dragstart", n), { destroy() { window.removeEventListener("dragstart", n); }, } ); }, props: { // `handleDOMEvents` is called before `transformPasted` so we can do // some checks before. However, `transformPasted` only runs when // editor content is pasted - not external content. handleDOMEvents: { // only create new ids for dropped content while holding `alt` // or content is dragged from another editor drop: (r, n) => { let e; return (t !== r.dom.parentElement || ((e = n.dataTransfer) === null || e === void 0 ? void 0 : e.effectAllowed) === "copy" ? (o = true) : (o = false), (t = null), false); }, // always create new ids on pasted content paste: () => ((o = true), false), }, // we’ll remove ids for every pasted node // so we can create a new one within `appendTransaction` transformPasted: (r) => { if (!o) return r; const { types: n, attributeName: e } = this.options, s = (i) => { const l = []; return ( i.forEach((a) => { if (a.isText) { l.push(a); return; } if (!n.includes(a.type.name)) { l.push(a.copy(s(a.content))); return; } const c = a.type.create( { ...a.attrs, [e]: null, }, s(a.content), a.marks, ); l.push(c); }), Fragment.from(l) ); }; return ((o = false), new Slice(s(r.content), r.openStart, r.openEnd)); }, }, }), ]; }, }); function K$2(t) { return t.type === "link"; } function X$2(t) { return typeof t != "string" && t.type === "link"; } function B$2(t) { return typeof t != "string" && t.type === "text"; } function S$1(t) { var o, r, n, e, s; return ( R$1(t) ? { ...t } : v$2(t) ? { type: "tableCell", content: [].concat(t.content), props: { backgroundColor: ((o = t.props) == null ? void 0 : o.backgroundColor) ?? "default", textColor: ((r = t.props) == null ? void 0 : r.textColor) ?? "default", textAlignment: ((n = t.props) == null ? void 0 : n.textAlignment) ?? "left", colspan: ((e = t.props) == null ? void 0 : e.colspan) ?? 1, rowspan: ((s = t.props) == null ? void 0 : s.rowspan) ?? 1, }, } : { type: "tableCell", content: [].concat(t), props: { backgroundColor: "default", textColor: "default", textAlignment: "left", colspan: 1, rowspan: 1, }, } ); } function v$2(t) { return t != null && typeof t != "string" && !Array.isArray(t) && t.type === "tableCell"; } function R$1(t) { return v$2(t) && t.props !== void 0 && t.content !== void 0; } function A$3(t) { return R$1(t) ? (t.props.colspan ?? 1) : 1; } function D$3(t) { return R$1(t) ? (t.props.rowspan ?? 1) : 1; } class O extends Error { constructor(o) { super(`Unreachable case: ${o}`); } } function At$2(t, o = true) { const { "data-test": r, ...n } = t; if (Object.keys(n).length > 0 && o) throw new Error("Object must be empty " + JSON.stringify(t)); } function Y$1(t, o) { const r = t.resolve(o); if (r.nodeAfter && r.nodeAfter.type.isInGroup("bnBlock")) return { posBeforeNode: r.pos, node: r.nodeAfter, }; let n = r.depth, e = r.node(n); for (; n > 0; ) { if (e.type.isInGroup("bnBlock")) return { posBeforeNode: r.before(n), node: e, }; (n--, (e = r.node(n))); } const s = []; (t.descendants((l, a) => { l.type.isInGroup("bnBlock") && s.push(a); }), console.warn(`Position ${o} is not within a blockContainer node.`)); const i = t.resolve(s.find((l) => l >= o) || s[s.length - 1]); return { posBeforeNode: i.pos, node: i.nodeAfter, }; } function $$4(t, o) { if (!t.type.isInGroup("bnBlock")) throw new Error(`Attempted to get bnBlock node at position but found node of different type ${t.type.name}`); const r = t, n = o, e = n + r.nodeSize, s = { node: r, beforePos: n, afterPos: e, }; if (r.type.name === "blockContainer") { let i, l; if ( (r.forEach((a, c) => { if (a.type.spec.group === "blockContent") { const h = a, f = n + c + 1, p = f + a.nodeSize; i = { node: h, beforePos: f, afterPos: p, }; } else if (a.type.name === "blockGroup") { const h = a, f = n + c + 1, p = f + a.nodeSize; l = { node: h, beforePos: f, afterPos: p, }; } }), !i) ) throw new Error(`blockContainer node does not contain a blockContent node in its children: ${r}`); return { isBlockContainer: true, bnBlock: s, blockContent: i, childContainer: l, blockNoteType: i.node.type.name, }; } else { if (!s.node.type.isInGroup("childContainer")) throw new Error(`bnBlock node is not in the childContainer group: ${s.node}`); return { isBlockContainer: false, bnBlock: s, childContainer: s, blockNoteType: s.node.type.name, }; } } function Z(t) { return $$4(t.node, t.posBeforeNode); } function It$1(t) { if (!t.nodeAfter) throw new Error(`Attempted to get blockContainer node at position ${t.pos} but a node at this position does not exist`); return $$4(t.nodeAfter, t.pos); } function Tt$2(t) { const o = Y$1(t.doc, t.selection.anchor); return Z(o); } function Ot$2(t) { const o = Y$1(t.doc, t.selection.anchor); return Z(o); } function ht(t) { return "doc" in t ? t.doc.type.schema : t.type.schema; } function G(t) { return t.cached.blockNoteEditor; } function J$1(t) { return G(t).schema; } function _$1(t) { return J$1(t).blockSchema; } function H$2(t) { return J$1(t).inlineContentSchema; } function N$1(t) { return J$1(t).styleSchema; } function j$1(t) { return G(t).blockCache; } function gt(t, o, r) { var s, i; const n = { type: "tableContent", columnWidths: [], headerRows: void 0, headerCols: void 0, rows: [], }, e = []; t.content.forEach((l, a, c) => { const h = { cells: [], }; (c === 0 && l.content.forEach((f) => { let p = f.attrs.colwidth; (p == null && (p = new Array(f.attrs.colspan ?? 1).fill(void 0)), n.columnWidths.push(...p)); }), (h.cells = l.content.content.map( (f, p) => ( e[c] || (e[c] = []), (e[c][p] = f.type.name === "tableHeader"), { type: "tableCell", content: f.content.content .map((g) => F$2(g, o, r)) .reduce((g, C) => { if (!g.length) return C; const d = g[g.length - 1], w = C[0]; return w && B$2(d) && B$2(w) && JSON.stringify(d.styles) === JSON.stringify(w.styles) ? ((d.text += ` ` + w.text), g.push(...C.slice(1)), g) : (g.push(...C), g); }, []), props: { colspan: f.attrs.colspan, rowspan: f.attrs.rowspan, backgroundColor: f.attrs.backgroundColor, textColor: f.attrs.textColor, textAlignment: f.attrs.textAlignment, }, } ), )), n.rows.push(h)); }); for (let l = 0; l < e.length; l++) (s = e[l]) != null && s.every((a) => a) && (n.headerRows = (n.headerRows ?? 0) + 1); for (let l = 0; l < ((i = e[0]) == null ? void 0 : i.length); l++) e != null && e.every((a) => a[l]) && (n.headerCols = (n.headerCols ?? 0) + 1); return n; } function F$2(t, o, r) { const n = []; let e; return ( t.content.forEach((s) => { if (s.type.name === "hardBreak") { if (e) if (B$2(e)) e.text += ` `; else if (K$2(e)) e.content[e.content.length - 1].text += ` `; else throw new Error("unexpected"); else e = { type: "text", text: ` `, styles: {}, }; return; } if (s.type.name !== "link" && s.type.name !== "text") { if (!o[s.type.name]) { console.warn("unrecognized inline content type", s.type.name); return; } (e && (n.push(e), (e = void 0)), n.push(wt$1(s, o, r))); return; } const i = {}; let l; for (const a of s.marks) if (a.type.name === "link") l = a; else { const c = r[a.type.name]; if (!c) { if (a.type.spec.blocknoteIgnore) continue; throw new Error(`style ${a.type.name} not found in styleSchema`); } if (c.propSchema === "boolean") i[c.type] = true; else if (c.propSchema === "string") i[c.type] = a.attrs.stringValue; else throw new O(c.propSchema); } e ? B$2(e) ? l ? (n.push(e), (e = { type: "link", href: l.attrs.href, content: [ { type: "text", text: s.textContent, styles: i, }, ], })) : JSON.stringify(e.styles) === JSON.stringify(i) ? (e.text += s.textContent) : (n.push(e), (e = { type: "text", text: s.textContent, styles: i, })) : K$2(e) && (l ? e.href === l.attrs.href ? JSON.stringify(e.content[e.content.length - 1].styles) === JSON.stringify(i) ? (e.content[e.content.length - 1].text += s.textContent) : e.content.push({ type: "text", text: s.textContent, styles: i, }) : (n.push(e), (e = { type: "link", href: l.attrs.href, content: [ { type: "text", text: s.textContent, styles: i, }, ], })) : (n.push(e), (e = { type: "text", text: s.textContent, styles: i, }))) : l ? (e = { type: "link", href: l.attrs.href, content: [ { type: "text", text: s.textContent, styles: i, }, ], }) : (e = { type: "text", text: s.textContent, styles: i, }); }), e && n.push(e), n ); } function wt$1(t, o, r) { if (t.type.name === "text" || t.type.name === "link") throw new Error("unexpected"); const n = {}, e = o[t.type.name]; for (const [l, a] of Object.entries(t.attrs)) { if (!e) throw Error("ic node is of an unrecognized type: " + t.type.name); const c = e.propSchema; l in c && (n[l] = a); } let s; return ( e.content === "styled" ? (s = F$2(t, o, r)) : (s = void 0), { type: t.type.name, props: n, content: s, } ); } function L$2(t, o, r = _$1(o), n = H$2(o), e = N$1(o), s = j$1(o)) { var C; if (!t.type.isInGroup("bnBlock")) throw Error("Node should be a bnBlock, but is instead: " + t.type.name); const i = s == null ? void 0 : s.get(t); if (i) return i; const l = $$4(t, 0); let a = l.bnBlock.node.attrs.id; a === null && (a = Q$2.options.generateID()); const c = r[l.blockNoteType]; if (!c) throw Error("Block is of an unrecognized type: " + l.blockNoteType); const h = {}; for (const [d, w] of Object.entries({ ...t.attrs, ...(l.isBlockContainer ? l.blockContent.node.attrs : {}), })) { const y = c.propSchema; d in y && !(y[d].default === void 0 && w === void 0) && (h[d] = w); } const f = r[l.blockNoteType], p = []; (C = l.childContainer) == null || C.node.forEach((d) => { p.push(L$2(d, o, r, n, e, s)); }); let u; if (f.content === "inline") { if (!l.isBlockContainer) throw new Error("impossible"); u = F$2(l.blockContent.node, n, e); } else if (f.content === "table") { if (!l.isBlockContainer) throw new Error("impossible"); u = gt(l.blockContent.node, n, e); } else if (f.content === "none") u = void 0; else throw new O(f.content); const g = { id: a, type: f.type, props: h, content: u, children: p, }; return (s == null || s.set(t, g), g); } function St$1(t, o = ht(t), r = _$1(o), n = H$2(o), e = N$1(o), s = j$1(o)) { const i = []; return (t.firstChild && t.firstChild.descendants((l) => (i.push(L$2(l, o, r, n, e, s)), false)), i); } function Dt$2(t, o, r = _$1(o), n = H$2(o), e = N$1(o), s = j$1(o)) { function i(l, a, c) { if (l.type.name !== "blockGroup") throw new Error("unexpected"); const h = []; let f, p; return ( l.forEach((u, g, C) => { if (u.type.name !== "blockContainer") throw new Error("unexpected"); if (u.childCount === 0) return; if (u.childCount === 0 || u.childCount > 2) throw new Error("unexpected, blockContainer.childCount: " + u.childCount); const d = C === 0, w = C === l.childCount - 1; if (u.firstChild.type.name === "blockGroup") { if (!d) throw new Error("unexpected"); const k = i(u.firstChild, Math.max(0, a - 1), w ? Math.max(0, c - 1) : 0); ((f = k.blockCutAtStart), w && (p = k.blockCutAtEnd), h.push(...k.blocks)); return; } const y = L$2(u, o, r, n, e, s), m = u.childCount > 1 ? u.child(1) : void 0; let b = []; if (m) { const k = i( m, 0, // TODO: can this be anything other than 0? w ? Math.max(0, c - 1) : 0, ); ((b = k.blocks), w && (p = k.blockCutAtEnd)); } (w && !m && c > 1 && (p = y.id), d && a > 1 && (f = y.id), h.push({ ...y, children: b, })); }), { blocks: h, blockCutAtStart: f, blockCutAtEnd: p } ); } if (t.content.childCount === 0) return { blocks: [], blockCutAtStart: void 0, blockCutAtEnd: void 0, }; if (t.content.childCount !== 1) throw new Error("slice must be a single block, did you forget includeParents=true?"); return i(t.content.firstChild, Math.max(t.openStart - 1, 0), Math.max(t.openEnd - 1, 0)); } function x(t) { const { height: o, width: r } = tt(t), n = new Array(o).fill(false).map(() => new Array(r).fill(null)), e = (s, i) => { for (let l = s; l < o; l++) for (let a = i; a < r; a++) if (!n[l][a]) return { row: l, col: a }; throw new Error("Unable to create occupancy grid for table, no more available cells"); }; for (let s = 0; s < t.content.rows.length; s++) for (let i = 0; i < t.content.rows[s].cells.length; i++) { const l = S$1(t.content.rows[s].cells[i]), a = D$3(l), c = A$3(l), { row: h, col: f } = e(s, i); for (let p = h; p < h + a; p++) for (let u = f; u < f + c; u++) { if (n[p][u]) throw new Error(`Unable to create occupancy grid for table, cell at ${p},${u} is already occupied`); n[p][u] = { row: s, col: i, rowspan: a, colspan: c, cell: l, }; } } return n; } function I$2(t) { const o = /* @__PURE__ */ new Set(); return t.map((r) => ({ cells: r.map((n) => (o.has(n.row + ":" + n.col) ? false : (o.add(n.row + ":" + n.col), n.cell))).filter((n) => n !== false), })); } function E$1(t, o, r = x(o)) { for (let n = 0; n < r.length; n++) for (let e = 0; e < r[n].length; e++) { const s = r[n][e]; if (s && s.row === t.row && s.col === t.col) return { row: n, col: e, cell: s.cell }; } throw new Error(`Unable to resolve relative table cell indices for table, cell at ${t.row},${t.col} is not occupied`); } function tt(t) { const o = t.content.rows.length; let r = 0; return ( t.content.rows.forEach((n) => { let e = 0; (n.cells.forEach((s) => { e += A$3(s); }), (r = Math.max(r, e))); }), { height: o, width: r } ); } function et(t, o, r = x(o)) { var e; const n = (e = r[t.row]) == null ? void 0 : e[t.col]; if (n) return { row: n.row, col: n.col, cell: n.cell, }; } function yt$1(t, o) { var s; const r = x(t); if (o < 0 || o >= r.length) return []; let n = 0; for (let i = 0; i < o; i++) { const l = (s = r[n]) == null ? void 0 : s[0]; if (!l) return []; n += l.rowspan; } const e = new Array(r[0].length) .fill(false) .map((i, l) => et({ row: n, col: l }, t, r)) .filter((i) => i !== void 0); return e.filter((i, l) => e.findIndex((a) => a.row === i.row && a.col === i.col) === l); } function Ct$1(t, o) { var s; const r = x(t); if (o < 0 || o >= r[0].length) return []; let n = 0; for (let i = 0; i < o; i++) { const l = (s = r[0]) == null ? void 0 : s[n]; if (!l) return []; n += l.colspan; } const e = new Array(r.length) .fill(false) .map((i, l) => et({ row: l, col: n }, t, r)) .filter((i) => i !== void 0); return e.filter((i, l) => e.findIndex((a) => a.row === i.row && a.col === i.col) === l); } function Mt$2(t, o, r, n = x(t)) { const { col: e } = E$1( { row: 0, col: o, }, t, n, ), { col: s } = E$1( { row: 0, col: r, }, t, n, ); return ( n.forEach((i) => { const [l] = i.splice(e, 1); i.splice(s, 0, l); }), I$2(n) ); } function Pt$2(t, o, r, n = x(t)) { const { row: e } = E$1( { row: o, col: 0, }, t, n, ), { row: s } = E$1( { row: r, col: 0, }, t, n, ), [i] = n.splice(e, 1); return (n.splice(s, 0, i), I$2(n)); } function M(t) { return ( t ? v$2(t) ? M(t.content) : typeof t == "string" ? t.length === 0 : Array.isArray(t) ? t.every((o) => typeof o == "string" ? o.length === 0 : B$2(o) ? o.text.length === 0 : X$2(o) ? typeof o.content == "string" ? o.content.length === 0 : o.content.every((r) => r.text.length === 0) : false, ) : false : true ); } function Rt$2(t, o, r = x(t)) { if (o === "columns") { let s = 0; for (let i = r[0].length - 1; i >= 0 && r.every((a) => M(a[i].cell) && a[i].colspan === 1); i--) s++; for (let i = r.length - 1; i >= 0; i--) { const l = Math.max(r[i].length - s, 1); r[i] = r[i].slice(0, l); } return I$2(r); } let n = 0; for (let s = r.length - 1; s >= 0 && r[s].every((l) => M(l.cell) && l.rowspan === 1); s--) n++; const e = Math.min(n, r.length - 1); return (r.splice(r.length - e, e), I$2(r)); } function $t$2(t, o, r, n = x(t)) { const { width: e, height: s } = tt(t); if (o === "columns") n.forEach((i, l) => { if (r >= 0) for (let a = 0; a < r; a++) i.push({ row: l, col: Math.max(...i.map((c) => c.col)) + 1, rowspan: 1, colspan: 1, cell: S$1(""), }); else i.splice(e + r, -1 * r); }); else if (r > 0) for (let i = 0; i < r; i++) { const l = new Array(e).fill(null).map((a, c) => ({ row: s + i, col: c, rowspan: 1, colspan: 1, cell: S$1(""), })); n.push(l); } else r < 0 && n.splice(s + r, -1 * r); return I$2(n); } function Jt$2(t, o, r) { const n = yt$1(t, r); if (!n.some((a) => D$3(a.cell) > 1)) return true; let s = r, i = r; return ( n.forEach((a) => { const c = D$3(a.cell); ((s = Math.max(s, a.row + c - 1)), (i = Math.min(i, a.row))); }), o < r ? r === s : r === i ); } function _t$3(t, o, r) { const n = Ct$1(t, r); if (!n.some((a) => A$3(a.cell) > 1)) return true; let s = r, i = r; return ( n.forEach((a) => { const c = A$3(a.cell); ((s = Math.max(s, a.col + c - 1)), (i = Math.min(i, a.col))); }), o < r ? r === s : r === i ); } function Ht$2(t, o, r) { const n = E$1(t, r), e = E$1(o, r); return n.col === e.col; } function V$1(t, o, r, n) { const e = []; for (const [i, l] of Object.entries(t.styles || {})) { const a = r[i]; if (!a) throw new Error(`style ${i} not found in styleSchema`); if (a.propSchema === "boolean") l && e.push(o.mark(i)); else if (a.propSchema === "string") l && e.push(o.mark(i, { stringValue: l })); else throw new O(a.propSchema); } return ( !n || !o.nodes[n].spec.code ? t.text .split(/(\n)/g) .filter((i) => i.length > 0) .map((i) => ( i === ` ` ) ? o.nodes.hardBreak.createChecked() : o.text(i, e), ) : t.text.length > 0 ? [o.text(t.text, e)] : [] ); } function mt(t, o, r) { const n = o.marks.link.create({ href: t.href, }); return P$1(t.content, o, r).map((e) => { if (e.type.name === "text") return e.mark([...e.marks, n]); if (e.type.name === "hardBreak") return e; throw new Error("unexpected node type"); }); } function P$1(t, o, r, n) { const e = []; if (typeof t == "string") return (e.push(...V$1({ text: t, styles: {} }, o, r, n)), e); for (const s of t) e.push(...V$1(s, o, r, n)); return e; } function T$1(t, o, r, n = N$1(o)) { const e = []; for (const s of t) typeof s == "string" ? e.push(...P$1(s, o, n, r)) : X$2(s) ? e.push(...mt(s, o, n)) : B$2(s) ? e.push(...P$1([s], o, n, r)) : e.push(nt(s, o, n)); return e; } function kt$1(t, o, r = N$1(o)) { const n = [], e = new Array(t.headerRows ?? 0).fill(true), s = new Array(t.headerCols ?? 0).fill(true), i = t.columnWidths ?? []; for (let l = 0; l < t.rows.length; l++) { const a = t.rows[l], c = [], h = e[l]; for (let p = 0; p < a.cells.length; p++) { const u = a.cells[p], g = s[p], C = void 0; let d = null; const w = E$1( { row: l, col: p, }, { content: t }, ); let y = i[w.col] ? [i[w.col]] : null; if (u) if (typeof u == "string") d = o.text(u); else if (v$2(u)) { u.content && (d = T$1(u.content, o, "tableParagraph", r)); const b = A$3(u); b > 1 && (y = new Array(b).fill(false).map((k, W) => i[w.col + W] ?? void 0)); } else d = T$1(u, o, "tableParagraph", r); const m = o.nodes[g || h ? "tableHeader" : "tableCell"].createChecked( { ...(v$2(u) ? u.props : {}), colwidth: y, }, o.nodes.tableParagraph.createChecked(C, d), ); c.push(m); } const f = o.nodes.tableRow.createChecked({}, c); n.push(f); } return n; } function nt(t, o, r) { let n, e = t.type; if ((e === void 0 && (e = "paragraph"), !o.nodes[e])) throw new Error(`node type ${e} not found in schema`); if (!t.content) n = o.nodes[e].createChecked(t.props); else if (typeof t.content == "string") { const s = T$1([t.content], o, e, r); n = o.nodes[e].createChecked(t.props, s); } else if (Array.isArray(t.content)) { const s = T$1(t.content, o, e, r); n = o.nodes[e].createChecked(t.props, s); } else if (t.content.type === "tableContent") { const s = kt$1(t.content, o, r); n = o.nodes[e].createChecked(t.props, s); } else throw new O(t.content.type); return n; } function bt$1(t, o, r = N$1(o)) { let n = t.id; n === void 0 && (n = Q$2.options.generateID()); const e = []; if (t.children) for (const i of t.children) e.push(bt$1(i, o, r)); if ( !t.type || // can happen if block.type is not defined (this should create the default node) o.nodes[t.type].isInGroup("blockContent") ) { const i = nt(t, o, r), l = e.length > 0 ? o.nodes.blockGroup.createChecked({}, e) : void 0; return o.nodes.blockContainer.createChecked( { id: n, ...t.props, }, l ? [i, l] : i, ); } else { if (o.nodes[t.type].isInGroup("bnBlock")) return o.nodes[t.type].createChecked( { id: n, ...t.props, }, e, ); throw new Error(`block type ${t.type} doesn't match blockContent or bnBlock group`); } } //#region src/tablemap.ts let readFromCache; let addToCache; if (typeof WeakMap != "undefined") { let cache = /* @__PURE__ */ new WeakMap(); readFromCache = (key) => cache.get(key); addToCache = (key, value) => { cache.set(key, value); return value; }; } else { const cache = []; const cacheSize = 10; let cachePos = 0; readFromCache = (key) => { for (let i = 0; i < cache.length; i += 2) if (cache[i] == key) return cache[i + 1]; }; addToCache = (key, value) => { if (cachePos == cacheSize) cachePos = 0; cache[cachePos++] = key; return (cache[cachePos++] = value); }; } /** * A table map describes the structure of a given table. To avoid * recomputing them all the time, they are cached per table node. To * be able to do that, positions saved in the map are relative to the * start of the table, rather than the start of the document. * * @public */ var TableMap = class { constructor(width, height, map, problems) { this.width = width; this.height = height; this.map = map; this.problems = problems; } findCell(pos) { for (let i = 0; i < this.map.length; i++) { const curPos = this.map[i]; if (curPos != pos) continue; const left = i % this.width; const top = (i / this.width) | 0; let right = left + 1; let bottom = top + 1; for (let j = 1; right < this.width && this.map[i + j] == curPos; j++) right++; for (let j = 1; bottom < this.height && this.map[i + this.width * j] == curPos; j++) bottom++; return { left, top, right, bottom, }; } throw new RangeError(`No cell with offset ${pos} found`); } colCount(pos) { for (let i = 0; i < this.map.length; i++) if (this.map[i] == pos) return i % this.width; throw new RangeError(`No cell with offset ${pos} found`); } nextCell(pos, axis, dir) { const { left, right, top, bottom } = this.findCell(pos); if (axis == "horiz") { if (dir < 0 ? left == 0 : right == this.width) return null; return this.map[top * this.width + (dir < 0 ? left - 1 : right)]; } else { if (dir < 0 ? top == 0 : bottom == this.height) return null; return this.map[left + this.width * (dir < 0 ? top - 1 : bottom)]; } } rectBetween(a, b) { const { left: leftA, right: rightA, top: topA, bottom: bottomA } = this.findCell(a); const { left: leftB, right: rightB, top: topB, bottom: bottomB } = this.findCell(b); return { left: Math.min(leftA, leftB), top: Math.min(topA, topB), right: Math.max(rightA, rightB), bottom: Math.max(bottomA, bottomB), }; } cellsInRect(rect) { const result = []; const seen = {}; for (let row = rect.top; row < rect.bottom; row++) for (let col = rect.left; col < rect.right; col++) { const index = row * this.width + col; const pos = this.map[index]; if (seen[pos]) continue; seen[pos] = true; if ((col == rect.left && col && this.map[index - 1] == pos) || (row == rect.top && row && this.map[index - this.width] == pos)) continue; result.push(pos); } return result; } positionAt(row, col, table) { for (let i = 0, rowStart = 0; ; i++) { const rowEnd = rowStart + table.child(i).nodeSize; if (i == row) { let index = col + row * this.width; const rowEndIndex = (row + 1) * this.width; while (index < rowEndIndex && this.map[index] < rowStart) index++; return index == rowEndIndex ? rowEnd - 1 : this.map[index]; } rowStart = rowEnd; } } static get(table) { return readFromCache(table) || addToCache(table, computeMap(table)); } }; function computeMap(table) { if (table.type.spec.tableRole != "table") throw new RangeError("Not a table node: " + table.type.name); const width = findWidth(table), height = table.childCount; const map = []; let mapPos = 0; let problems = null; const colWidths = []; for (let i = 0, e = width * height; i < e; i++) map[i] = 0; for (let row = 0, pos = 0; row < height; row++) { const rowNode = table.child(row); pos++; for (let i = 0; ; i++) { while (mapPos < map.length && map[mapPos] != 0) mapPos++; if (i == rowNode.childCount) break; const cellNode = rowNode.child(i); const { colspan, rowspan, colwidth } = cellNode.attrs; for (let h = 0; h < rowspan; h++) { if (h + row >= height) { (problems || (problems = [])).push({ type: "overlong_rowspan", pos, n: rowspan - h, }); break; } const start = mapPos + h * width; for (let w = 0; w < colspan; w++) { if (map[start + w] == 0) map[start + w] = pos; else (problems || (problems = [])).push({ type: "collision", row, pos, n: colspan - w, }); const colW = colwidth && colwidth[w]; if (colW) { const widthIndex = ((start + w) % width) * 2, prev = colWidths[widthIndex]; if (prev == null || (prev != colW && colWidths[widthIndex + 1] == 1)) { colWidths[widthIndex] = colW; colWidths[widthIndex + 1] = 1; } else if (prev == colW) colWidths[widthIndex + 1]++; } } } mapPos += colspan; pos += cellNode.nodeSize; } const expectedPos = (row + 1) * width; let missing = 0; while (mapPos < expectedPos) if (map[mapPos++] == 0) missing++; if (missing) (problems || (problems = [])).push({ type: "missing", row, n: missing, }); pos++; } if (width === 0 || height === 0) (problems || (problems = [])).push({ type: "zero_sized" }); const tableMap = new TableMap(width, height, map, problems); let badWidths = false; for (let i = 0; !badWidths && i < colWidths.length; i += 2) if (colWidths[i] != null && colWidths[i + 1] < height) badWidths = true; if (badWidths) findBadColWidths(tableMap, colWidths, table); return tableMap; } function findWidth(table) { let width = -1; let hasRowSpan = false; for (let row = 0; row < table.childCount; row++) { const rowNode = table.child(row); let rowWidth = 0; if (hasRowSpan) for (let j = 0; j < row; j++) { const prevRow = table.child(j); for (let i = 0; i < prevRow.childCount; i++) { const cell = prevRow.child(i); if (j + cell.attrs.rowspan > row) rowWidth += cell.attrs.colspan; } } for (let i = 0; i < rowNode.childCount; i++) { const cell = rowNode.child(i); rowWidth += cell.attrs.colspan; if (cell.attrs.rowspan > 1) hasRowSpan = true; } if (width == -1) width = rowWidth; else if (width != rowWidth) width = Math.max(width, rowWidth); } return width; } function findBadColWidths(map, colWidths, table) { if (!map.problems) map.problems = []; const seen = {}; for (let i = 0; i < map.map.length; i++) { const pos = map.map[i]; if (seen[pos]) continue; seen[pos] = true; const node = table.nodeAt(pos); if (!node) throw new RangeError(`No cell with offset ${pos} found`); let updated = null; const attrs = node.attrs; for (let j = 0; j < attrs.colspan; j++) { const colWidth = colWidths[((i + j) % map.width) * 2]; if (colWidth != null && (!attrs.colwidth || attrs.colwidth[j] != colWidth)) (updated || (updated = freshColWidth(attrs)))[j] = colWidth; } if (updated) map.problems.unshift({ type: "colwidth mismatch", pos, colwidth: updated, }); } } function freshColWidth(attrs) { if (attrs.colwidth) return attrs.colwidth.slice(); const result = []; for (let i = 0; i < attrs.colspan; i++) result.push(0); return result; } /** * @public */ function tableNodeTypes(schema) { let result = schema.cached.tableNodeTypes; if (!result) { result = schema.cached.tableNodeTypes = {}; for (const name in schema.nodes) { const type = schema.nodes[name], role = type.spec.tableRole; if (role) result[role] = type; } } return result; } //#endregion //#region src/util.ts /** * @public */ const tableEditingKey = new PluginKey("selectingCells"); /** * @public */ function cellAround($pos) { for (let d = $pos.depth - 1; d > 0; d--) if ($pos.node(d).type.spec.tableRole == "row") return $pos.node(0).resolve($pos.before(d + 1)); return null; } function cellWrapping($pos) { for (let d = $pos.depth; d > 0; d--) { const role = $pos.node(d).type.spec.tableRole; if (role === "cell" || role === "header_cell") return $pos.node(d); } return null; } /** * @public */ function isInTable(state) { const $head = state.selection.$head; for (let d = $head.depth; d > 0; d--) if ($head.node(d).type.spec.tableRole == "row") return true; return false; } /** * @internal */ function selectionCell(state) { const sel = state.selection; if ("$anchorCell" in sel && sel.$anchorCell) return sel.$anchorCell.pos > sel.$headCell.pos ? sel.$anchorCell : sel.$headCell; else if ("node" in sel && sel.node && sel.node.type.spec.tableRole == "cell") return sel.$anchor; const $cell = cellAround(sel.$head) || cellNear(sel.$head); if ($cell) return $cell; throw new RangeError(`No cell found around position ${sel.head}`); } /** * @public */ function cellNear($pos) { for (let after = $pos.nodeAfter, pos = $pos.pos; after; after = after.firstChild, pos++) { const role = after.type.spec.tableRole; if (role == "cell" || role == "header_cell") return $pos.doc.resolve(pos); } for (let before = $pos.nodeBefore, pos = $pos.pos; before; before = before.lastChild, pos--) { const role = before.type.spec.tableRole; if (role == "cell" || role == "header_cell") return $pos.doc.resolve(pos - before.nodeSize); } } /** * @public */ function pointsAtCell($pos) { return $pos.parent.type.spec.tableRole == "row" && !!$pos.nodeAfter; } /** * @public */ function moveCellForward($pos) { return $pos.node(0).resolve($pos.pos + $pos.nodeAfter.nodeSize); } /** * @internal */ function inSameTable($cellA, $cellB) { return $cellA.depth == $cellB.depth && $cellA.pos >= $cellB.start(-1) && $cellA.pos <= $cellB.end(-1); } /** * @public */ function nextCell($pos, axis, dir) { const table = $pos.node(-1); const map = TableMap.get(table); const tableStart = $pos.start(-1); const moved = map.nextCell($pos.pos - tableStart, axis, dir); return moved == null ? null : $pos.node(0).resolve(tableStart + moved); } /** * @public */ function removeColSpan(attrs, pos, n = 1) { const result = { ...attrs, colspan: attrs.colspan - n, }; if (result.colwidth) { result.colwidth = result.colwidth.slice(); result.colwidth.splice(pos, n); if (!result.colwidth.some((w) => w > 0)) result.colwidth = null; } return result; } /** * @public */ function addColSpan(attrs, pos, n = 1) { const result = { ...attrs, colspan: attrs.colspan + n, }; if (result.colwidth) { result.colwidth = result.colwidth.slice(); for (let i = 0; i < n; i++) result.colwidth.splice(pos, 0, 0); } return result; } /** * @public */ function columnIsHeader(map, table, col) { const headerCell = tableNodeTypes(table.type.schema).header_cell; for (let row = 0; row < map.height; row++) if (table.nodeAt(map.map[col + row * map.width]).type != headerCell) return false; return true; } //#endregion //#region src/cellselection.ts /** * A [`Selection`](http://prosemirror.net/docs/ref/#state.Selection) * subclass that represents a cell selection spanning part of a table. * With the plugin enabled, these will be created when the user * selects across cells, and will be drawn by giving selected cells a * `selectedCell` CSS class. * * @public */ var CellSelection = class CellSelection extends Selection { constructor($anchorCell, $headCell = $anchorCell) { const table = $anchorCell.node(-1); const map = TableMap.get(table); const tableStart = $anchorCell.start(-1); const rect = map.rectBetween($anchorCell.pos - tableStart, $headCell.pos - tableStart); const doc = $anchorCell.node(0); const cells = map.cellsInRect(rect).filter((p) => p != $headCell.pos - tableStart); cells.unshift($headCell.pos - tableStart); const ranges = cells.map((pos) => { const cell = table.nodeAt(pos); if (!cell) throw new RangeError(`No cell with offset ${pos} found`); const from = tableStart + pos + 1; return new SelectionRange(doc.resolve(from), doc.resolve(from + cell.content.size)); }); super(ranges[0].$from, ranges[0].$to, ranges); this.$anchorCell = $anchorCell; this.$headCell = $headCell; } map(doc, mapping) { const $anchorCell = doc.resolve(mapping.map(this.$anchorCell.pos)); const $headCell = doc.resolve(mapping.map(this.$headCell.pos)); if (pointsAtCell($anchorCell) && pointsAtCell($headCell) && inSameTable($anchorCell, $headCell)) { const tableChanged = this.$anchorCell.node(-1) != $anchorCell.node(-1); if (tableChanged && this.isRowSelection()) return CellSelection.rowSelection($anchorCell, $headCell); else if (tableChanged && this.isColSelection()) return CellSelection.colSelection($anchorCell, $headCell); else return new CellSelection($anchorCell, $headCell); } return TextSelection.between($anchorCell, $headCell); } content() { const table = this.$anchorCell.node(-1); const map = TableMap.get(table); const tableStart = this.$anchorCell.start(-1); const rect = map.rectBetween(this.$anchorCell.pos - tableStart, this.$headCell.pos - tableStart); const seen = {}; const rows = []; for (let row = rect.top; row < rect.bottom; row++) { const rowContent = []; for (let index = row * map.width + rect.left, col = rect.left; col < rect.right; col++, index++) { const pos = map.map[index]; if (seen[pos]) continue; seen[pos] = true; const cellRect = map.findCell(pos); let cell = table.nodeAt(pos); if (!cell) throw new RangeError(`No cell with offset ${pos} found`); const extraLeft = rect.left - cellRect.left; const extraRight = cellRect.right - rect.right; if (extraLeft > 0 || extraRight > 0) { let attrs = cell.attrs; if (extraLeft > 0) attrs = removeColSpan(attrs, 0, extraLeft); if (extraRight > 0) attrs = removeColSpan(attrs, attrs.colspan - extraRight, extraRight); if (cellRect.left < rect.left) { cell = cell.type.createAndFill(attrs); if (!cell) throw new RangeError(`Could not create cell with attrs ${JSON.stringify(attrs)}`); } else cell = cell.type.create(attrs, cell.content); } if (cellRect.top < rect.top || cellRect.bottom > rect.bottom) { const attrs = { ...cell.attrs, rowspan: Math.min(cellRect.bottom, rect.bottom) - Math.max(cellRect.top, rect.top), }; if (cellRect.top < rect.top) cell = cell.type.createAndFill(attrs); else cell = cell.type.create(attrs, cell.content); } rowContent.push(cell); } rows.push(table.child(row).copy(Fragment.from(rowContent))); } const fragment = this.isColSelection() && this.isRowSelection() ? table : rows; return new Slice(Fragment.from(fragment), 1, 1); } replace(tr, content = Slice.empty) { const mapFrom = tr.steps.length, ranges = this.ranges; for (let i = 0; i < ranges.length; i++) { const { $from, $to } = ranges[i], mapping = tr.mapping.slice(mapFrom); tr.replace(mapping.map($from.pos), mapping.map($to.pos), i ? Slice.empty : content); } const sel = Selection.findFrom(tr.doc.resolve(tr.mapping.slice(mapFrom).map(this.to)), -1); if (sel) tr.setSelection(sel); } replaceWith(tr, node) { this.replace(tr, new Slice(Fragment.from(node), 0, 0)); } forEachCell(f) { const table = this.$anchorCell.node(-1); const map = TableMap.get(table); const tableStart = this.$anchorCell.start(-1); const cells = map.cellsInRect(map.rectBetween(this.$anchorCell.pos - tableStart, this.$headCell.pos - tableStart)); for (let i = 0; i < cells.length; i++) f(table.nodeAt(cells[i]), tableStart + cells[i]); } isColSelection() { const anchorTop = this.$anchorCell.index(-1); const headTop = this.$headCell.index(-1); if (Math.min(anchorTop, headTop) > 0) return false; const anchorBottom = anchorTop + this.$anchorCell.nodeAfter.attrs.rowspan; const headBottom = headTop + this.$headCell.nodeAfter.attrs.rowspan; return Math.max(anchorBottom, headBottom) == this.$headCell.node(-1).childCount; } static colSelection($anchorCell, $headCell = $anchorCell) { const table = $anchorCell.node(-1); const map = TableMap.get(table); const tableStart = $anchorCell.start(-1); const anchorRect = map.findCell($anchorCell.pos - tableStart); const headRect = map.findCell($headCell.pos - tableStart); const doc = $anchorCell.node(0); if (anchorRect.top <= headRect.top) { if (anchorRect.top > 0) $anchorCell = doc.resolve(tableStart + map.map[anchorRect.left]); if (headRect.bottom < map.height) $headCell = doc.resolve(tableStart + map.map[map.width * (map.height - 1) + headRect.right - 1]); } else { if (headRect.top > 0) $headCell = doc.resolve(tableStart + map.map[headRect.left]); if (anchorRect.bottom < map.height) $anchorCell = doc.resolve(tableStart + map.map[map.width * (map.height - 1) + anchorRect.right - 1]); } return new CellSelection($anchorCell, $headCell); } isRowSelection() { const table = this.$anchorCell.node(-1); const map = TableMap.get(table); const tableStart = this.$anchorCell.start(-1); const anchorLeft = map.colCount(this.$anchorCell.pos - tableStart); const headLeft = map.colCount(this.$headCell.pos - tableStart); if (Math.min(anchorLeft, headLeft) > 0) return false; const anchorRight = anchorLeft + this.$anchorCell.nodeAfter.attrs.colspan; const headRight = headLeft + this.$headCell.nodeAfter.attrs.colspan; return Math.max(anchorRight, headRight) == map.width; } eq(other) { return other instanceof CellSelection && other.$anchorCell.pos == this.$anchorCell.pos && other.$headCell.pos == this.$headCell.pos; } static rowSelection($anchorCell, $headCell = $anchorCell) { const table = $anchorCell.node(-1); const map = TableMap.get(table); const tableStart = $anchorCell.start(-1); const anchorRect = map.findCell($anchorCell.pos - tableStart); const headRect = map.findCell($headCell.pos - tableStart); const doc = $anchorCell.node(0); if (anchorRect.left <= headRect.left) { if (anchorRect.left > 0) $anchorCell = doc.resolve(tableStart + map.map[anchorRect.top * map.width]); if (headRect.right < map.width) $headCell = doc.resolve(tableStart + map.map[map.width * (headRect.top + 1) - 1]); } else { if (headRect.left > 0) $headCell = doc.resolve(tableStart + map.map[headRect.top * map.width]); if (anchorRect.right < map.width) $anchorCell = doc.resolve(tableStart + map.map[map.width * (anchorRect.top + 1) - 1]); } return new CellSelection($anchorCell, $headCell); } toJSON() { return { type: "cell", anchor: this.$anchorCell.pos, head: this.$headCell.pos, }; } static fromJSON(doc, json) { return new CellSelection(doc.resolve(json.anchor), doc.resolve(json.head)); } static create(doc, anchorCell, headCell = anchorCell) { return new CellSelection(doc.resolve(anchorCell), doc.resolve(headCell)); } getBookmark() { return new CellBookmark(this.$anchorCell.pos, this.$headCell.pos); } }; CellSelection.prototype.visible = false; Selection.jsonID("cell", CellSelection); /** * @public */ var CellBookmark = class CellBookmark { constructor(anchor, head) { this.anchor = anchor; this.head = head; } map(mapping) { return new CellBookmark(mapping.map(this.anchor), mapping.map(this.head)); } resolve(doc) { const $anchorCell = doc.resolve(this.anchor), $headCell = doc.resolve(this.head); if ( $anchorCell.parent.type.spec.tableRole == "row" && $headCell.parent.type.spec.tableRole == "row" && $anchorCell.index() < $anchorCell.parent.childCount && $headCell.index() < $headCell.parent.childCount && inSameTable($anchorCell, $headCell) ) return new CellSelection($anchorCell, $headCell); else return Selection.near($headCell, 1); } }; function drawCellSelection(state) { if (!(state.selection instanceof CellSelection)) return null; const cells = []; state.selection.forEachCell((node, pos) => { cells.push(Decoration.node(pos, pos + node.nodeSize, { class: "selectedCell" })); }); return DecorationSet.create(state.doc, cells); } function isCellBoundarySelection({ $from, $to }) { if ($from.pos == $to.pos || $from.pos < $to.pos - 6) return false; let afterFrom = $from.pos; let beforeTo = $to.pos; let depth = $from.depth; for (; depth >= 0; depth--, afterFrom++) if ($from.after(depth + 1) < $from.end(depth)) break; for (let d = $to.depth; d >= 0; d--, beforeTo--) if ($to.before(d + 1) > $to.start(d)) break; return afterFrom == beforeTo && /row|table/.test($from.node(depth).type.spec.tableRole); } function isTextSelectionAcrossCells({ $from, $to }) { let fromCellBoundaryNode; let toCellBoundaryNode; for (let i = $from.depth; i > 0; i--) { const node = $from.node(i); if (node.type.spec.tableRole === "cell" || node.type.spec.tableRole === "header_cell") { fromCellBoundaryNode = node; break; } } for (let i = $to.depth; i > 0; i--) { const node = $to.node(i); if (node.type.spec.tableRole === "cell" || node.type.spec.tableRole === "header_cell") { toCellBoundaryNode = node; break; } } return fromCellBoundaryNode !== toCellBoundaryNode && $to.parentOffset === 0; } function normalizeSelection(state, tr, allowTableNodeSelection) { const sel = (tr || state).selection; const doc = (tr || state).doc; let normalize; let role; if (sel instanceof NodeSelection && (role = sel.node.type.spec.tableRole)) { if (role == "cell" || role == "header_cell") normalize = CellSelection.create(doc, sel.from); else if (role == "row") { const $cell = doc.resolve(sel.from + 1); normalize = CellSelection.rowSelection($cell, $cell); } else if (!allowTableNodeSelection) { const map = TableMap.get(sel.node); const start = sel.from + 1; const lastCell = start + map.map[map.width * map.height - 1]; normalize = CellSelection.create(doc, start + 1, lastCell); } } else if (sel instanceof TextSelection && isCellBoundarySelection(sel)) normalize = TextSelection.create(doc, sel.from); else if (sel instanceof TextSelection && isTextSelectionAcrossCells(sel)) normalize = TextSelection.create(doc, sel.$from.start(), sel.$from.end()); if (normalize) (tr || (tr = state.tr)).setSelection(normalize); return tr; } //#endregion //#region src/fixtables.ts /** * @public */ const fixTablesKey = new PluginKey("fix-tables"); /** * Helper for iterating through the nodes in a document that changed * compared to the given previous document. Useful for avoiding * duplicate work on each transaction. * * @public */ function changedDescendants(old, cur, offset, f) { const oldSize = old.childCount, curSize = cur.childCount; outer: for (let i = 0, j = 0; i < curSize; i++) { const child = cur.child(i); for (let scan = j, e = Math.min(oldSize, i + 3); scan < e; scan++) if (old.child(scan) == child) { j = scan + 1; offset += child.nodeSize; continue outer; } f(child, offset); if (j < oldSize && old.child(j).sameMarkup(child)) changedDescendants(old.child(j), child, offset + 1, f); else child.nodesBetween(0, child.content.size, f, offset + 1); offset += child.nodeSize; } } /** * Inspect all tables in the given state's document and return a * transaction that fixes them, if necessary. If `oldState` was * provided, that is assumed to hold a previous, known-good state, * which will be used to avoid re-scanning unchanged parts of the * document. * * @public */ function fixTables(state, oldState) { let tr; const check = (node, pos) => { if (node.type.spec.tableRole == "table") tr = fixTable(state, node, pos, tr); }; if (!oldState) state.doc.descendants(check); else if (oldState.doc != state.doc) changedDescendants(oldState.doc, state.doc, 0, check); return tr; } function fixTable(state, table, tablePos, tr) { const map = TableMap.get(table); if (!map.problems) return tr; if (!tr) tr = state.tr; const mustAdd = []; for (let i = 0; i < map.height; i++) mustAdd.push(0); for (let i = 0; i < map.problems.length; i++) { const prob = map.problems[i]; if (prob.type == "collision") { const cell = table.nodeAt(prob.pos); if (!cell) continue; const attrs = cell.attrs; for (let j = 0; j < attrs.rowspan; j++) mustAdd[prob.row + j] += prob.n; tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, removeColSpan(attrs, attrs.colspan - prob.n, prob.n)); } else if (prob.type == "missing") mustAdd[prob.row] += prob.n; else if (prob.type == "overlong_rowspan") { const cell = table.nodeAt(prob.pos); if (!cell) continue; tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, { ...cell.attrs, rowspan: cell.attrs.rowspan - prob.n, }); } else if (prob.type == "colwidth mismatch") { const cell = table.nodeAt(prob.pos); if (!cell) continue; tr.setNodeMarkup(tr.mapping.map(tablePos + 1 + prob.pos), null, { ...cell.attrs, colwidth: prob.colwidth, }); } else if (prob.type == "zero_sized") { const pos = tr.mapping.map(tablePos); tr.delete(pos, pos + table.nodeSize); } } let first, last; for (let i = 0; i < mustAdd.length; i++) if (mustAdd[i]) { if (first == null) first = i; last = i; } for (let i = 0, pos = tablePos + 1; i < map.height; i++) { const row = table.child(i); const end = pos + row.nodeSize; const add = mustAdd[i]; if (add > 0) { let role = "cell"; if (row.firstChild) role = row.firstChild.type.spec.tableRole; const nodes = []; for (let j = 0; j < add; j++) { const node = tableNodeTypes(state.schema)[role].createAndFill(); if (node) nodes.push(node); } const side = (i == 0 || first == i - 1) && last == i ? pos + 1 : end - 1; tr.insert(tr.mapping.map(side), nodes); } pos = end; } return tr.setMeta(fixTablesKey, { fixTables: true }); } //#endregion //#region src/commands.ts /** * Helper to get the selected rectangle in a table, if any. Adds table * map, table node, and table start offset to the object for * convenience. * * @public */ function selectedRect(state) { const sel = state.selection; const $pos = selectionCell(state); const table = $pos.node(-1); const tableStart = $pos.start(-1); const map = TableMap.get(table); return { ...(sel instanceof CellSelection ? map.rectBetween(sel.$anchorCell.pos - tableStart, sel.$headCell.pos - tableStart) : map.findCell($pos.pos - tableStart)), tableStart, map, table, }; } /** * Add a column at the given position in a table. * * @public */ function addColumn(tr, { map, tableStart, table }, col) { let refColumn = col > 0 ? -1 : 0; if (columnIsHeader(map, table, col + refColumn)) refColumn = col == 0 || col == map.width ? null : 0; for (let row = 0; row < map.height; row++) { const index = row * map.width + col; if (col > 0 && col < map.width && map.map[index - 1] == map.map[index]) { const pos = map.map[index]; const cell = table.nodeAt(pos); tr.setNodeMarkup(tr.mapping.map(tableStart + pos), null, addColSpan(cell.attrs, col - map.colCount(pos))); row += cell.attrs.rowspan - 1; } else { const type = refColumn == null ? tableNodeTypes(table.type.schema).cell : table.nodeAt(map.map[index + refColumn]).type; const pos = map.positionAt(row, col, table); tr.insert(tr.mapping.map(tableStart + pos), type.createAndFill()); } } return tr; } /** * Command to add a column before the column with the selection. * * @public */ function addColumnBefore(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); dispatch(addColumn(state.tr, rect, rect.left)); } return true; } /** * Command to add a column after the column with the selection. * * @public */ function addColumnAfter(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); dispatch(addColumn(state.tr, rect, rect.right)); } return true; } /** * @public */ function removeColumn(tr, { map, table, tableStart }, col) { const mapStart = tr.mapping.maps.length; for (let row = 0; row < map.height; ) { const index = row * map.width + col; const pos = map.map[index]; const cell = table.nodeAt(pos); const attrs = cell.attrs; if ((col > 0 && map.map[index - 1] == pos) || (col < map.width - 1 && map.map[index + 1] == pos)) tr.setNodeMarkup(tr.mapping.slice(mapStart).map(tableStart + pos), null, removeColSpan(attrs, col - map.colCount(pos))); else { const start = tr.mapping.slice(mapStart).map(tableStart + pos); tr.delete(start, start + cell.nodeSize); } row += attrs.rowspan; } } /** * Command function that removes the selected columns from a table. * * @public */ function deleteColumn(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); const tr = state.tr; if (rect.left == 0 && rect.right == rect.map.width) return false; for (let i = rect.right - 1; ; i--) { removeColumn(tr, rect, i); if (i == rect.left) break; const table = rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc; if (!table) throw new RangeError("No table found"); rect.table = table; rect.map = TableMap.get(table); } dispatch(tr); } return true; } /** * @public */ function rowIsHeader(map, table, row) { var _table$nodeAt; const headerCell = tableNodeTypes(table.type.schema).header_cell; for (let col = 0; col < map.width; col++) if (((_table$nodeAt = table.nodeAt(map.map[col + row * map.width])) === null || _table$nodeAt === void 0 ? void 0 : _table$nodeAt.type) != headerCell) return false; return true; } /** * @public */ function addRow(tr, { map, tableStart, table }, row) { let rowPos = tableStart; for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; const cells = []; let refRow = row > 0 ? -1 : 0; if (rowIsHeader(map, table, row + refRow)) refRow = row == 0 || row == map.height ? null : 0; for (let col = 0, index = map.width * row; col < map.width; col++, index++) if (row > 0 && row < map.height && map.map[index] == map.map[index - map.width]) { const pos = map.map[index]; const attrs = table.nodeAt(pos).attrs; tr.setNodeMarkup(tableStart + pos, null, { ...attrs, rowspan: attrs.rowspan + 1, }); col += attrs.colspan - 1; } else { var _table$nodeAt2; const type = refRow == null ? tableNodeTypes(table.type.schema).cell : (_table$nodeAt2 = table.nodeAt(map.map[index + refRow * map.width])) === null || _table$nodeAt2 === void 0 ? void 0 : _table$nodeAt2.type; const node = type === null || type === void 0 ? void 0 : type.createAndFill(); if (node) cells.push(node); } tr.insert(rowPos, tableNodeTypes(table.type.schema).row.create(null, cells)); return tr; } /** * Add a table row before the selection. * * @public */ function addRowBefore(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); dispatch(addRow(state.tr, rect, rect.top)); } return true; } /** * Add a table row after the selection. * * @public */ function addRowAfter(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state); dispatch(addRow(state.tr, rect, rect.bottom)); } return true; } /** * @public */ function removeRow(tr, { map, table, tableStart }, row) { let rowPos = 0; for (let i = 0; i < row; i++) rowPos += table.child(i).nodeSize; const nextRow = rowPos + table.child(row).nodeSize; const mapFrom = tr.mapping.maps.length; tr.delete(rowPos + tableStart, nextRow + tableStart); const seen = /* @__PURE__ */ new Set(); for (let col = 0, index = row * map.width; col < map.width; col++, index++) { const pos = map.map[index]; if (seen.has(pos)) continue; seen.add(pos); if (row > 0 && pos == map.map[index - map.width]) { const attrs = table.nodeAt(pos).attrs; tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + tableStart), null, { ...attrs, rowspan: attrs.rowspan - 1, }); col += attrs.colspan - 1; } else if (row < map.height && pos == map.map[index + map.width]) { const cell = table.nodeAt(pos); const attrs = cell.attrs; const copy = cell.type.create( { ...attrs, rowspan: cell.attrs.rowspan - 1, }, cell.content, ); const newPos = map.positionAt(row + 1, col, table); tr.insert(tr.mapping.slice(mapFrom).map(tableStart + newPos), copy); col += attrs.colspan - 1; } } } /** * Remove the selected rows from a table. * * @public */ function deleteRow(state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const rect = selectedRect(state), tr = state.tr; if (rect.top == 0 && rect.bottom == rect.map.height) return false; for (let i = rect.bottom - 1; ; i--) { removeRow(tr, rect, i); if (i == rect.top) break; const table = rect.tableStart ? tr.doc.nodeAt(rect.tableStart - 1) : tr.doc; if (!table) throw new RangeError("No table found"); rect.table = table; rect.map = TableMap.get(rect.table); } dispatch(tr); } return true; } function isEmpty$1(cell) { const c = cell.content; return c.childCount == 1 && c.child(0).isTextblock && c.child(0).childCount == 0; } function cellsOverlapRectangle({ width, height, map }, rect) { let indexTop = rect.top * width + rect.left, indexLeft = indexTop; let indexBottom = (rect.bottom - 1) * width + rect.left, indexRight = indexTop + (rect.right - rect.left - 1); for (let i = rect.top; i < rect.bottom; i++) { if ((rect.left > 0 && map[indexLeft] == map[indexLeft - 1]) || (rect.right < width && map[indexRight] == map[indexRight + 1])) return true; indexLeft += width; indexRight += width; } for (let i = rect.left; i < rect.right; i++) { if ((rect.top > 0 && map[indexTop] == map[indexTop - width]) || (rect.bottom < height && map[indexBottom] == map[indexBottom + width])) return true; indexTop++; indexBottom++; } return false; } /** * Merge the selected cells into a single cell. Only available when * the selected cells' outline forms a rectangle. * * @public */ function mergeCells(state, dispatch) { const sel = state.selection; if (!(sel instanceof CellSelection) || sel.$anchorCell.pos == sel.$headCell.pos) return false; const rect = selectedRect(state), { map } = rect; if (cellsOverlapRectangle(map, rect)) return false; if (dispatch) { const tr = state.tr; const seen = {}; let content = Fragment.empty; let mergedPos; let mergedCell; for (let row = rect.top; row < rect.bottom; row++) for (let col = rect.left; col < rect.right; col++) { const cellPos = map.map[row * map.width + col]; const cell = rect.table.nodeAt(cellPos); if (seen[cellPos] || !cell) continue; seen[cellPos] = true; if (mergedPos == null) { mergedPos = cellPos; mergedCell = cell; } else { if (!isEmpty$1(cell)) content = content.append(cell.content); const mapped = tr.mapping.map(cellPos + rect.tableStart); tr.delete(mapped, mapped + cell.nodeSize); } } if (mergedPos == null || mergedCell == null) return true; tr.setNodeMarkup(mergedPos + rect.tableStart, null, { ...addColSpan(mergedCell.attrs, mergedCell.attrs.colspan, rect.right - rect.left - mergedCell.attrs.colspan), rowspan: rect.bottom - rect.top, }); if (content.size > 0) { const end = mergedPos + 1 + mergedCell.content.size; const start = isEmpty$1(mergedCell) ? mergedPos + 1 : end; tr.replaceWith(start + rect.tableStart, end + rect.tableStart, content); } tr.setSelection(new CellSelection(tr.doc.resolve(mergedPos + rect.tableStart))); dispatch(tr); } return true; } /** * Split a selected cell, whose rowpan or colspan is greater than one, * into smaller cells. Use the first cell type for the new cells. * * @public */ function splitCell(state, dispatch) { const nodeTypes = tableNodeTypes(state.schema); return splitCellWithType(({ node }) => { return nodeTypes[node.type.spec.tableRole]; })(state, dispatch); } /** * Split a selected cell, whose rowpan or colspan is greater than one, * into smaller cells with the cell type (th, td) returned by getType function. * * @public */ function splitCellWithType(getCellType) { return (state, dispatch) => { const sel = state.selection; let cellNode; let cellPos; if (!(sel instanceof CellSelection)) { var _cellAround; cellNode = cellWrapping(sel.$from); if (!cellNode) return false; cellPos = (_cellAround = cellAround(sel.$from)) === null || _cellAround === void 0 ? void 0 : _cellAround.pos; } else { if (sel.$anchorCell.pos != sel.$headCell.pos) return false; cellNode = sel.$anchorCell.nodeAfter; cellPos = sel.$anchorCell.pos; } if (cellNode == null || cellPos == null) return false; if (cellNode.attrs.colspan == 1 && cellNode.attrs.rowspan == 1) return false; if (dispatch) { let baseAttrs = cellNode.attrs; const attrs = []; const colwidth = baseAttrs.colwidth; if (baseAttrs.rowspan > 1) baseAttrs = { ...baseAttrs, rowspan: 1, }; if (baseAttrs.colspan > 1) baseAttrs = { ...baseAttrs, colspan: 1, }; const rect = selectedRect(state), tr = state.tr; for (let i = 0; i < rect.right - rect.left; i++) attrs.push( colwidth ? { ...baseAttrs, colwidth: colwidth && colwidth[i] ? [colwidth[i]] : null, } : baseAttrs, ); let lastCell; for (let row = rect.top; row < rect.bottom; row++) { let pos = rect.map.positionAt(row, rect.left, rect.table); if (row == rect.top) pos += cellNode.nodeSize; for (let col = rect.left, i = 0; col < rect.right; col++, i++) { if (col == rect.left && row == rect.top) continue; tr.insert( (lastCell = tr.mapping.map(pos + rect.tableStart, 1)), getCellType({ node: cellNode, row, col, }).createAndFill(attrs[i]), ); } } tr.setNodeMarkup( cellPos, getCellType({ node: cellNode, row: rect.top, col: rect.left, }), attrs[0], ); if (sel instanceof CellSelection) tr.setSelection(new CellSelection(tr.doc.resolve(sel.$anchorCell.pos), lastCell ? tr.doc.resolve(lastCell) : void 0)); dispatch(tr); } return true; }; } function deprecated_toggleHeader(type) { return function (state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const types = tableNodeTypes(state.schema); const rect = selectedRect(state), tr = state.tr; const cells = rect.map.cellsInRect( type == "column" ? { left: rect.left, top: 0, right: rect.right, bottom: rect.map.height, } : type == "row" ? { left: 0, top: rect.top, right: rect.map.width, bottom: rect.bottom, } : rect, ); const nodes = cells.map((pos) => rect.table.nodeAt(pos)); for (let i = 0; i < cells.length; i++) if (nodes[i].type == types.header_cell) tr.setNodeMarkup(rect.tableStart + cells[i], types.cell, nodes[i].attrs); if (tr.steps.length === 0) for (let i = 0; i < cells.length; i++) tr.setNodeMarkup(rect.tableStart + cells[i], types.header_cell, nodes[i].attrs); dispatch(tr); } return true; }; } function isHeaderEnabledByType(type, rect, types) { const cellPositions = rect.map.cellsInRect({ left: 0, top: 0, right: type == "row" ? rect.map.width : 1, bottom: type == "column" ? rect.map.height : 1, }); for (let i = 0; i < cellPositions.length; i++) { const cell = rect.table.nodeAt(cellPositions[i]); if (cell && cell.type !== types.header_cell) return false; } return true; } /** * Toggles between row/column header and normal cells (Only applies to first row/column). * For deprecated behavior pass `useDeprecatedLogic` in options with true. * * @public */ function toggleHeader(type, options) { options = options || { useDeprecatedLogic: false }; if (options.useDeprecatedLogic) return deprecated_toggleHeader(type); return function (state, dispatch) { if (!isInTable(state)) return false; if (dispatch) { const types = tableNodeTypes(state.schema); const rect = selectedRect(state), tr = state.tr; const isHeaderRowEnabled = isHeaderEnabledByType("row", rect, types); const isHeaderColumnEnabled = isHeaderEnabledByType("column", rect, types); const selectionStartsAt = ( type === "column" ? isHeaderRowEnabled : type === "row" ? isHeaderColumnEnabled : false ) ? 1 : 0; const cellsRect = type == "column" ? { left: 0, top: selectionStartsAt, right: 1, bottom: rect.map.height, } : type == "row" ? { left: selectionStartsAt, top: 0, right: rect.map.width, bottom: 1, } : rect; const newType = type == "column" ? isHeaderColumnEnabled ? types.cell : types.header_cell : type == "row" ? isHeaderRowEnabled ? types.cell : types.header_cell : types.cell; rect.map.cellsInRect(cellsRect).forEach((relativeCellPos) => { const cellPos = relativeCellPos + rect.tableStart; const cell = tr.doc.nodeAt(cellPos); if (cell) tr.setNodeMarkup(cellPos, newType, cell.attrs); }); dispatch(tr); } return true; }; } /** * Toggles whether the selected row contains header cells. * * @public */ toggleHeader("row", { useDeprecatedLogic: true }); /** * Toggles whether the selected column contains header cells. * * @public */ toggleHeader("column", { useDeprecatedLogic: true }); /** * Toggles whether the selected cells are header cells. * * @public */ toggleHeader("cell", { useDeprecatedLogic: true }); function findNextCell($cell, dir) { if (dir < 0) { const before = $cell.nodeBefore; if (before) return $cell.pos - before.nodeSize; for (let row = $cell.index(-1) - 1, rowEnd = $cell.before(); row >= 0; row--) { const rowNode = $cell.node(-1).child(row); const lastChild = rowNode.lastChild; if (lastChild) return rowEnd - 1 - lastChild.nodeSize; rowEnd -= rowNode.nodeSize; } } else { if ($cell.index() < $cell.parent.childCount - 1) return $cell.pos + $cell.nodeAfter.nodeSize; const table = $cell.node(-1); for (let row = $cell.indexAfter(-1), rowStart = $cell.after(); row < table.childCount; row++) { const rowNode = table.child(row); if (rowNode.childCount) return rowStart + 1; rowStart += rowNode.nodeSize; } } return null; } /** * Returns a command for selecting the next (direction=1) or previous * (direction=-1) cell in a table. * * @public */ function goToNextCell(direction) { return function (state, dispatch) { if (!isInTable(state)) return false; const cell = findNextCell(selectionCell(state), direction); if (cell == null) return false; if (dispatch) { const $cell = state.doc.resolve(cell); dispatch(state.tr.setSelection(TextSelection.between($cell, moveCellForward($cell))).scrollIntoView()); } return true; }; } /** * Deletes the content of the selected cells, if they are not empty. * * @public */ function deleteCellSelection(state, dispatch) { const sel = state.selection; if (!(sel instanceof CellSelection)) return false; if (dispatch) { const tr = state.tr; const baseContent = tableNodeTypes(state.schema).cell.createAndFill().content; sel.forEachCell((cell, pos) => { if (!cell.content.eq(baseContent)) tr.replace(tr.mapping.map(pos + 1), tr.mapping.map(pos + cell.nodeSize - 1), new Slice(baseContent, 0, 0)); }); if (tr.docChanged) dispatch(tr); } return true; } //#endregion //#region src/copypaste.ts /** * Get a rectangular area of cells from a slice, or null if the outer * nodes of the slice aren't table cells or rows. * * @internal */ function pastedCells(slice) { if (slice.size === 0) return null; let { content, openStart, openEnd } = slice; while (content.childCount == 1 && ((openStart > 0 && openEnd > 0) || content.child(0).type.spec.tableRole == "table")) { openStart--; openEnd--; content = content.child(0).content; } const first = content.child(0); const role = first.type.spec.tableRole; const schema = first.type.schema, rows = []; if (role == "row") for (let i = 0; i < content.childCount; i++) { let cells = content.child(i).content; const left = i ? 0 : Math.max(0, openStart - 1); const right = i < content.childCount - 1 ? 0 : Math.max(0, openEnd - 1); if (left || right) cells = fitSlice(tableNodeTypes(schema).row, new Slice(cells, left, right)).content; rows.push(cells); } else if (role == "cell" || role == "header_cell") rows.push(openStart || openEnd ? fitSlice(tableNodeTypes(schema).row, new Slice(content, openStart, openEnd)).content : content); else return null; return ensureRectangular(schema, rows); } function ensureRectangular(schema, rows) { const widths = []; for (let i = 0; i < rows.length; i++) { const row = rows[i]; for (let j = row.childCount - 1; j >= 0; j--) { const { rowspan, colspan } = row.child(j).attrs; for (let r = i; r < i + rowspan; r++) widths[r] = (widths[r] || 0) + colspan; } } let width = 0; for (let r = 0; r < widths.length; r++) width = Math.max(width, widths[r]); for (let r = 0; r < widths.length; r++) { if (r >= rows.length) rows.push(Fragment.empty); if (widths[r] < width) { const empty = tableNodeTypes(schema).cell.createAndFill(); const cells = []; for (let i = widths[r]; i < width; i++) cells.push(empty); rows[r] = rows[r].append(Fragment.from(cells)); } } return { height: rows.length, width, rows, }; } function fitSlice(nodeType, slice) { const node = nodeType.createAndFill(); return new Transform(node).replace(0, node.content.size, slice).doc; } /** * Clip or extend (repeat) the given set of cells to cover the given * width and height. Will clip rowspan/colspan cells at the edges when * they stick out. * * @internal */ function clipCells({ width, height, rows }, newWidth, newHeight) { if (width != newWidth) { const added = []; const newRows = []; for (let row = 0; row < rows.length; row++) { const frag = rows[row], cells = []; for (let col = added[row] || 0, i = 0; col < newWidth; i++) { let cell = frag.child(i % frag.childCount); if (col + cell.attrs.colspan > newWidth) cell = cell.type.createChecked(removeColSpan(cell.attrs, cell.attrs.colspan, col + cell.attrs.colspan - newWidth), cell.content); cells.push(cell); col += cell.attrs.colspan; for (let j = 1; j < cell.attrs.rowspan; j++) added[row + j] = (added[row + j] || 0) + cell.attrs.colspan; } newRows.push(Fragment.from(cells)); } rows = newRows; width = newWidth; } if (height != newHeight) { const newRows = []; for (let row = 0, i = 0; row < newHeight; row++, i++) { const cells = [], source = rows[i % height]; for (let j = 0; j < source.childCount; j++) { let cell = source.child(j); if (row + cell.attrs.rowspan > newHeight) cell = cell.type.create( { ...cell.attrs, rowspan: Math.max(1, newHeight - cell.attrs.rowspan), }, cell.content, ); cells.push(cell); } newRows.push(Fragment.from(cells)); } rows = newRows; height = newHeight; } return { width, height, rows, }; } function growTable(tr, map, table, start, width, height, mapFrom) { const schema = tr.doc.type.schema; const types = tableNodeTypes(schema); let empty; let emptyHead; if (width > map.width) for (let row = 0, rowEnd = 0; row < map.height; row++) { const rowNode = table.child(row); rowEnd += rowNode.nodeSize; const cells = []; let add; if (rowNode.lastChild == null || rowNode.lastChild.type == types.cell) add = empty || (empty = types.cell.createAndFill()); else add = emptyHead || (emptyHead = types.header_cell.createAndFill()); for (let i = map.width; i < width; i++) cells.push(add); tr.insert(tr.mapping.slice(mapFrom).map(rowEnd - 1 + start), cells); } if (height > map.height) { const cells = []; for (let i = 0, start$1 = (map.height - 1) * map.width; i < Math.max(map.width, width); i++) { const header = i >= map.width ? false : table.nodeAt(map.map[start$1 + i]).type == types.header_cell; cells.push(header ? emptyHead || (emptyHead = types.header_cell.createAndFill()) : empty || (empty = types.cell.createAndFill())); } const emptyRow = types.row.create(null, Fragment.from(cells)), rows = []; for (let i = map.height; i < height; i++) rows.push(emptyRow); tr.insert(tr.mapping.slice(mapFrom).map(start + table.nodeSize - 2), rows); } return !!(empty || emptyHead); } function isolateHorizontal(tr, map, table, start, left, right, top, mapFrom) { if (top == 0 || top == map.height) return false; let found = false; for (let col = left; col < right; col++) { const index = top * map.width + col, pos = map.map[index]; if (map.map[index - map.width] == pos) { found = true; const cell = table.nodeAt(pos); const { top: cellTop, left: cellLeft } = map.findCell(pos); tr.setNodeMarkup(tr.mapping.slice(mapFrom).map(pos + start), null, { ...cell.attrs, rowspan: top - cellTop, }); tr.insert( tr.mapping.slice(mapFrom).map(map.positionAt(top, cellLeft, table)), cell.type.createAndFill({ ...cell.attrs, rowspan: cellTop + cell.attrs.rowspan - top, }), ); col += cell.attrs.colspan - 1; } } return found; } function isolateVertical(tr, map, table, start, top, bottom, left, mapFrom) { if (left == 0 || left == map.width) return false; let found = false; for (let row = top; row < bottom; row++) { const index = row * map.width + left, pos = map.map[index]; if (map.map[index - 1] == pos) { found = true; const cell = table.nodeAt(pos); const cellLeft = map.colCount(pos); const updatePos = tr.mapping.slice(mapFrom).map(pos + start); tr.setNodeMarkup(updatePos, null, removeColSpan(cell.attrs, left - cellLeft, cell.attrs.colspan - (left - cellLeft))); tr.insert(updatePos + cell.nodeSize, cell.type.createAndFill(removeColSpan(cell.attrs, 0, left - cellLeft))); row += cell.attrs.rowspan - 1; } } return found; } /** * Insert the given set of cells (as returned by `pastedCells`) into a * table, at the position pointed at by rect. * * @internal */ function insertCells(state, dispatch, tableStart, rect, cells) { let table = tableStart ? state.doc.nodeAt(tableStart - 1) : state.doc; if (!table) throw new Error("No table found"); let map = TableMap.get(table); const { top, left } = rect; const right = left + cells.width, bottom = top + cells.height; const tr = state.tr; let mapFrom = 0; function recomp() { table = tableStart ? tr.doc.nodeAt(tableStart - 1) : tr.doc; if (!table) throw new Error("No table found"); map = TableMap.get(table); mapFrom = tr.mapping.maps.length; } if (growTable(tr, map, table, tableStart, right, bottom, mapFrom)) recomp(); if (isolateHorizontal(tr, map, table, tableStart, left, right, top, mapFrom)) recomp(); if (isolateHorizontal(tr, map, table, tableStart, left, right, bottom, mapFrom)) recomp(); if (isolateVertical(tr, map, table, tableStart, top, bottom, left, mapFrom)) recomp(); if (isolateVertical(tr, map, table, tableStart, top, bottom, right, mapFrom)) recomp(); for (let row = top; row < bottom; row++) { const from = map.positionAt(row, left, table), to = map.positionAt(row, right, table); tr.replace(tr.mapping.slice(mapFrom).map(from + tableStart), tr.mapping.slice(mapFrom).map(to + tableStart), new Slice(cells.rows[row - top], 0, 0)); } recomp(); tr.setSelection(new CellSelection(tr.doc.resolve(tableStart + map.positionAt(top, left, table)), tr.doc.resolve(tableStart + map.positionAt(bottom - 1, right - 1, table)))); dispatch(tr); } //#endregion //#region src/input.ts const handleKeyDown = keydownHandler({ "ArrowLeft": arrow$4("horiz", -1), "ArrowRight": arrow$4("horiz", 1), "ArrowUp": arrow$4("vert", -1), "ArrowDown": arrow$4("vert", 1), "Shift-ArrowLeft": shiftArrow("horiz", -1), "Shift-ArrowRight": shiftArrow("horiz", 1), "Shift-ArrowUp": shiftArrow("vert", -1), "Shift-ArrowDown": shiftArrow("vert", 1), "Backspace": deleteCellSelection, "Mod-Backspace": deleteCellSelection, "Delete": deleteCellSelection, "Mod-Delete": deleteCellSelection, }); function maybeSetSelection(state, dispatch, selection) { if (selection.eq(state.selection)) return false; if (dispatch) dispatch(state.tr.setSelection(selection).scrollIntoView()); return true; } /** * @internal */ function arrow$4(axis, dir) { return (state, dispatch, view) => { if (!view) return false; const sel = state.selection; if (sel instanceof CellSelection) return maybeSetSelection(state, dispatch, Selection.near(sel.$headCell, dir)); if (axis != "horiz" && !sel.empty) return false; const end = atEndOfCell(view, axis, dir); if (end == null) return false; if (axis == "horiz") return maybeSetSelection(state, dispatch, Selection.near(state.doc.resolve(sel.head + dir), dir)); else { const $cell = state.doc.resolve(end); const $next = nextCell($cell, axis, dir); let newSel; if ($next) newSel = Selection.near($next, 1); else if (dir < 0) newSel = Selection.near(state.doc.resolve($cell.before(-1)), -1); else newSel = Selection.near(state.doc.resolve($cell.after(-1)), 1); return maybeSetSelection(state, dispatch, newSel); } }; } function shiftArrow(axis, dir) { return (state, dispatch, view) => { if (!view) return false; const sel = state.selection; let cellSel; if (sel instanceof CellSelection) cellSel = sel; else { const end = atEndOfCell(view, axis, dir); if (end == null) return false; cellSel = new CellSelection(state.doc.resolve(end)); } const $head = nextCell(cellSel.$headCell, axis, dir); if (!$head) return false; return maybeSetSelection(state, dispatch, new CellSelection(cellSel.$anchorCell, $head)); }; } function handleTripleClick(view, pos) { const doc = view.state.doc, $cell = cellAround(doc.resolve(pos)); if (!$cell) return false; view.dispatch(view.state.tr.setSelection(new CellSelection($cell))); return true; } /** * @public */ function handlePaste(view, _, slice) { if (!isInTable(view.state)) return false; let cells = pastedCells(slice); const sel = view.state.selection; if (sel instanceof CellSelection) { if (!cells) cells = { width: 1, height: 1, rows: [Fragment.from(fitSlice(tableNodeTypes(view.state.schema).cell, slice))], }; const table = sel.$anchorCell.node(-1); const start = sel.$anchorCell.start(-1); const rect = TableMap.get(table).rectBetween(sel.$anchorCell.pos - start, sel.$headCell.pos - start); cells = clipCells(cells, rect.right - rect.left, rect.bottom - rect.top); insertCells(view.state, view.dispatch, start, rect, cells); return true; } else if (cells) { const $cell = selectionCell(view.state); const start = $cell.start(-1); insertCells(view.state, view.dispatch, start, TableMap.get($cell.node(-1)).findCell($cell.pos - start), cells); return true; } else return false; } function handleMouseDown$1(view, startEvent) { var _cellUnderMouse; if (startEvent.button != 0) return; if (startEvent.ctrlKey || startEvent.metaKey) return; const startDOMCell = domInCell(view, startEvent.target); let $anchor; if (startEvent.shiftKey && view.state.selection instanceof CellSelection) { setCellSelection(view.state.selection.$anchorCell, startEvent); startEvent.preventDefault(); } else if ( startEvent.shiftKey && startDOMCell && ($anchor = cellAround(view.state.selection.$anchor)) != null && ((_cellUnderMouse = cellUnderMouse(view, startEvent)) === null || _cellUnderMouse === void 0 ? void 0 : _cellUnderMouse.pos) != $anchor.pos ) { setCellSelection($anchor, startEvent); startEvent.preventDefault(); } else if (!startDOMCell) return; function setCellSelection($anchor$1, event) { let $head = cellUnderMouse(view, event); const starting = tableEditingKey.getState(view.state) == null; if (!$head || !inSameTable($anchor$1, $head)) if (starting) $head = $anchor$1; else return; const selection = new CellSelection($anchor$1, $head); if (starting || !view.state.selection.eq(selection)) { const tr = view.state.tr.setSelection(selection); if (starting) tr.setMeta(tableEditingKey, $anchor$1.pos); view.dispatch(tr); } } function stop() { view.root.removeEventListener("mouseup", stop); view.root.removeEventListener("dragstart", stop); view.root.removeEventListener("mousemove", move); if (tableEditingKey.getState(view.state) != null) view.dispatch(view.state.tr.setMeta(tableEditingKey, -1)); } function move(_event) { const event = _event; const anchor = tableEditingKey.getState(view.state); let $anchor$1; if (anchor != null) $anchor$1 = view.state.doc.resolve(anchor); else if (domInCell(view, event.target) != startDOMCell) { $anchor$1 = cellUnderMouse(view, startEvent); if (!$anchor$1) return stop(); } if ($anchor$1) setCellSelection($anchor$1, event); } view.root.addEventListener("mouseup", stop); view.root.addEventListener("dragstart", stop); view.root.addEventListener("mousemove", move); } function atEndOfCell(view, axis, dir) { if (!(view.state.selection instanceof TextSelection)) return null; const { $head } = view.state.selection; for (let d = $head.depth - 1; d >= 0; d--) { const parent = $head.node(d); if ((dir < 0 ? $head.index(d) : $head.indexAfter(d)) != (dir < 0 ? 0 : parent.childCount)) return null; if (parent.type.spec.tableRole == "cell" || parent.type.spec.tableRole == "header_cell") { const cellPos = $head.before(d); const dirStr = axis == "vert" ? dir > 0 ? "down" : "up" : dir > 0 ? "right" : "left"; return view.endOfTextblock(dirStr) ? cellPos : null; } } return null; } function domInCell(view, dom) { for (; dom && dom != view.dom; dom = dom.parentNode) if (dom.nodeName == "TD" || dom.nodeName == "TH") return dom; return null; } function cellUnderMouse(view, event) { const mousePos = view.posAtCoords({ left: event.clientX, top: event.clientY, }); if (!mousePos) return null; let { inside, pos } = mousePos; return (inside >= 0 && cellAround(view.state.doc.resolve(inside))) || cellAround(view.state.doc.resolve(pos)); } //#endregion //#region src/tableview.ts /** * @public */ var TableView = class { constructor(node, defaultCellMinWidth) { this.node = node; this.defaultCellMinWidth = defaultCellMinWidth; this.dom = document.createElement("div"); this.dom.className = "tableWrapper"; this.table = this.dom.appendChild(document.createElement("table")); this.table.style.setProperty("--default-cell-min-width", `${defaultCellMinWidth}px`); this.colgroup = this.table.appendChild(document.createElement("colgroup")); updateColumnsOnResize(node, this.colgroup, this.table, defaultCellMinWidth); this.contentDOM = this.table.appendChild(document.createElement("tbody")); } update(node) { if (node.type != this.node.type) return false; this.node = node; updateColumnsOnResize(node, this.colgroup, this.table, this.defaultCellMinWidth); return true; } ignoreMutation(record) { return record.type == "attributes" && (record.target == this.table || this.colgroup.contains(record.target)); } }; /** * @public */ function updateColumnsOnResize(node, colgroup, table, defaultCellMinWidth, overrideCol, overrideValue) { let totalWidth = 0; let fixedWidth = true; let nextDOM = colgroup.firstChild; const row = node.firstChild; if (!row) return; for (let i = 0, col = 0; i < row.childCount; i++) { const { colspan, colwidth } = row.child(i).attrs; for (let j = 0; j < colspan; j++, col++) { const hasWidth = overrideCol == col ? overrideValue : colwidth && colwidth[j]; const cssWidth = hasWidth ? hasWidth + "px" : ""; totalWidth += hasWidth || defaultCellMinWidth; if (!hasWidth) fixedWidth = false; if (!nextDOM) { const col$1 = document.createElement("col"); col$1.style.width = cssWidth; colgroup.appendChild(col$1); } else { if (nextDOM.style.width != cssWidth) nextDOM.style.width = cssWidth; nextDOM = nextDOM.nextSibling; } } } while (nextDOM) { var _nextDOM$parentNode; const after = nextDOM.nextSibling; (_nextDOM$parentNode = nextDOM.parentNode) === null || _nextDOM$parentNode === void 0 || _nextDOM$parentNode.removeChild(nextDOM); nextDOM = after; } if (fixedWidth) { table.style.width = totalWidth + "px"; table.style.minWidth = ""; } else { table.style.width = ""; table.style.minWidth = totalWidth + "px"; } } //#endregion //#region src/columnresizing.ts /** * @public */ const columnResizingPluginKey = new PluginKey("tableColumnResizing"); /** * @public */ function columnResizing({ handleWidth = 5, cellMinWidth = 25, defaultCellMinWidth = 100, View = TableView, lastColumnResizable = true } = {}) { const plugin = new Plugin({ key: columnResizingPluginKey, state: { init(_, state) { var _plugin$spec; const nodeViews = (_plugin$spec = plugin.spec) === null || _plugin$spec === void 0 || (_plugin$spec = _plugin$spec.props) === null || _plugin$spec === void 0 ? void 0 : _plugin$spec.nodeViews; const tableName = tableNodeTypes(state.schema).table.name; if (View && nodeViews) nodeViews[tableName] = (node, view) => { return new View(node, defaultCellMinWidth, view); }; return new ResizeState(-1, false); }, apply(tr, prev) { return prev.apply(tr); }, }, props: { attributes: (state) => { const pluginState = columnResizingPluginKey.getState(state); return pluginState && pluginState.activeHandle > -1 ? { class: "resize-cursor" } : {}; }, handleDOMEvents: { mousemove: (view, event) => { handleMouseMove(view, event, handleWidth, lastColumnResizable); }, mouseleave: (view) => { handleMouseLeave(view); }, mousedown: (view, event) => { handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth); }, }, decorations: (state) => { const pluginState = columnResizingPluginKey.getState(state); if (pluginState && pluginState.activeHandle > -1) return handleDecorations(state, pluginState.activeHandle); }, nodeViews: {}, }, }); return plugin; } /** * @public */ var ResizeState = class ResizeState { constructor(activeHandle, dragging) { this.activeHandle = activeHandle; this.dragging = dragging; } apply(tr) { const state = this; const action = tr.getMeta(columnResizingPluginKey); if (action && action.setHandle != null) return new ResizeState(action.setHandle, false); if (action && action.setDragging !== void 0) return new ResizeState(state.activeHandle, action.setDragging); if (state.activeHandle > -1 && tr.docChanged) { let handle = tr.mapping.map(state.activeHandle, -1); if (!pointsAtCell(tr.doc.resolve(handle))) handle = -1; return new ResizeState(handle, state.dragging); } return state; } }; function handleMouseMove(view, event, handleWidth, lastColumnResizable) { if (!view.editable) return; const pluginState = columnResizingPluginKey.getState(view.state); if (!pluginState) return; if (!pluginState.dragging) { const target = domCellAround(event.target); let cell = -1; if (target) { const { left, right } = target.getBoundingClientRect(); if (event.clientX - left <= handleWidth) cell = edgeCell(view, event, "left", handleWidth); else if (right - event.clientX <= handleWidth) cell = edgeCell(view, event, "right", handleWidth); } if (cell != pluginState.activeHandle) { if (!lastColumnResizable && cell !== -1) { const $cell = view.state.doc.resolve(cell); const table = $cell.node(-1); const map = TableMap.get(table); const tableStart = $cell.start(-1); if (map.colCount($cell.pos - tableStart) + $cell.nodeAfter.attrs.colspan - 1 == map.width - 1) return; } updateHandle(view, cell); } } } function handleMouseLeave(view) { if (!view.editable) return; const pluginState = columnResizingPluginKey.getState(view.state); if (pluginState && pluginState.activeHandle > -1 && !pluginState.dragging) updateHandle(view, -1); } function handleMouseDown(view, event, cellMinWidth, defaultCellMinWidth) { var _view$dom$ownerDocume; if (!view.editable) return false; const win = (_view$dom$ownerDocume = view.dom.ownerDocument.defaultView) !== null && _view$dom$ownerDocume !== void 0 ? _view$dom$ownerDocume : window; const pluginState = columnResizingPluginKey.getState(view.state); if (!pluginState || pluginState.activeHandle == -1 || pluginState.dragging) return false; const cell = view.state.doc.nodeAt(pluginState.activeHandle); const width = currentColWidth(view, pluginState.activeHandle, cell.attrs); view.dispatch( view.state.tr.setMeta(columnResizingPluginKey, { setDragging: { startX: event.clientX, startWidth: width, }, }), ); function finish(event$1) { win.removeEventListener("mouseup", finish); win.removeEventListener("mousemove", move); const pluginState$1 = columnResizingPluginKey.getState(view.state); if (pluginState$1 === null || pluginState$1 === void 0 ? void 0 : pluginState$1.dragging) { updateColumnWidth(view, pluginState$1.activeHandle, draggedWidth(pluginState$1.dragging, event$1, cellMinWidth)); view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setDragging: null })); } } function move(event$1) { if (!event$1.which) return finish(event$1); const pluginState$1 = columnResizingPluginKey.getState(view.state); if (!pluginState$1) return; if (pluginState$1.dragging) { const dragged = draggedWidth(pluginState$1.dragging, event$1, cellMinWidth); displayColumnWidth(view, pluginState$1.activeHandle, dragged, defaultCellMinWidth); } } displayColumnWidth(view, pluginState.activeHandle, width, defaultCellMinWidth); win.addEventListener("mouseup", finish); win.addEventListener("mousemove", move); event.preventDefault(); return true; } function currentColWidth(view, cellPos, { colspan, colwidth }) { const width = colwidth && colwidth[colwidth.length - 1]; if (width) return width; const dom = view.domAtPos(cellPos); let domWidth = dom.node.childNodes[dom.offset].offsetWidth, parts = colspan; if (colwidth) { for (let i = 0; i < colspan; i++) if (colwidth[i]) { domWidth -= colwidth[i]; parts--; } } return domWidth / parts; } function domCellAround(target) { while (target && target.nodeName != "TD" && target.nodeName != "TH") target = target.classList && target.classList.contains("ProseMirror") ? null : target.parentNode; return target; } function edgeCell(view, event, side, handleWidth) { const offset = side == "right" ? -handleWidth : handleWidth; const found = view.posAtCoords({ left: event.clientX + offset, top: event.clientY, }); if (!found) return -1; const { pos } = found; const $cell = cellAround(view.state.doc.resolve(pos)); if (!$cell) return -1; if (side == "right") return $cell.pos; const map = TableMap.get($cell.node(-1)), start = $cell.start(-1); const index = map.map.indexOf($cell.pos - start); return index % map.width == 0 ? -1 : start + map.map[index - 1]; } function draggedWidth(dragging, event, resizeMinWidth) { const offset = event.clientX - dragging.startX; return Math.max(resizeMinWidth, dragging.startWidth + offset); } function updateHandle(view, value) { view.dispatch(view.state.tr.setMeta(columnResizingPluginKey, { setHandle: value })); } function updateColumnWidth(view, cell, width) { const $cell = view.state.doc.resolve(cell); const table = $cell.node(-1), map = TableMap.get(table), start = $cell.start(-1); const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; const tr = view.state.tr; for (let row = 0; row < map.height; row++) { const mapIndex = row * map.width + col; if (row && map.map[mapIndex] == map.map[mapIndex - map.width]) continue; const pos = map.map[mapIndex]; const attrs = table.nodeAt(pos).attrs; const index = attrs.colspan == 1 ? 0 : col - map.colCount(pos); if (attrs.colwidth && attrs.colwidth[index] == width) continue; const colwidth = attrs.colwidth ? attrs.colwidth.slice() : zeroes(attrs.colspan); colwidth[index] = width; tr.setNodeMarkup(start + pos, null, { ...attrs, colwidth, }); } if (tr.docChanged) view.dispatch(tr); } function displayColumnWidth(view, cell, width, defaultCellMinWidth) { const $cell = view.state.doc.resolve(cell); const table = $cell.node(-1), start = $cell.start(-1); const col = TableMap.get(table).colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; let dom = view.domAtPos($cell.start(-1)).node; while (dom && dom.nodeName != "TABLE") dom = dom.parentNode; if (!dom) return; updateColumnsOnResize(table, dom.firstChild, dom, defaultCellMinWidth, col, width); } function zeroes(n) { return Array(n).fill(0); } function handleDecorations(state, cell) { const decorations = []; const $cell = state.doc.resolve(cell); const table = $cell.node(-1); if (!table) return DecorationSet.empty; const map = TableMap.get(table); const start = $cell.start(-1); const col = map.colCount($cell.pos - start) + $cell.nodeAfter.attrs.colspan - 1; for (let row = 0; row < map.height; row++) { const index = col + row * map.width; if ((col == map.width - 1 || map.map[index] != map.map[index + 1]) && (row == 0 || map.map[index] != map.map[index - map.width])) { var _columnResizingPlugin; const cellPos = map.map[index]; const pos = start + cellPos + table.nodeAt(cellPos).nodeSize - 1; const dom = document.createElement("div"); dom.className = "column-resize-handle"; if ((_columnResizingPlugin = columnResizingPluginKey.getState(state)) === null || _columnResizingPlugin === void 0 ? void 0 : _columnResizingPlugin.dragging) decorations.push(Decoration.node(start + cellPos, start + cellPos + table.nodeAt(cellPos).nodeSize, { class: "column-resize-dragging" })); decorations.push(Decoration.widget(pos, dom)); } } return DecorationSet.create(state.doc, decorations); } //#endregion //#region src/index.ts /** * Creates a [plugin](http://prosemirror.net/docs/ref/#state.Plugin) * that, when added to an editor, enables cell-selection, handles * cell-based copy/paste, and makes sure tables stay well-formed (each * row has the same width, and cells don't overlap). * * You should probably put this plugin near the end of your array of * plugins, since it handles mouse and arrow key events in tables * rather broadly, and other plugins, like the gap cursor or the * column-width dragging plugin, might want to get a turn first to * perform more specific behavior. * * @public */ function tableEditing({ allowTableNodeSelection = false } = {}) { return new Plugin({ key: tableEditingKey, state: { init() { return null; }, apply(tr, cur) { const set = tr.getMeta(tableEditingKey); if (set != null) return set == -1 ? null : set; if (cur == null || !tr.docChanged) return cur; const { deleted, pos } = tr.mapping.mapResult(cur); return deleted ? null : pos; }, }, props: { decorations: drawCellSelection, handleDOMEvents: { mousedown: handleMouseDown$1 }, createSelectionBetween(view) { return tableEditingKey.getState(view.state) != null ? view.state.selection : null; }, handleTripleClick, handleKeyDown, handlePaste, }, appendTransaction(_, oldState, state) { return normalizeSelection(state, fixTables(state, oldState), allowTableNodeSelection); }, }); } /** * Utility module to work with key-value stores. * * @module map */ /** * @template K * @template V * @typedef {Map} GlobalMap */ /** * Creates a new Map instance. * * @function * @return {Map} * * @function */ const create$9 = () => new Map(); /** * Copy a Map object into a fresh Map object. * * @function * @template K,V * @param {Map} m * @return {Map} */ const copy = (m) => { const r = create$9(); m.forEach((v, k) => { r.set(k, v); }); return r; }; /** * Get map property. Create T if property is undefined and set T on map. * * ```js * const listeners = map.setIfUndefined(events, 'eventName', set.create) * listeners.add(listener) * ``` * * @function * @template {Map} MAP * @template {MAP extends Map ? function():V : unknown} CF * @param {MAP} map * @param {MAP extends Map ? K : unknown} key * @param {CF} createT * @return {ReturnType} */ const setIfUndefined = (map, key, createT) => { let set = map.get(key); if (set === undefined) { map.set(key, (set = createT())); } return set; }; /** * Creates an Array and populates it with the content of all key-value pairs using the `f(value, key)` function. * * @function * @template K * @template V * @template R * @param {Map} m * @param {function(V,K):R} f * @return {Array} */ const map$3 = (m, f) => { const res = []; for (const [key, value] of m) { res.push(f(value, key)); } return res; }; /** * Tests whether any key-value pairs pass the test implemented by `f(value, key)`. * * @todo should rename to some - similarly to Array.some * * @function * @template K * @template V * @param {Map} m * @param {function(V,K):boolean} f * @return {boolean} */ const any = (m, f) => { for (const [key, value] of m) { if (f(value, key)) { return true; } } return false; }; /** * Utility module to work with sets. * * @module set */ const create$8 = () => new Set(); /** * Utility module to work with Arrays. * * @module array */ /** * Return the last element of an array. The element must exist * * @template L * @param {ArrayLike} arr * @return {L} */ const last = (arr) => arr[arr.length - 1]; /** * Append elements from src to dest * * @template M * @param {Array} dest * @param {Array} src */ const appendTo = (dest, src) => { for (let i = 0; i < src.length; i++) { dest.push(src[i]); } }; /** * Transforms something array-like to an actual Array. * * @function * @template T * @param {ArrayLike|Iterable} arraylike * @return {T} */ const from = Array.from; /** * True iff condition holds on every element in the Array. * * @function * @template {ArrayLike} ARR * * @param {ARR} arr * @param {ARR extends ArrayLike ? ((value:S, index:number, arr:ARR) => boolean) : any} f * @return {boolean} */ const every$1 = (arr, f) => { for (let i = 0; i < arr.length; i++) { if (!f(arr[i], i, arr)) { return false; } } return true; }; /** * True iff condition holds on some element in the Array. * * @function * @template {ArrayLike} ARR * * @param {ARR} arr * @param {ARR extends ArrayLike ? ((value:S, index:number, arr:ARR) => boolean) : never} f * @return {boolean} */ const some = (arr, f) => { for (let i = 0; i < arr.length; i++) { if (f(arr[i], i, arr)) { return true; } } return false; }; /** * @template T * @param {number} len * @param {function(number, Array):T} f * @return {Array} */ const unfold = (len, f) => { const array = new Array(len); for (let i = 0; i < len; i++) { array[i] = f(i, array); } return array; }; const isArray = Array.isArray; /** * Observable class prototype. * * @module observable */ /** * Handles named events. * @experimental * * This is basically a (better typed) duplicate of Observable, which will replace Observable in the * next release. * * @template {{[key in keyof EVENTS]: function(...any):void}} EVENTS */ class ObservableV2 { constructor() { /** * Some desc. * @type {Map>} */ this._observers = create$9(); } /** * @template {keyof EVENTS & string} NAME * @param {NAME} name * @param {EVENTS[NAME]} f */ on(name, f) { setIfUndefined(this._observers, /** @type {string} */ (name), create$8).add(f); return f; } /** * @template {keyof EVENTS & string} NAME * @param {NAME} name * @param {EVENTS[NAME]} f */ once(name, f) { /** * @param {...any} args */ const _f = (...args) => { this.off(name, /** @type {any} */ (_f)); f(...args); }; this.on(name, /** @type {any} */ (_f)); } /** * @template {keyof EVENTS & string} NAME * @param {NAME} name * @param {EVENTS[NAME]} f */ off(name, f) { const observers = this._observers.get(name); if (observers !== undefined) { observers.delete(f); if (observers.size === 0) { this._observers.delete(name); } } } /** * Emit a named event. All registered event listeners that listen to the * specified name will receive the event. * * @todo This should catch exceptions * * @template {keyof EVENTS & string} NAME * @param {NAME} name The event name. * @param {Parameters} args The arguments that are applied to the event listener. */ emit(name, args) { // copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called. return from((this._observers.get(name) || create$9()).values()).forEach((f) => f(...args)); } destroy() { this._observers = create$9(); } } /* c8 ignore end */ /** * Common Math expressions. * * @module math */ const floor$1 = Math.floor; const abs = Math.abs; /** * @function * @param {number} a * @param {number} b * @return {number} The smaller element of a and b */ const min$1 = (a, b) => (a < b ? a : b); /** * @function * @param {number} a * @param {number} b * @return {number} The bigger element of a and b */ const max$1 = (a, b) => (a > b ? a : b); /** * Check whether n is negative, while considering the -0 edge case. While `-0 < 0` is false, this * function returns true for -0,-1,,.. and returns false for 0,1,2,... * @param {number} n * @return {boolean} Wether n is negative. This function also distinguishes between -0 and +0 */ const isNegativeZero = (n) => (n !== 0 ? n < 0 : 1 / n < 0); /* eslint-env browser */ /** * Binary data constants. * * @module binary */ /** * n-th bit activated. * * @type {number} */ const BIT1 = 1; const BIT2 = 2; const BIT3 = 4; const BIT4 = 8; const BIT6 = 32; const BIT7 = 64; const BIT8 = 128; const BIT30 = 1 << 29; const BITS5 = 31; const BITS6 = 63; const BITS7 = 127; /** * @type {number} */ const BITS31 = 0x7fffffff; /** * Utility helpers for working with numbers. * * @module number */ const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; const MIN_SAFE_INTEGER = Number.MIN_SAFE_INTEGER; /* c8 ignore next */ const isInteger$1 = Number.isInteger || ((num) => typeof num === "number" && isFinite(num) && floor$1(num) === num); /** * Utility module to work with strings. * * @module string */ const fromCharCode = String.fromCharCode; /** * @param {string} s * @return {string} */ const toLowerCase = (s) => s.toLowerCase(); const trimLeftRegex = /^\s*/g; /** * @param {string} s * @return {string} */ const trimLeft = (s) => s.replace(trimLeftRegex, ""); const fromCamelCaseRegex = /([A-Z])/g; /** * @param {string} s * @param {string} separator * @return {string} */ const fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, (match) => `${separator}${toLowerCase(match)}`)); /** * @param {string} str * @return {Uint8Array} */ const _encodeUtf8Polyfill = (str) => { const encodedString = unescape(encodeURIComponent(str)); const len = encodedString.length; const buf = new Uint8Array(len); for (let i = 0; i < len; i++) { buf[i] = /** @type {number} */ (encodedString.codePointAt(i)); } return buf; }; /* c8 ignore next */ const utf8TextEncoder = /** @type {TextEncoder} */ (typeof TextEncoder !== "undefined" ? new TextEncoder() : null); /** * @param {string} str * @return {Uint8Array} */ const _encodeUtf8Native = (str) => utf8TextEncoder.encode(str); /** * @param {string} str * @return {Uint8Array} */ /* c8 ignore next */ const encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill; /* c8 ignore next */ let utf8TextDecoder = typeof TextDecoder === "undefined" ? null : new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }); /* c8 ignore start */ if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) { // Safari doesn't handle BOM correctly. // This fixes a bug in Safari 13.0.5 where it produces a BOM the first time it is called. // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the first call and // utf8TextDecoder.decode(new Uint8Array()).length === 1 on the second call // Another issue is that from then on no BOM chars are recognized anymore /* c8 ignore next */ utf8TextDecoder = null; } /** * @param {string} source * @param {number} n */ const repeat = (source, n) => unfold(n, () => source).join(""); /** * Efficient schema-less binary encoding with support for variable length encoding. * * Use [lib0/encoding] with [lib0/decoding]. Every encoding function has a corresponding decoding function. * * Encodes numbers in little-endian order (least to most significant byte order) * and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/) * which is also used in Protocol Buffers. * * ```js * // encoding step * const encoder = encoding.createEncoder() * encoding.writeVarUint(encoder, 256) * encoding.writeVarString(encoder, 'Hello world!') * const buf = encoding.toUint8Array(encoder) * ``` * * ```js * // decoding step * const decoder = decoding.createDecoder(buf) * decoding.readVarUint(decoder) // => 256 * decoding.readVarString(decoder) // => 'Hello world!' * decoding.hasContent(decoder) // => false - all data is read * ``` * * @module encoding */ /** * A BinaryEncoder handles the encoding to an Uint8Array. */ class Encoder { constructor() { this.cpos = 0; this.cbuf = new Uint8Array(100); /** * @type {Array} */ this.bufs = []; } } /** * @function * @return {Encoder} */ const createEncoder = () => new Encoder(); /** * @param {function(Encoder):void} f */ const encode = (f) => { const encoder = createEncoder(); f(encoder); return toUint8Array(encoder); }; /** * The current length of the encoded data. * * @function * @param {Encoder} encoder * @return {number} */ const length = (encoder) => { let len = encoder.cpos; for (let i = 0; i < encoder.bufs.length; i++) { len += encoder.bufs[i].length; } return len; }; /** * Transform to Uint8Array. * * @function * @param {Encoder} encoder * @return {Uint8Array} The created ArrayBuffer. */ const toUint8Array = (encoder) => { const uint8arr = new Uint8Array(length(encoder)); let curPos = 0; for (let i = 0; i < encoder.bufs.length; i++) { const d = encoder.bufs[i]; uint8arr.set(d, curPos); curPos += d.length; } uint8arr.set(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos), curPos); return uint8arr; }; /** * Verify that it is possible to write `len` bytes wtihout checking. If * necessary, a new Buffer with the required length is attached. * * @param {Encoder} encoder * @param {number} len */ const verifyLen = (encoder, len) => { const bufferLen = encoder.cbuf.length; if (bufferLen - encoder.cpos < len) { encoder.bufs.push(new Uint8Array(encoder.cbuf.buffer, 0, encoder.cpos)); encoder.cbuf = new Uint8Array(max$1(bufferLen, len) * 2); encoder.cpos = 0; } }; /** * Write one byte to the encoder. * * @function * @param {Encoder} encoder * @param {number} num The byte that is to be encoded. */ const write = (encoder, num) => { const bufferLen = encoder.cbuf.length; if (encoder.cpos === bufferLen) { encoder.bufs.push(encoder.cbuf); encoder.cbuf = new Uint8Array(bufferLen * 2); encoder.cpos = 0; } encoder.cbuf[encoder.cpos++] = num; }; /** * Write one byte as an unsigned integer. * * @function * @param {Encoder} encoder * @param {number} num The number that is to be encoded. */ const writeUint8 = write; /** * Write a variable length unsigned integer. Max encodable integer is 2^53. * * @function * @param {Encoder} encoder * @param {number} num The number that is to be encoded. */ const writeVarUint = (encoder, num) => { while (num > BITS7) { write(encoder, BIT8 | (BITS7 & num)); num = floor$1(num / 128); // shift >>> 7 } write(encoder, BITS7 & num); }; /** * Write a variable length integer. * * We use the 7th bit instead for signaling that this is a negative number. * * @function * @param {Encoder} encoder * @param {number} num The number that is to be encoded. */ const writeVarInt = (encoder, num) => { const isNegative = isNegativeZero(num); if (isNegative) { num = -num; } // |- whether to continue reading |- whether is negative |- number write(encoder, (num > BITS6 ? BIT8 : 0) | (isNegative ? BIT7 : 0) | (BITS6 & num)); num = floor$1(num / 64); // shift >>> 6 // We don't need to consider the case of num === 0 so we can use a different // pattern here than above. while (num > 0) { write(encoder, (num > BITS7 ? BIT8 : 0) | (BITS7 & num)); num = floor$1(num / 128); // shift >>> 7 } }; /** * A cache to store strings temporarily */ const _strBuffer = new Uint8Array(30000); const _maxStrBSize = _strBuffer.length / 3; /** * Write a variable length string. * * @function * @param {Encoder} encoder * @param {String} str The string that is to be encoded. */ const _writeVarStringNative = (encoder, str) => { if (str.length < _maxStrBSize) { // We can encode the string into the existing buffer /* c8 ignore next */ const written = utf8TextEncoder.encodeInto(str, _strBuffer).written || 0; writeVarUint(encoder, written); for (let i = 0; i < written; i++) { write(encoder, _strBuffer[i]); } } else { writeVarUint8Array(encoder, encodeUtf8(str)); } }; /** * Write a variable length string. * * @function * @param {Encoder} encoder * @param {String} str The string that is to be encoded. */ const _writeVarStringPolyfill = (encoder, str) => { const encodedString = unescape(encodeURIComponent(str)); const len = encodedString.length; writeVarUint(encoder, len); for (let i = 0; i < len; i++) { write(encoder, /** @type {number} */ (encodedString.codePointAt(i))); } }; /** * Write a variable length string. * * @function * @param {Encoder} encoder * @param {String} str The string that is to be encoded. */ /* c8 ignore next */ const writeVarString = utf8TextEncoder && /** @type {any} */ (utf8TextEncoder).encodeInto ? _writeVarStringNative : _writeVarStringPolyfill; /** * Append fixed-length Uint8Array to the encoder. * * @function * @param {Encoder} encoder * @param {Uint8Array} uint8Array */ const writeUint8Array = (encoder, uint8Array) => { const bufferLen = encoder.cbuf.length; const cpos = encoder.cpos; const leftCopyLen = min$1(bufferLen - cpos, uint8Array.length); const rightCopyLen = uint8Array.length - leftCopyLen; encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos); encoder.cpos += leftCopyLen; if (rightCopyLen > 0) { // Still something to write, write right half.. // Append new buffer encoder.bufs.push(encoder.cbuf); // must have at least size of remaining buffer encoder.cbuf = new Uint8Array(max$1(bufferLen * 2, rightCopyLen)); // copy array encoder.cbuf.set(uint8Array.subarray(leftCopyLen)); encoder.cpos = rightCopyLen; } }; /** * Append an Uint8Array to Encoder. * * @function * @param {Encoder} encoder * @param {Uint8Array} uint8Array */ const writeVarUint8Array = (encoder, uint8Array) => { writeVarUint(encoder, uint8Array.byteLength); writeUint8Array(encoder, uint8Array); }; /** * Create an DataView of the next `len` bytes. Use it to write data after * calling this function. * * ```js * // write float32 using DataView * const dv = writeOnDataView(encoder, 4) * dv.setFloat32(0, 1.1) * // read float32 using DataView * const dv = readFromDataView(encoder, 4) * dv.getFloat32(0) // => 1.100000023841858 (leaving it to the reader to find out why this is the correct result) * ``` * * @param {Encoder} encoder * @param {number} len * @return {DataView} */ const writeOnDataView = (encoder, len) => { verifyLen(encoder, len); const dview = new DataView(encoder.cbuf.buffer, encoder.cpos, len); encoder.cpos += len; return dview; }; /** * @param {Encoder} encoder * @param {number} num */ const writeFloat32 = (encoder, num) => writeOnDataView(encoder, 4).setFloat32(0, num, false); /** * @param {Encoder} encoder * @param {number} num */ const writeFloat64 = (encoder, num) => writeOnDataView(encoder, 8).setFloat64(0, num, false); /** * @param {Encoder} encoder * @param {bigint} num */ const writeBigInt64 = (encoder, num) => /** @type {any} */ (writeOnDataView(encoder, 8)).setBigInt64(0, num, false); const floatTestBed = new DataView(new ArrayBuffer(4)); /** * Check if a number can be encoded as a 32 bit float. * * @param {number} num * @return {boolean} */ const isFloat32 = (num) => { floatTestBed.setFloat32(0, num); return floatTestBed.getFloat32(0) === num; }; /** * @typedef {Array} AnyEncodableArray */ /** * @typedef {undefined|null|number|bigint|boolean|string|{[k:string]:AnyEncodable}|AnyEncodableArray|Uint8Array} AnyEncodable */ /** * Encode data with efficient binary format. * * Differences to JSON: * • Transforms data to a binary format (not to a string) * • Encodes undefined, NaN, and ArrayBuffer (these can't be represented in JSON) * • Numbers are efficiently encoded either as a variable length integer, as a * 32 bit float, as a 64 bit float, or as a 64 bit bigint. * * Encoding table: * * | Data Type | Prefix | Encoding Method | Comment | * | ------------------- | -------- | ------------------ | ------- | * | undefined | 127 | | Functions, symbol, and everything that cannot be identified is encoded as undefined | * | null | 126 | | | * | integer | 125 | writeVarInt | Only encodes 32 bit signed integers | * | float32 | 124 | writeFloat32 | | * | float64 | 123 | writeFloat64 | | * | bigint | 122 | writeBigInt64 | | * | boolean (false) | 121 | | True and false are different data types so we save the following byte | * | boolean (true) | 120 | | - 0b01111000 so the last bit determines whether true or false | * | string | 119 | writeVarString | | * | object | 118 | custom | Writes {length} then {length} key-value pairs | * | array | 117 | custom | Writes {length} then {length} json values | * | Uint8Array | 116 | writeVarUint8Array | We use Uint8Array for any kind of binary data | * * Reasons for the decreasing prefix: * We need the first bit for extendability (later we may want to encode the * prefix with writeVarUint). The remaining 7 bits are divided as follows: * [0-30] the beginning of the data range is used for custom purposes * (defined by the function that uses this library) * [31-127] the end of the data range is used for data encoding by * lib0/encoding.js * * @param {Encoder} encoder * @param {AnyEncodable} data */ const writeAny = (encoder, data) => { switch (typeof data) { case "string": // TYPE 119: STRING write(encoder, 119); writeVarString(encoder, data); break; case "number": if (isInteger$1(data) && abs(data) <= BITS31) { // TYPE 125: INTEGER write(encoder, 125); writeVarInt(encoder, data); } else if (isFloat32(data)) { // TYPE 124: FLOAT32 write(encoder, 124); writeFloat32(encoder, data); } else { // TYPE 123: FLOAT64 write(encoder, 123); writeFloat64(encoder, data); } break; case "bigint": // TYPE 122: BigInt write(encoder, 122); writeBigInt64(encoder, data); break; case "object": if (data === null) { // TYPE 126: null write(encoder, 126); } else if (isArray(data)) { // TYPE 117: Array write(encoder, 117); writeVarUint(encoder, data.length); for (let i = 0; i < data.length; i++) { writeAny(encoder, data[i]); } } else if (data instanceof Uint8Array) { // TYPE 116: ArrayBuffer write(encoder, 116); writeVarUint8Array(encoder, data); } else { // TYPE 118: Object write(encoder, 118); const keys = Object.keys(data); writeVarUint(encoder, keys.length); for (let i = 0; i < keys.length; i++) { const key = keys[i]; writeVarString(encoder, key); writeAny(encoder, data[key]); } } break; case "boolean": // TYPE 120/121: boolean (true/false) write(encoder, data ? 120 : 121); break; default: // TYPE 127: undefined write(encoder, 127); } }; /** * Now come a few stateful encoder that have their own classes. */ /** * Basic Run Length Encoder - a basic compression implementation. * * Encodes [1,1,1,7] to [1,3,7,1] (3 times 1, 1 time 7). This encoder might do more harm than good if there are a lot of values that are not repeated. * * It was originally used for image compression. Cool .. article http://csbruce.com/cbm/transactor/pdfs/trans_v7_i06.pdf * * @note T must not be null! * * @template T */ class RleEncoder extends Encoder { /** * @param {function(Encoder, T):void} writer */ constructor(writer) { super(); /** * The writer */ this.w = writer; /** * Current state * @type {T|null} */ this.s = null; this.count = 0; } /** * @param {T} v */ write(v) { if (this.s === v) { this.count++; } else { if (this.count > 0) { // flush counter, unless this is the first value (count = 0) writeVarUint(this, this.count - 1); // since count is always > 0, we can decrement by one. non-standard encoding ftw } this.count = 1; // write first value this.w(this, v); this.s = v; } } } /** * @param {UintOptRleEncoder} encoder */ const flushUintOptRleEncoder = (encoder) => { if (encoder.count > 0) { // flush counter, unless this is the first value (count = 0) // case 1: just a single value. set sign to positive // case 2: write several values. set sign to negative to indicate that there is a length coming writeVarInt(encoder.encoder, encoder.count === 1 ? encoder.s : -encoder.s); if (encoder.count > 1) { writeVarUint(encoder.encoder, encoder.count - 2); // since count is always > 1, we can decrement by one. non-standard encoding ftw } } }; /** * Optimized Rle encoder that does not suffer from the mentioned problem of the basic Rle encoder. * * Internally uses VarInt encoder to write unsigned integers. If the input occurs multiple times, we write * write it as a negative number. The UintOptRleDecoder then understands that it needs to read a count. * * Encodes [1,2,3,3,3] as [1,2,-3,3] (once 1, once 2, three times 3) */ class UintOptRleEncoder { constructor() { this.encoder = new Encoder(); /** * @type {number} */ this.s = 0; this.count = 0; } /** * @param {number} v */ write(v) { if (this.s === v) { this.count++; } else { flushUintOptRleEncoder(this); this.count = 1; this.s = v; } } /** * Flush the encoded state and transform this to a Uint8Array. * * Note that this should only be called once. */ toUint8Array() { flushUintOptRleEncoder(this); return toUint8Array(this.encoder); } } /** * @param {IntDiffOptRleEncoder} encoder */ const flushIntDiffOptRleEncoder = (encoder) => { if (encoder.count > 0) { // 31 bit making up the diff | wether to write the counter // const encodedDiff = encoder.diff << 1 | (encoder.count === 1 ? 0 : 1) const encodedDiff = encoder.diff * 2 + (encoder.count === 1 ? 0 : 1); // flush counter, unless this is the first value (count = 0) // case 1: just a single value. set first bit to positive // case 2: write several values. set first bit to negative to indicate that there is a length coming writeVarInt(encoder.encoder, encodedDiff); if (encoder.count > 1) { writeVarUint(encoder.encoder, encoder.count - 2); // since count is always > 1, we can decrement by one. non-standard encoding ftw } } }; /** * A combination of the IntDiffEncoder and the UintOptRleEncoder. * * The count approach is similar to the UintDiffOptRleEncoder, but instead of using the negative bitflag, it encodes * in the LSB whether a count is to be read. Therefore this Encoder only supports 31 bit integers! * * Encodes [1, 2, 3, 2] as [3, 1, 6, -1] (more specifically [(1 << 1) | 1, (3 << 0) | 0, -1]) * * Internally uses variable length encoding. Contrary to normal UintVar encoding, the first byte contains: * * 1 bit that denotes whether the next value is a count (LSB) * * 1 bit that denotes whether this value is negative (MSB - 1) * * 1 bit that denotes whether to continue reading the variable length integer (MSB) * * Therefore, only five bits remain to encode diff ranges. * * Use this Encoder only when appropriate. In most cases, this is probably a bad idea. */ class IntDiffOptRleEncoder { constructor() { this.encoder = new Encoder(); /** * @type {number} */ this.s = 0; this.count = 0; this.diff = 0; } /** * @param {number} v */ write(v) { if (this.diff === v - this.s) { this.s = v; this.count++; } else { flushIntDiffOptRleEncoder(this); this.count = 1; this.diff = v - this.s; this.s = v; } } /** * Flush the encoded state and transform this to a Uint8Array. * * Note that this should only be called once. */ toUint8Array() { flushIntDiffOptRleEncoder(this); return toUint8Array(this.encoder); } } /** * Optimized String Encoder. * * Encoding many small strings in a simple Encoder is not very efficient. The function call to decode a string takes some time and creates references that must be eventually deleted. * In practice, when decoding several million small strings, the GC will kick in more and more often to collect orphaned string objects (or maybe there is another reason?). * * This string encoder solves the above problem. All strings are concatenated and written as a single string using a single encoding call. * * The lengths are encoded using a UintOptRleEncoder. */ class StringEncoder { constructor() { /** * @type {Array} */ this.sarr = []; this.s = ""; this.lensE = new UintOptRleEncoder(); } /** * @param {string} string */ write(string) { this.s += string; if (this.s.length > 19) { this.sarr.push(this.s); this.s = ""; } this.lensE.write(string.length); } toUint8Array() { const encoder = new Encoder(); this.sarr.push(this.s); this.s = ""; writeVarString(encoder, this.sarr.join("")); writeUint8Array(encoder, this.lensE.toUint8Array()); return toUint8Array(encoder); } } /** * Error helpers. * * @module error */ /** * @param {string} s * @return {Error} */ /* c8 ignore next */ const create$7 = (s) => new Error(s); /** * @throws {Error} * @return {never} */ /* c8 ignore next 3 */ const methodUnimplemented = () => { throw create$7("Method unimplemented"); }; /** * @throws {Error} * @return {never} */ /* c8 ignore next 3 */ const unexpectedCase = () => { throw create$7("Unexpected case"); }; /** * Efficient schema-less binary decoding with support for variable length encoding. * * Use [lib0/decoding] with [lib0/encoding]. Every encoding function has a corresponding decoding function. * * Encodes numbers in little-endian order (least to most significant byte order) * and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/) * which is also used in Protocol Buffers. * * ```js * // encoding step * const encoder = encoding.createEncoder() * encoding.writeVarUint(encoder, 256) * encoding.writeVarString(encoder, 'Hello world!') * const buf = encoding.toUint8Array(encoder) * ``` * * ```js * // decoding step * const decoder = decoding.createDecoder(buf) * decoding.readVarUint(decoder) // => 256 * decoding.readVarString(decoder) // => 'Hello world!' * decoding.hasContent(decoder) // => false - all data is read * ``` * * @module decoding */ const errorUnexpectedEndOfArray = create$7("Unexpected end of array"); const errorIntegerOutOfRange = create$7("Integer out of Range"); /** * A Decoder handles the decoding of an Uint8Array. * @template {ArrayBufferLike} [Buf=ArrayBufferLike] */ class Decoder { /** * @param {Uint8Array} uint8Array Binary data to decode */ constructor(uint8Array) { /** * Decoding target. * * @type {Uint8Array} */ this.arr = uint8Array; /** * Current decoding position. * * @type {number} */ this.pos = 0; } } /** * @function * @template {ArrayBufferLike} Buf * @param {Uint8Array} uint8Array * @return {Decoder} */ const createDecoder = (uint8Array) => new Decoder(uint8Array); /** * @function * @param {Decoder} decoder * @return {boolean} */ const hasContent = (decoder) => decoder.pos !== decoder.arr.length; /** * Create an Uint8Array view of the next `len` bytes and advance the position by `len`. * * Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks. * Use `buffer.copyUint8Array` to copy the result into a new Uint8Array. * * @function * @template {ArrayBufferLike} Buf * @param {Decoder} decoder The decoder instance * @param {number} len The length of bytes to read * @return {Uint8Array} */ const readUint8Array = (decoder, len) => { const view = new Uint8Array(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len); decoder.pos += len; return view; }; /** * Read variable length Uint8Array. * * Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks. * Use `buffer.copyUint8Array` to copy the result into a new Uint8Array. * * @function * @template {ArrayBufferLike} Buf * @param {Decoder} decoder * @return {Uint8Array} */ const readVarUint8Array = (decoder) => readUint8Array(decoder, readVarUint(decoder)); /** * Read one byte as unsigned integer. * @function * @param {Decoder} decoder The decoder instance * @return {number} Unsigned 8-bit integer */ const readUint8 = (decoder) => decoder.arr[decoder.pos++]; /** * Read unsigned integer (32bit) with variable length. * 1/8th of the storage is used as encoding overhead. * * numbers < 2^7 is stored in one bytlength * * numbers < 2^14 is stored in two bylength * * @function * @param {Decoder} decoder * @return {number} An unsigned integer.length */ const readVarUint = (decoder) => { let num = 0; let mult = 1; const len = decoder.arr.length; while (decoder.pos < len) { const r = decoder.arr[decoder.pos++]; // num = num | ((r & binary.BITS7) << len) num = num + (r & BITS7) * mult; // shift $r << (7*#iterations) and add it to num mult *= 128; // next iteration, shift 7 "more" to the left if (r < BIT8) { return num; } /* c8 ignore start */ if (num > MAX_SAFE_INTEGER) { throw errorIntegerOutOfRange; } /* c8 ignore stop */ } throw errorUnexpectedEndOfArray; }; /** * Read signed integer (32bit) with variable length. * 1/8th of the storage is used as encoding overhead. * * numbers < 2^7 is stored in one bytlength * * numbers < 2^14 is stored in two bylength * @todo This should probably create the inverse ~num if number is negative - but this would be a breaking change. * * @function * @param {Decoder} decoder * @return {number} An unsigned integer.length */ const readVarInt = (decoder) => { let r = decoder.arr[decoder.pos++]; let num = r & BITS6; let mult = 64; const sign = (r & BIT7) > 0 ? -1 : 1; if ((r & BIT8) === 0) { // don't continue reading return sign * num; } const len = decoder.arr.length; while (decoder.pos < len) { r = decoder.arr[decoder.pos++]; // num = num | ((r & binary.BITS7) << len) num = num + (r & BITS7) * mult; mult *= 128; if (r < BIT8) { return sign * num; } /* c8 ignore start */ if (num > MAX_SAFE_INTEGER) { throw errorIntegerOutOfRange; } /* c8 ignore stop */ } throw errorUnexpectedEndOfArray; }; /** * We don't test this function anymore as we use native decoding/encoding by default now. * Better not modify this anymore.. * * Transforming utf8 to a string is pretty expensive. The code performs 10x better * when String.fromCodePoint is fed with all characters as arguments. * But most environments have a maximum number of arguments per functions. * For effiency reasons we apply a maximum of 10000 characters at once. * * @function * @param {Decoder} decoder * @return {String} The read String. */ /* c8 ignore start */ const _readVarStringPolyfill = (decoder) => { let remainingLen = readVarUint(decoder); if (remainingLen === 0) { return ""; } else { let encodedString = String.fromCodePoint(readUint8(decoder)); // remember to decrease remainingLen if (--remainingLen < 100) { // do not create a Uint8Array for small strings while (remainingLen--) { encodedString += String.fromCodePoint(readUint8(decoder)); } } else { while (remainingLen > 0) { const nextLen = remainingLen < 10000 ? remainingLen : 10000; // this is dangerous, we create a fresh array view from the existing buffer const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen); decoder.pos += nextLen; // Starting with ES5.1 we can supply a generic array-like object as arguments encodedString += String.fromCodePoint.apply(null, /** @type {any} */ (bytes)); remainingLen -= nextLen; } } return decodeURIComponent(escape(encodedString)); } }; /* c8 ignore stop */ /** * @function * @param {Decoder} decoder * @return {String} The read String */ const _readVarStringNative = (decoder) => /** @type any */ (utf8TextDecoder).decode(readVarUint8Array(decoder)); /** * Read string of variable length * * varUint is used to store the length of the string * * @function * @param {Decoder} decoder * @return {String} The read String * */ /* c8 ignore next */ const readVarString = utf8TextDecoder ? _readVarStringNative : _readVarStringPolyfill; /** * @param {Decoder} decoder * @param {number} len * @return {DataView} */ const readFromDataView = (decoder, len) => { const dv = new DataView(decoder.arr.buffer, decoder.arr.byteOffset + decoder.pos, len); decoder.pos += len; return dv; }; /** * @param {Decoder} decoder */ const readFloat32 = (decoder) => readFromDataView(decoder, 4).getFloat32(0, false); /** * @param {Decoder} decoder */ const readFloat64 = (decoder) => readFromDataView(decoder, 8).getFloat64(0, false); /** * @param {Decoder} decoder */ const readBigInt64 = (decoder) => /** @type {any} */ (readFromDataView(decoder, 8)).getBigInt64(0, false); /** * @type {Array} */ const readAnyLookupTable = [ (decoder) => undefined, // CASE 127: undefined (decoder) => null, // CASE 126: null readVarInt, // CASE 125: integer readFloat32, // CASE 124: float32 readFloat64, // CASE 123: float64 readBigInt64, // CASE 122: bigint (decoder) => false, // CASE 121: boolean (false) (decoder) => true, // CASE 120: boolean (true) readVarString, // CASE 119: string (decoder) => { // CASE 118: object const len = readVarUint(decoder); /** * @type {Object} */ const obj = {}; for (let i = 0; i < len; i++) { const key = readVarString(decoder); obj[key] = readAny(decoder); } return obj; }, (decoder) => { // CASE 117: array const len = readVarUint(decoder); const arr = []; for (let i = 0; i < len; i++) { arr.push(readAny(decoder)); } return arr; }, readVarUint8Array, // CASE 116: Uint8Array ]; /** * @param {Decoder} decoder */ const readAny = (decoder) => readAnyLookupTable[127 - readUint8(decoder)](decoder); /** * T must not be null. * * @template T */ class RleDecoder extends Decoder { /** * @param {Uint8Array} uint8Array * @param {function(Decoder):T} reader */ constructor(uint8Array, reader) { super(uint8Array); /** * The reader */ this.reader = reader; /** * Current state * @type {T|null} */ this.s = null; this.count = 0; } read() { if (this.count === 0) { this.s = this.reader(this); if (hasContent(this)) { this.count = readVarUint(this) + 1; // see encoder implementation for the reason why this is incremented } else { this.count = -1; // read the current value forever } } this.count--; return /** @type {T} */ (this.s); } } class UintOptRleDecoder extends Decoder { /** * @param {Uint8Array} uint8Array */ constructor(uint8Array) { super(uint8Array); /** * @type {number} */ this.s = 0; this.count = 0; } read() { if (this.count === 0) { this.s = readVarInt(this); // if the sign is negative, we read the count too, otherwise count is 1 const isNegative = isNegativeZero(this.s); this.count = 1; if (isNegative) { this.s = -this.s; this.count = readVarUint(this) + 2; } } this.count--; return /** @type {number} */ (this.s); } } class IntDiffOptRleDecoder extends Decoder { /** * @param {Uint8Array} uint8Array */ constructor(uint8Array) { super(uint8Array); /** * @type {number} */ this.s = 0; this.count = 0; this.diff = 0; } /** * @return {number} */ read() { if (this.count === 0) { const diff = readVarInt(this); // if the first bit is set, we read more data const hasCount = diff & 1; this.diff = floor$1(diff / 2); // shift >> 1 this.count = 1; if (hasCount) { this.count = readVarUint(this) + 2; } } this.s += this.diff; this.count--; return this.s; } } class StringDecoder { /** * @param {Uint8Array} uint8Array */ constructor(uint8Array) { this.decoder = new UintOptRleDecoder(uint8Array); this.str = readVarString(this.decoder); /** * @type {number} */ this.spos = 0; } /** * @return {string} */ read() { const end = this.spos + this.decoder.read(); const res = this.str.slice(this.spos, end); this.spos = end; return res; } } /* eslint-env browser */ const getRandomValues = crypto.getRandomValues.bind(crypto); /** * Isomorphic module for true random numbers / buffers / uuids. * * Attention: falls back to Math.random if the browser does not support crypto. * * @module random */ const rand = Math.random; const uint32 = () => getRandomValues(new Uint32Array(1))[0]; /** * @template T * @param {Array} arr * @return {T} */ const oneOf$1 = (arr) => arr[floor$1(rand() * arr.length)]; // @ts-ignore const uuidv4Template = [1e7] + -1e3 + -4e3 + -8e3 + -1e11; /** * @return {string} */ const uuidv4 = () => uuidv4Template.replace(/[018]/g, /** @param {number} c */ (c) => (c ^ (uint32() & (15 >> (c / 4)))).toString(16)); /** * Utility module to work with time. * * @module time */ /** * Return current unix time. * * @return {number} */ const getUnixTime = Date.now; /** * Utility helpers to work with promises. * * @module promise */ /** * @template T * @callback PromiseResolve * @param {T|PromiseLike} [result] */ /** * @template T * @param {function(PromiseResolve,function(Error):void):any} f * @return {Promise} */ const create$6 = (f) => /** @type {Promise} */ (new Promise(f)); /** * `Promise.all` wait for all promises in the array to resolve and return the result * @template {unknown[] | []} PS * * @param {PS} ps * @return {Promise<{ -readonly [P in keyof PS]: Awaited }>} */ Promise.all.bind(Promise); /** * Often used conditions. * * @module conditions */ /** * @template T * @param {T|null|undefined} v * @return {T|null} */ /* c8 ignore next */ const undefinedToNull = (v) => (v === undefined ? null : v); /* eslint-env browser */ /** * Isomorphic variable storage. * * Uses LocalStorage in the browser and falls back to in-memory storage. * * @module storage */ /* c8 ignore start */ class VarStoragePolyfill { constructor() { this.map = new Map(); } /** * @param {string} key * @param {any} newValue */ setItem(key, newValue) { this.map.set(key, newValue); } /** * @param {string} key */ getItem(key) { return this.map.get(key); } } /* c8 ignore stop */ /** * @type {any} */ let _localStorage = new VarStoragePolyfill(); let usePolyfill = true; /* c8 ignore start */ try { // if the same-origin rule is violated, accessing localStorage might thrown an error if (typeof localStorage !== "undefined" && localStorage) { _localStorage = localStorage; usePolyfill = false; } } catch (e) {} /* c8 ignore stop */ /** * This is basically localStorage in browser, or a polyfill in nodejs */ /* c8 ignore next */ const varStorage = _localStorage; const EqualityTraitSymbol = Symbol("Equality"); /** * @typedef {{ [EqualityTraitSymbol]:(other:EqualityTrait)=>boolean }} EqualityTrait */ /** * * Utility function to compare any two objects. * * Note that it is expected that the first parameter is more specific than the latter one. * * @example js * class X { [traits.EqualityTraitSymbol] (other) { return other === this } } * class X2 { [traits.EqualityTraitSymbol] (other) { return other === this }, x2 () { return 2 } } * // this is fine * traits.equals(new X2(), new X()) * // this is not, because the left type is less specific than the right one * traits.equals(new X(), new X2()) * * @template {EqualityTrait} T * @param {NoInfer} a * @param {T} b * @return {boolean} */ const equals = (a, b) => a === b || !!a?.[EqualityTraitSymbol]?.(b) || false; /** * @param {any} o * @return {o is { [k:string]:any }} */ const isObject$1 = (o) => typeof o === "object"; /** * Object.assign */ const assign = Object.assign; /** * @param {Object} obj */ const keys$1 = Object.keys; /** * @template V * @param {{[k:string]:V}} obj * @param {function(V,string):any} f */ const forEach = (obj, f) => { for (const key in obj) { f(obj[key], key); } }; /** * @param {Object} obj * @return {number} */ const size$3 = (obj) => keys$1(obj).length; /** * @param {Object|null|undefined} obj */ const isEmpty = (obj) => { // eslint-disable-next-line no-unreachable-loop for (const _k in obj) { return false; } return true; }; /** * @template {{ [key:string|number|symbol]: any }} T * @param {T} obj * @param {(v:T[keyof T],k:keyof T)=>boolean} f * @return {boolean} */ const every = (obj, f) => { for (const key in obj) { if (!f(obj[key], key)) { return false; } } return true; }; /** * Calls `Object.prototype.hasOwnProperty`. * * @param {any} obj * @param {string|number|symbol} key * @return {boolean} */ const hasProperty$1 = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); /** * @param {Object} a * @param {Object} b * @return {boolean} */ const equalFlat = (a, b) => a === b || (size$3(a) === size$3(b) && every(a, (val, key) => (val !== undefined || hasProperty$1(b, key)) && equals(b[key], val))); /** * Make an object immutable. This hurts performance and is usually not needed if you perform good * coding practices. */ const freeze = Object.freeze; /** * Make an object and all its children immutable. * This *really* hurts performance and is usually not needed if you perform good coding practices. * * @template {any} T * @param {T} o * @return {Readonly} */ const deepFreeze = (o) => { for (const key in o) { const c = o[key]; if (typeof c === "object" || typeof c === "function") { deepFreeze(o[key]); } } return freeze(o); }; /** * Common functions and function call helpers. * * @module function */ /** * Calls all functions in `fs` with args. Only throws after all functions were called. * * @param {Array} fs * @param {Array} args */ const callAll = (fs, args, i = 0) => { try { for (; i < fs.length; i++) { fs[i](...args); } } finally { if (i < fs.length) { callAll(fs, args, i + 1); } } }; /** * @template A * * @param {A} a * @return {A} */ const id = (a) => a; /* c8 ignore start */ /** * @param {any} a * @param {any} b * @return {boolean} */ const equalityDeep = (a, b) => { if (a === b) { return true; } if (a == null || b == null || (a.constructor !== b.constructor && (a.constructor || Object) !== (b.constructor || Object))) { return false; } if (a[EqualityTraitSymbol] != null) { return a[EqualityTraitSymbol](b); } switch (a.constructor) { case ArrayBuffer: a = new Uint8Array(a); b = new Uint8Array(b); // eslint-disable-next-line no-fallthrough case Uint8Array: { if (a.byteLength !== b.byteLength) { return false; } for (let i = 0; i < a.length; i++) { if (a[i] !== b[i]) { return false; } } break; } case Set: { if (a.size !== b.size) { return false; } for (const value of a) { if (!b.has(value)) { return false; } } break; } case Map: { if (a.size !== b.size) { return false; } for (const key of a.keys()) { if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) { return false; } } break; } case undefined: case Object: if (size$3(a) !== size$3(b)) { return false; } for (const key in a) { if (!hasProperty$1(a, key) || !equalityDeep(a[key], b[key])) { return false; } } break; case Array: if (a.length !== b.length) { return false; } for (let i = 0; i < a.length; i++) { if (!equalityDeep(a[i], b[i])) { return false; } } break; default: return false; } return true; }; /** * @template V * @template {V} OPTS * * @param {V} value * @param {Array} options */ // @ts-ignore const isOneOf = (value, options) => options.includes(value); var define_process_env_default = {}; const isNode$1 = typeof process !== "undefined" && process.release && /node|io\.js/.test(process.release.name) && Object.prototype.toString.call(typeof process !== "undefined" ? process : 0) === "[object process]"; const isBrowser = typeof window !== "undefined" && typeof document !== "undefined" && !isNode$1; let params; const computeParams = () => { if (params === void 0) { if (isNode$1) { params = create$9(); const pargs = process.argv; let currParamName = null; for (let i = 0; i < pargs.length; i++) { const parg = pargs[i]; if (parg[0] === "-") { if (currParamName !== null) { params.set(currParamName, ""); } currParamName = parg; } else { if (currParamName !== null) { params.set(currParamName, parg); currParamName = null; } } } if (currParamName !== null) { params.set(currParamName, ""); } } else if (typeof location === "object") { params = create$9(); (location.search || "?") .slice(1) .split("&") .forEach((kv) => { if (kv.length !== 0) { const [key, value] = kv.split("="); params.set(`--${fromCamelCase(key, "-")}`, value); params.set(`-${fromCamelCase(key, "-")}`, value); } }); } else { params = create$9(); } } return params; }; const hasParam = (name) => computeParams().has(name); const getVariable = (name) => (isNode$1 ? undefinedToNull(define_process_env_default[name.toUpperCase().replaceAll("-", "_")]) : undefinedToNull(varStorage.getItem(name))); const hasConf = (name) => hasParam("--" + name) || getVariable(name) !== null; const production = hasConf("production"); const forceColor = isNode$1 && isOneOf(define_process_env_default.FORCE_COLOR, ["true", "1", "2"]); const supportsColor = forceColor || (!hasParam("--no-colors") && // @todo deprecate --no-colors !hasConf("no-color") && (!isNode$1 || process.stdout.isTTY) && (!isNode$1 || hasParam("--color") || getVariable("COLORTERM") !== null || (getVariable("TERM") || "").includes("color"))); /** * Utility functions to work with buffers (Uint8Array). * * @module buffer */ /** * @param {number} len */ const createUint8ArrayFromLen = (len) => new Uint8Array(len); /* c8 ignore start */ /** * @param {Uint8Array} bytes * @return {string} */ const toBase64Browser = (bytes) => { let s = ""; for (let i = 0; i < bytes.byteLength; i++) { s += fromCharCode(bytes[i]); } // eslint-disable-next-line no-undef return btoa(s); }; /* c8 ignore stop */ /** * @param {Uint8Array} bytes * @return {string} */ const toBase64Node = (bytes) => Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64"); /* c8 ignore next */ const toBase64 = isBrowser ? toBase64Browser : toBase64Node; /** * Copy the content of an Uint8Array view to a new ArrayBuffer. * * @param {Uint8Array} uint8Array * @return {Uint8Array} */ const copyUint8Array = (uint8Array) => { const newBuf = createUint8ArrayFromLen(uint8Array.byteLength); newBuf.set(uint8Array); return newBuf; }; /** * Encode anything as a UInt8Array. It's a pun on typescripts's `any` type. * See encoding.writeAny for more information. * * @param {any} data * @return {Uint8Array} */ const encodeAny = (data) => encode((encoder) => writeAny(encoder, data)); /** * Working with value pairs. * * @module pair */ /** * @template L,R */ class Pair { /** * @param {L} left * @param {R} right */ constructor(left, right) { this.left = left; this.right = right; } } /** * @template L,R * @param {L} left * @param {R} right * @return {Pair} */ const create$5 = (left, right) => new Pair(left, right); /** * Fast Pseudo Random Number Generators. * * Given a seed a PRNG generates a sequence of numbers that cannot be reasonably predicted. * Two PRNGs must generate the same random sequence of numbers if given the same seed. * * @module prng */ /** * Generates a single random bool. * * @param {PRNG} gen A random number generator. * @return {Boolean} A random boolean */ const bool = (gen) => gen.next() >= 0.5; /** * Generates a random integer with 53 bit resolution. * * @param {PRNG} gen A random number generator. * @param {Number} min The lower bound of the allowed return values (inclusive). * @param {Number} max The upper bound of the allowed return values (inclusive). * @return {Number} A random integer on [min, max] */ const int53 = (gen, min, max) => floor$1(gen.next() * (max + 1 - min) + min); /** * Generates a random integer with 32 bit resolution. * * @param {PRNG} gen A random number generator. * @param {Number} min The lower bound of the allowed return values (inclusive). * @param {Number} max The upper bound of the allowed return values (inclusive). * @return {Number} A random integer on [min, max] */ const int32 = (gen, min, max) => floor$1(gen.next() * (max + 1 - min) + min); /** * @deprecated * Optimized version of prng.int32. It has the same precision as prng.int32, but should be preferred when * openaring on smaller ranges. * * @param {PRNG} gen A random number generator. * @param {Number} min The lower bound of the allowed return values (inclusive). * @param {Number} max The upper bound of the allowed return values (inclusive). The max inclusive number is `binary.BITS31-1` * @return {Number} A random integer on [min, max] */ const int31 = (gen, min, max) => int32(gen, min, max); /** * @param {PRNG} gen * @return {string} A single letter (a-z) */ const letter = (gen) => fromCharCode(int31(gen, 97, 122)); /** * @param {PRNG} gen * @param {number} [minLen=0] * @param {number} [maxLen=20] * @return {string} A random word (0-20 characters) without spaces consisting of letters (a-z) */ const word = (gen, minLen = 0, maxLen = 20) => { const len = int31(gen, minLen, maxLen); let str = ""; for (let i = 0; i < len; i++) { str += letter(gen); } return str; }; /** * Returns one element of a given array. * * @param {PRNG} gen A random number generator. * @param {Array} array Non empty Array of possible values. * @return {T} One of the values of the supplied Array. * @template T */ const oneOf = (gen, array) => array[int31(gen, 0, array.length - 1)]; /* c8 ignore stop */ /** * @experimental WIP * * Simple & efficient schemas for your data. */ /** * @typedef {string|number|bigint|boolean|null|undefined|symbol} Primitive */ /** * @typedef {{ [k:string|number|symbol]: any }} AnyObject */ /** * @template T * @typedef {T extends Schema ? X : T} Unwrap */ /** * @template T * @typedef {T extends Schema ? X : T} TypeOf */ /** * @template {readonly unknown[]} T * @typedef {T extends readonly [Schema, ...infer Rest] ? [First, ...UnwrapArray] : [] } UnwrapArray */ /** * @template T * @typedef {T extends Schema ? Schema : never} CastToSchema */ /** * @template {unknown[]} Arr * @typedef {Arr extends [...unknown[], infer L] ? L : never} TupleLast */ /** * @template {unknown[]} Arr * @typedef {Arr extends [...infer Fs, unknown] ? Fs : never} TuplePop */ /** * @template {readonly unknown[]} T * @typedef {T extends [] * ? {} * : T extends [infer First] * ? First * : T extends [infer First, ...infer Rest] * ? First & Intersect * : never * } Intersect */ const schemaSymbol = Symbol("0schema"); class ValidationError { constructor() { /** * Reverse errors * @type {Array<{ path: string?, expected: string, has: string, message: string? }>} */ this._rerrs = []; } /** * @param {string?} path * @param {string} expected * @param {string} has * @param {string?} message */ extend(path, expected, has, message = null) { this._rerrs.push({ path, expected, has, message }); } toString() { const s = []; for (let i = this._rerrs.length - 1; i > 0; i--) { const r = this._rerrs[i]; /* c8 ignore next */ s.push(repeat(" ", (this._rerrs.length - i) * 2) + `${r.path != null ? `[${r.path}] ` : ""}${r.has} doesn't match ${r.expected}. ${r.message}`); } return s.join("\n"); } } /** * @param {any} a * @param {any} b * @return {boolean} */ const shapeExtends = (a, b) => { if (a === b) return true; if (a == null || b == null || a.constructor !== b.constructor) return false; if (a[EqualityTraitSymbol]) return equals(a, b); // last resort: check equality (do this before array and obj check which don't implement the equality trait) if (isArray(a)) { return every$1(a, (aitem) => some(b, (bitem) => shapeExtends(aitem, bitem))); } else if (isObject$1(a)) { return every(a, (aitem, akey) => shapeExtends(aitem, b[akey])); } /* c8 ignore next */ return false; }; /** * @template T * @implements {equalityTraits.EqualityTrait} */ let Schema$1 = class Schema { // this.shape must not be defined on Schema. Otherwise typecheck on metatypes (e.g. $$object) won't work as expected anymore /** * If true, the more things are added to the shape the more objects this schema will accept (e.g. * union). By default, the more objects are added, the the fewer objects this schema will accept. * @protected */ static _dilutes = false; /** * @param {Schema} other */ extends(other) { let [a, b] = [/** @type {any} */ (this).shape, /** @type {any} */ (other).shape]; if (/** @type {typeof Schema} */ (this.constructor)._dilutes) [b, a] = [a, b]; return shapeExtends(a, b); } /** * Overwrite this when necessary. By default, we only check the `shape` property which every shape * should have. * @param {Schema} other */ equals(other) { // @ts-ignore return this.constructor === other.constructor && equalityDeep(this.shape, other.shape); } [schemaSymbol]() { return true; } /** * @param {object} other */ [EqualityTraitSymbol](other) { return this.equals(/** @type {any} */ (other)); } /** * Use `schema.validate(obj)` with a typed parameter that is already of typed to be an instance of * Schema. Validate will check the structure of the parameter and return true iff the instance * really is an instance of Schema. * * @param {T} o * @return {boolean} */ validate(o) { return this.check(o); } /* c8 ignore start */ /** * Similar to validate, but this method accepts untyped parameters. * * @param {any} _o * @param {ValidationError} [_err] * @return {_o is T} */ check(_o, _err) { methodUnimplemented(); } /* c8 ignore stop */ /** * @type {Schema} */ get nullable() { // @ts-ignore return $union(this, $null); } /** * @type {$Optional>} */ get optional() { return new $Optional(/** @type {Schema} */ (this)); } /** * Cast a variable to a specific type. Returns the casted value, or throws an exception otherwise. * Use this if you know that the type is of a specific type and you just want to convince the type * system. * * **Do not rely on these error messages!** * Performs an assertion check only if not in a production environment. * * @template OO * @param {OO} o * @return {Extract extends never ? T : (OO extends Array ? T : Extract)} */ cast(o) { assert(o, this); return /** @type {any} */ (o); } /** * EXPECTO PATRONUM!! 🪄 * This function protects against type errors. Though it may not work in the real world. * * "After all this time?" * "Always." - Snape, talking about type safety * * Ensures that a variable is a a specific type. Returns the value, or throws an exception if the assertion check failed. * Use this if you know that the type is of a specific type and you just want to convince the type * system. * * Can be useful when defining lambdas: `s.lambda(s.$number, s.$void).expect((n) => n + 1)` * * **Do not rely on these error messages!** * Performs an assertion check if not in a production environment. * * @param {T} o * @return {o extends T ? T : never} */ expect(o) { assert(o, this); return o; } }; /** * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} Constr * @typedef {Constr extends ((...args:any[]) => infer T) ? T : (Constr extends (new (...args:any[]) => any) ? InstanceType : never)} Instance */ /** * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} C * @extends {Schema>} */ class $ConstructedBy extends Schema$1 { /** * @param {C} c * @param {((o:Instance)=>boolean)|null} check */ constructor(c, check) { super(); this.shape = c; this._c = check; } /** * @param {any} o * @param {ValidationError} [err] * @return {o is C extends ((...args:any[]) => infer T) ? T : (C extends (new (...args:any[]) => any) ? InstanceType : never)} o */ check(o, err = undefined) { const c = o?.constructor === this.shape && (this._c == null || this._c(o)); /* c8 ignore next */ !c && err?.extend(null, this.shape.name, o?.constructor.name, o?.constructor !== this.shape ? "Constructor match failed" : "Check failed"); return c; } } /** * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} C * @param {C} c * @param {((o:Instance) => boolean)|null} check * @return {CastToSchema<$ConstructedBy>} */ const $constructedBy = (c, check = null) => new $ConstructedBy(c, check); $constructedBy($ConstructedBy); /** * Check custom properties on any object. You may want to overwrite the generated Schema. * * @extends {Schema} */ class $Custom extends Schema$1 { /** * @param {(o:any) => boolean} check */ constructor(check) { super(); /** * @type {(o:any) => boolean} */ this.shape = check; } /** * @param {any} o * @param {ValidationError} err * @return {o is any} */ check(o, err) { const c = this.shape(o); /* c8 ignore next */ !c && err?.extend(null, "custom prop", o?.constructor.name, "failed to check custom prop"); return c; } } /** * @param {(o:any) => boolean} check * @return {Schema} */ const $custom = (check) => new $Custom(check); $constructedBy($Custom); /** * @template {Primitive} T * @extends {Schema} */ class $Literal extends Schema$1 { /** * @param {Array} literals */ constructor(literals) { super(); this.shape = literals; } /** * * @param {any} o * @param {ValidationError} [err] * @return {o is T} */ check(o, err) { const c = this.shape.some((a) => a === o); /* c8 ignore next */ !c && err?.extend(null, this.shape.join(" | "), o.toString()); return c; } } /** * @template {Primitive[]} T * @param {T} literals * @return {CastToSchema<$Literal>} */ const $literal = (...literals) => new $Literal(literals); const $$literal = $constructedBy($Literal); /** * @template {Array>} Ts * @typedef {Ts extends [] ? `` : (Ts extends [infer T] ? (Unwrap extends (string|number) ? Unwrap : never) : (Ts extends [infer T1, ...infer Rest] ? `${Unwrap extends (string|number) ? Unwrap : never}${Rest extends Array> ? CastStringTemplateArgsToTemplate : never}` : never))} CastStringTemplateArgsToTemplate */ /** * @param {string} str * @return {string} */ const _regexEscape = /** @type {any} */ (RegExp).escape || /** @type {(str:string) => string} */ ((str) => str.replace(/[().|&,$^[\]]/g, (s) => "\\" + s)); /** * @param {string|Schema} s * @return {string[]} */ const _schemaStringTemplateToRegex = (s) => { if ($string.check(s)) { return [_regexEscape(s)]; } if ($$literal.check(s)) { return /** @type {Array} */ (s.shape).map((v) => v + ""); } if ($$number.check(s)) { return ["[+-]?\\d+.?\\d*"]; } if ($$string.check(s)) { return [".*"]; } if ($$union.check(s)) { return s.shape.map(_schemaStringTemplateToRegex).flat(1); } /* c8 ignore next 2 */ // unexpected schema structure (only supports unions and string in literal types) unexpectedCase(); }; /** * @template {Array>} T * @extends {Schema>} */ class $StringTemplate extends Schema$1 { /** * @param {T} shape */ constructor(shape) { super(); this.shape = shape; this._r = new RegExp( "^" + shape .map(_schemaStringTemplateToRegex) .map((opts) => `(${opts.join("|")})`) .join("") + "$", ); } /** * @param {any} o * @param {ValidationError} [err] * @return {o is CastStringTemplateArgsToTemplate} */ check(o, err) { const c = this._r.exec(o) != null; /* c8 ignore next */ !c && err?.extend(null, this._r.toString(), o.toString(), "String doesn't match string template."); return c; } } $constructedBy($StringTemplate); const isOptionalSymbol = Symbol("optional"); /** * @template {Schema} S * @extends Schema|undefined> */ class $Optional extends Schema$1 { /** * @param {S} shape */ constructor(shape) { super(); this.shape = shape; } /** * @param {any} o * @param {ValidationError} [err] * @return {o is (Unwrap|undefined)} */ check(o, err) { const c = o === undefined || this.shape.check(o); /* c8 ignore next */ !c && err?.extend(null, "undefined (optional)", "()"); return c; } get [isOptionalSymbol]() { return true; } } const $$optional = $constructedBy($Optional); /** * @extends Schema */ class $Never extends Schema$1 { /** * @param {any} _o * @param {ValidationError} [err] * @return {_o is never} */ check(_o, err) { /* c8 ignore next */ err?.extend(null, "never", typeof _o); return false; } } $constructedBy($Never); /** * @template {{ [key: string|symbol|number]: Schema }} S * @typedef {{ [Key in keyof S as S[Key] extends $Optional> ? Key : never]?: S[Key] extends $Optional> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional> ? never : Key]: S[Key] extends Schema ? Type : never }} $ObjectToType */ /** * @template {{[key:string|symbol|number]: Schema}} S * @extends {Schema<$ObjectToType>} */ class $Object extends Schema$1 { /** * @param {S} shape * @param {boolean} partial */ constructor(shape, partial = false) { super(); /** * @type {S} */ this.shape = shape; this._isPartial = partial; } static _dilutes = true; /** * @type {Schema>>} */ get partial() { return new $Object(this.shape, true); } /** * @param {any} o * @param {ValidationError} err * @return {o is $ObjectToType} */ check(o, err) { if (o == null) { /* c8 ignore next */ err?.extend(null, "object", "null"); return false; } return every(this.shape, (vv, vk) => { const c = (this._isPartial && !hasProperty$1(o, vk)) || vv.check(o[vk], err); !c && err?.extend(vk.toString(), vv.toString(), typeof o[vk], "Object property does not match"); return c; }); } } /** * @template S * @typedef {Schema<{ [Key in keyof S as S[Key] extends $Optional> ? Key : never]?: S[Key] extends $Optional> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional> ? never : Key]: S[Key] extends Schema ? Type : never }>} _ObjectDefToSchema */ // I used an explicit type annotation instead of $ObjectToType, so that the user doesn't see the // weird type definitions when inspecting type definions. /** * @template {{ [key:string|symbol|number]: Schema }} S * @param {S} def * @return {_ObjectDefToSchema extends Schema ? Schema<{ [K in keyof S]: S[K] }> : never} */ const $object = (def) => /** @type {any} */ (new $Object(def)); const $$object = $constructedBy($Object); /** * @type {Schema<{[key:string]: any}>} */ const $objectAny = $custom((o) => o != null && (o.constructor === Object || o.constructor == null)); /** * @template {Schema} Keys * @template {Schema} Values * @extends {Schema<{ [key in Unwrap]: Unwrap }>} */ class $Record extends Schema$1 { /** * @param {Keys} keys * @param {Values} values */ constructor(keys, values) { super(); this.shape = { keys, values, }; } /** * @param {any} o * @param {ValidationError} err * @return {o is { [key in Unwrap]: Unwrap }} */ check(o, err) { return ( o != null && every(o, (vv, vk) => { const ck = this.shape.keys.check(vk, err); /* c8 ignore next */ !ck && err?.extend(vk + "", "Record", typeof o, ck ? "Key doesn't match schema" : "Value doesn't match value"); return ck && this.shape.values.check(vv, err); }) ); } } /** * @template {Schema} Keys * @template {Schema} Values * @param {Keys} keys * @param {Values} values * @return {CastToSchema<$Record>} */ const $record = (keys, values) => new $Record(keys, values); const $$record = $constructedBy($Record); /** * @template {Schema[]} S * @extends {Schema<{ [Key in keyof S]: S[Key] extends Schema ? Type : never }>} */ class $Tuple extends Schema$1 { /** * @param {S} shape */ constructor(shape) { super(); this.shape = shape; } /** * @param {any} o * @param {ValidationError} err * @return {o is { [K in keyof S]: S[K] extends Schema ? Type : never }} */ check(o, err) { return ( o != null && every(this.shape, (vv, vk) => { const c = /** @type {Schema} */ (vv).check(o[vk], err); /* c8 ignore next */ !c && err?.extend(vk.toString(), "Tuple", typeof vv); return c; }) ); } } /** * @template {Array>} T * @param {T} def * @return {CastToSchema<$Tuple>} */ const $tuple = (...def) => new $Tuple(def); $constructedBy($Tuple); /** * @template {Schema} S * @extends {Schema ? T : never>>} */ class $Array extends Schema$1 { /** * @param {Array} v */ constructor(v) { super(); /** * @type {Schema ? T : never>} */ this.shape = v.length === 1 ? v[0] : new $Union(v); } /** * @param {any} o * @param {ValidationError} [err] * @return {o is Array ? T : never>} o */ check(o, err) { const c = isArray(o) && every$1(o, (oi) => this.shape.check(oi)); /* c8 ignore next */ !c && err?.extend(null, "Array", ""); return c; } } /** * @template {Array>} T * @param {T} def * @return {Schema> ? S : never>>} */ const $array = (...def) => new $Array(def); const $$array = $constructedBy($Array); /** * @type {Schema>} */ const $arrayAny = $custom((o) => isArray(o)); /** * @template T * @extends {Schema} */ class $InstanceOf extends Schema$1 { /** * @param {new (...args:any) => T} constructor * @param {((o:T) => boolean)|null} check */ constructor(constructor, check) { super(); this.shape = constructor; this._c = check; } /** * @param {any} o * @param {ValidationError} err * @return {o is T} */ check(o, err) { const c = o instanceof this.shape && (this._c == null || this._c(o)); /* c8 ignore next */ !c && err?.extend(null, this.shape.name, o?.constructor.name); return c; } } /** * @template T * @param {new (...args:any) => T} c * @param {((o:T) => boolean)|null} check * @return {Schema} */ const $instanceOf = (c, check = null) => new $InstanceOf(c, check); $constructedBy($InstanceOf); const $$schema = $instanceOf(Schema$1); /** * @template {Schema[]} Args * @typedef {(...args:UnwrapArray>)=>Unwrap>} _LArgsToLambdaDef */ /** * @template {Array>} Args * @extends {Schema<_LArgsToLambdaDef>} */ class $Lambda extends Schema$1 { /** * @param {Args} args */ constructor(args) { super(); this.len = args.length - 1; this.args = $tuple(...args.slice(-1)); this.res = args[this.len]; } /** * @param {any} f * @param {ValidationError} err * @return {f is _LArgsToLambdaDef} */ check(f, err) { const c = f.constructor === Function && f.length <= this.len; /* c8 ignore next */ !c && err?.extend(null, "function", typeof f); return c; } } const $$lambda = $constructedBy($Lambda); /** * @type {Schema} */ const $function = $custom((o) => typeof o === "function"); /** * @template {Array>} T * @extends {Schema>>} */ class $Intersection extends Schema$1 { /** * @param {T} v */ constructor(v) { super(); /** * @type {T} */ this.shape = v; } /** * @param {any} o * @param {ValidationError} [err] * @return {o is Intersect>} */ check(o, err) { // @ts-ignore const c = every$1(this.shape, (check) => check.check(o, err)); /* c8 ignore next */ !c && err?.extend(null, "Intersectinon", typeof o); return c; } } $constructedBy($Intersection, (o) => o.shape.length > 0); // Intersection with length=0 is considered "any" /** * @template S * @extends {Schema} */ class $Union extends Schema$1 { static _dilutes = true; /** * @param {Array>} v */ constructor(v) { super(); this.shape = v; } /** * @param {any} o * @param {ValidationError} [err] * @return {o is S} */ check(o, err) { const c = some(this.shape, (vv) => vv.check(o, err)); err?.extend(null, "Union", typeof o); return c; } } /** * @template {Array} T * @param {T} schemas * @return {CastToSchema<$Union>>>} */ const $union = (...schemas) => schemas.findIndex(($s) => $$union.check($s)) >= 0 ? $union( ...schemas .map(($s) => $$3($s)) .map(($s) => ($$union.check($s) ? $s.shape : [$s])) .flat(1), ) : schemas.length === 1 ? schemas[0] : new $Union(schemas); const $$union = /** @type {Schema<$Union>} */ ($constructedBy($Union)); const _t$2 = () => true; /** * @type {Schema} */ const $any = $custom(_t$2); const $$any = /** @type {Schema>} */ ($constructedBy($Custom, (o) => o.shape === _t$2)); /** * @type {Schema} */ const $bigint = $custom((o) => typeof o === "bigint"); const $$bigint = /** @type {Schema>} */ ($custom((o) => o === $bigint)); /** * @type {Schema} */ const $symbol = $custom((o) => typeof o === "symbol"); /** @type {Schema>} */ ($custom((o) => o === $symbol)); /** * @type {Schema} */ const $number = $custom((o) => typeof o === "number"); const $$number = /** @type {Schema>} */ ($custom((o) => o === $number)); /** * @type {Schema} */ const $string = $custom((o) => typeof o === "string"); const $$string = /** @type {Schema>} */ ($custom((o) => o === $string)); /** * @type {Schema} */ const $boolean = $custom((o) => typeof o === "boolean"); const $$boolean = /** @type {Schema>} */ ($custom((o) => o === $boolean)); /** * @type {Schema} */ const $undefined = $literal(undefined); /** @type {Schema>} */ ($constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === undefined)); /** * @type {Schema} */ $literal(undefined); const $null = $literal(null); const $$null = /** @type {Schema>} */ ($constructedBy($Literal, (o) => o.shape.length === 1 && o.shape[0] === null)); $constructedBy(Uint8Array); /** @type {Schema>} */ ($constructedBy($ConstructedBy, (o) => o.shape === Uint8Array)); /** * @type {Schema} */ const $primitive = $union($number, $string, $null, $undefined, $bigint, $boolean, $symbol); /** * @typedef {JSON[]} JSONArray */ /** * @typedef {Primitive|JSONArray|{ [key:string]:JSON }} JSON */ /** * @type {Schema} */ ( () => { const $jsonArr = /** @type {$Array<$any>} */ ($array($any)); const $jsonRecord = /** @type {$Record<$string,$any>} */ ($record($string, $any)); const $json = $union($number, $string, $null, $boolean, $jsonArr, $jsonRecord); $jsonArr.shape = $json; $jsonRecord.shape.values = $json; return $json; } )(); /** * @template {any} IN * @typedef {IN extends Schema ? IN * : (IN extends string|number|boolean|null ? Schema * : (IN extends new (...args:any[])=>any ? Schema> * : (IN extends any[] ? Schema<{ [K in keyof IN]: Unwrap> }[number]> * : (IN extends object ? (_ObjectDefToSchema<{[K in keyof IN]:ReadSchema}> extends Schema ? Schema<{ [K in keyof S]: S[K] }> : never) * : never) * ) * ) * ) * } ReadSchemaOld */ /** * @template {any} IN * @typedef {[Extract>,Extract,Extractany>,Extract,Extract|string|number|boolean|null|(new (...args:any[])=>any)|any[]>,object>] extends [infer Schemas, infer Primitives, infer Constructors, infer Arrs, infer Obj] * ? Schema< * (Schemas extends Schema ? S : never) * | Primitives * | (Constructors extends new (...args:any[])=>any ? InstanceType : never) * | (Arrs extends any[] ? { [K in keyof Arrs]: Unwrap> }[number] : never) * | (Obj extends object ? Unwrap<(_ObjectDefToSchema<{[K in keyof Obj]:ReadSchema}> extends Schema ? Schema<{ [K in keyof S]: S[K] }> : never)> : never)> * : never * } ReadSchema */ /** * @typedef {ReadSchema<{x:42}|{y:99}|Schema|[1,2,{}]>} Q */ /** * @template IN * @param {IN} o * @return {ReadSchema} */ const $$3 = (o) => { if ($$schema.check(o)) { return /** @type {any} */ (o); } else if ($objectAny.check(o)) { /** * @type {any} */ const o2 = {}; for (const k in o) { o2[k] = $$3(o[k]); } return /** @type {any} */ ($object(o2)); } else if ($arrayAny.check(o)) { return /** @type {any} */ ($union(...o.map($$3))); } else if ($primitive.check(o)) { return /** @type {any} */ ($literal(o)); } else if ($function.check(o)) { return /** @type {any} */ ($constructedBy(/** @type {any} */ (o))); } /* c8 ignore next */ unexpectedCase(); }; /* c8 ignore start */ /** * Assert that a variable is of this specific type. * The assertion check is only performed in non-production environments. * * @type {(o:any,schema:Schema) => asserts o is T} */ const assert = production ? () => {} : (o, schema) => { const err = new ValidationError(); if (!schema.check(o, err)) { throw create$7(`Expected value to be of type ${schema.constructor.name}.\n${err.toString()}`); } }; /* c8 ignore end */ /** * @template In * @template Out * @typedef {{ if: Schema, h: (o:In,state?:any)=>Out }} Pattern */ /** * @template {Pattern} P * @template In * @typedef {ReturnType>['h']>} PatternMatchResult */ /** * @todo move this to separate library * @template {any} [State=undefined] * @template {Pattern} [Patterns=never] */ class PatternMatcher { /** * @param {Schema} [$state] */ constructor($state) { /** * @type {Array} */ this.patterns = []; this.$state = $state; } /** * @template P * @template R * @param {P} pattern * @param {(o:NoInfer>>,s:State)=>R} handler * @return {PatternMatcher>,R>>} */ if(pattern, handler) { // @ts-ignore this.patterns.push({ if: $$3(pattern), h: handler }); // @ts-ignore return this; } /** * @template R * @param {(o:any,s:State)=>R} h */ else(h) { return this.if($any, h); } /** * @return {State extends undefined * ? >(o:In,state?:undefined)=>PatternMatchResult * : >(o:In,state:State)=>PatternMatchResult} */ done() { // @ts-ignore return /** @type {any} */ (o, s) => { for (let i = 0; i < this.patterns.length; i++) { const p = this.patterns[i]; if (p.if.check(o)) { // @ts-ignore return p.h(o, s); } } throw create$7("Unhandled pattern"); }; } } /** * @template [State=undefined] * @param {State} [state] * @return {PatternMatcher>>} */ const match = (state) => new PatternMatcher(/** @type {any} */ (state)); /** * Helper function to generate a (non-exhaustive) sample set from a gives schema. * * @type {(o:T,gen:prng.PRNG)=>T} */ const _random = /** @type {any} */ ( match(/** @type {Schema} */ ($any)) .if($$number, (_o, gen) => int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER)) .if($$string, (_o, gen) => word(gen)) .if($$boolean, (_o, gen) => bool(gen)) .if($$bigint, (_o, gen) => BigInt(int53(gen, MIN_SAFE_INTEGER, MAX_SAFE_INTEGER))) .if($$union, (o, gen) => random(gen, oneOf(gen, o.shape))) .if($$object, (o, gen) => { /** * @type {any} */ const res = {}; for (const k in o.shape) { let prop = o.shape[k]; if ($$optional.check(prop)) { if (bool(gen)) { continue; } prop = prop.shape; } res[k] = _random(prop, gen); } return res; }) .if($$array, (o, gen) => { const arr = []; const n = int32(gen, 0, 42); for (let i = 0; i < n; i++) { arr.push(random(gen, o.shape)); } return arr; }) .if($$literal, (o, gen) => { return oneOf(gen, o.shape); }) .if($$null, (o, gen) => { return null; }) .if($$lambda, (o, gen) => { const res = random(gen, o.res); return () => res; }) .if($$any, (o, gen) => random(gen, oneOf(gen, [$number, $string, $null, $undefined, $bigint, $boolean, $array($number), $record($union("a", "b", "c"), $number)]))) .if($$record, (o, gen) => { /** * @type {any} */ const res = {}; const keysN = int53(gen, 0, 3); for (let i = 0; i < keysN; i++) { const key = random(gen, o.shape.keys); const val = random(gen, o.shape.values); res[key] = val; } return res; }) .done() ); /** * @template S * @param {prng.PRNG} gen * @param {S} schema * @return {Unwrap>} */ const random = (gen, schema) => /** @type {any} */ (_random($$3(schema), gen)); /* eslint-env browser */ /* c8 ignore start */ /** * @type {Document} */ const doc = /** @type {Document} */ (typeof document !== "undefined" ? document : {}); /** * @type {$.Schema} */ $custom((el) => el.nodeType === DOCUMENT_FRAGMENT_NODE); /** @type {DOMParser} */ (typeof DOMParser !== "undefined" ? new DOMParser() : null); /** * @type {$.Schema} */ $custom((el) => el.nodeType === ELEMENT_NODE); /** * @type {$.Schema} */ $custom((el) => el.nodeType === TEXT_NODE); /** * @param {Map} m * @return {string} */ const mapToStyleString = (m) => map$3(m, (value, key) => `${key}:${value};`).join(""); const ELEMENT_NODE = doc.ELEMENT_NODE; const TEXT_NODE = doc.TEXT_NODE; const DOCUMENT_NODE = doc.DOCUMENT_NODE; const DOCUMENT_FRAGMENT_NODE = doc.DOCUMENT_FRAGMENT_NODE; /** * @type {$.Schema} */ $custom((el) => el.nodeType === DOCUMENT_NODE); /* c8 ignore stop */ /* global requestIdleCallback, requestAnimationFrame, cancelIdleCallback, cancelAnimationFrame */ /** * @typedef {Object} TimeoutObject * @property {function} TimeoutObject.destroy */ /** * @param {function(number):void} clearFunction */ const createTimeoutClass = (clearFunction) => class TT { /** * @param {number} timeoutId */ constructor(timeoutId) { this._ = timeoutId; } destroy() { clearFunction(this._); } }; const Timeout = createTimeoutClass(clearTimeout); /** * @param {number} timeout * @param {function} callback * @return {TimeoutObject} */ const timeout = (timeout, callback) => new Timeout(setTimeout(callback, timeout)); /** * Utility module to work with EcmaScript Symbols. * * @module symbol */ /** * Return fresh symbol. */ const create$4 = Symbol; const BOLD = create$4(); const UNBOLD = create$4(); const BLUE = create$4(); const GREY = create$4(); const GREEN = create$4(); const RED = create$4(); const PURPLE = create$4(); const ORANGE = create$4(); const UNCOLOR = create$4(); /* c8 ignore start */ /** * @param {Array} args * @return {Array} */ const computeNoColorLoggingArgs = (args) => { if (args.length === 1 && args[0]?.constructor === Function) { args = /** @type {Array} */ (/** @type {[function]} */ (args)[0]()); } const strBuilder = []; const logArgs = []; // try with formatting until we find something unsupported let i = 0; for (; i < args.length; i++) { const arg = args[i]; if (arg === undefined) { break; } else if (arg.constructor === String || arg.constructor === Number) { strBuilder.push(arg); } else if (arg.constructor === Object) { break; } } if (i > 0) { // create logArgs with what we have so far logArgs.push(strBuilder.join("")); } // append the rest for (; i < args.length; i++) { const arg = args[i]; if (!(arg instanceof Symbol)) { logArgs.push(arg); } } return logArgs; }; /* c8 ignore stop */ /** * Isomorphic logging module with support for colors! * * @module logging */ /** * @type {Object>} */ const _browserStyleMap = { [BOLD]: create$5("font-weight", "bold"), [UNBOLD]: create$5("font-weight", "normal"), [BLUE]: create$5("color", "blue"), [GREEN]: create$5("color", "green"), [GREY]: create$5("color", "grey"), [RED]: create$5("color", "red"), [PURPLE]: create$5("color", "purple"), [ORANGE]: create$5("color", "orange"), // not well supported in chrome when debugging node with inspector - TODO: deprecate [UNCOLOR]: create$5("color", "black"), }; /** * @param {Array} args * @return {Array} */ /* c8 ignore start */ const computeBrowserLoggingArgs = (args) => { if (args.length === 1 && args[0]?.constructor === Function) { args = /** @type {Array} */ (/** @type {[function]} */ (args)[0]()); } const strBuilder = []; const styles = []; const currentStyle = create$9(); /** * @type {Array} */ let logArgs = []; // try with formatting until we find something unsupported let i = 0; for (; i < args.length; i++) { const arg = args[i]; // @ts-ignore const style = _browserStyleMap[arg]; if (style !== undefined) { currentStyle.set(style.left, style.right); } else { if (arg === undefined) { break; } if (arg.constructor === String || arg.constructor === Number) { const style = mapToStyleString(currentStyle); if (i > 0 || style.length > 0) { strBuilder.push("%c" + arg); styles.push(style); } else { strBuilder.push(arg); } } else { break; } } } if (i > 0) { // create logArgs with what we have so far logArgs = styles; logArgs.unshift(strBuilder.join("")); } // append the rest for (; i < args.length; i++) { const arg = args[i]; if (!(arg instanceof Symbol)) { logArgs.push(arg); } } return logArgs; }; /* c8 ignore stop */ /* c8 ignore start */ const computeLoggingArgs = supportsColor ? computeBrowserLoggingArgs : computeNoColorLoggingArgs; /* c8 ignore stop */ /** * @param {Array} args */ const print = (...args) => { console.log(...computeLoggingArgs(args)); /* c8 ignore next */ vconsoles.forEach((vc) => vc.print(args)); }; /* c8 ignore start */ /** * @param {Array} args */ const warn = (...args) => { console.warn(...computeLoggingArgs(args)); args.unshift(ORANGE); vconsoles.forEach((vc) => vc.print(args)); }; const vconsoles = create$8(); /** * Utility module to create and manipulate Iterators. * * @module iterator */ /** * @template T * @param {function():IteratorResult} next * @return {IterableIterator} */ const createIterator = (next) => ({ /** * @return {IterableIterator} */ [Symbol.iterator]() { return this; }, // @ts-ignore next, }); /** * @template T * @param {Iterator} iterator * @param {function(T):boolean} filter */ const iteratorFilter = (iterator, filter) => createIterator(() => { let res; do { res = iterator.next(); } while (!res.done && !filter(res.value)); return res; }); /** * @template T,M * @param {Iterator} iterator * @param {function(T):M} fmap */ const iteratorMap = (iterator, fmap) => createIterator(() => { const { done, value } = iterator.next(); return { done, value: done ? undefined : fmap(value) }; }); class DeleteItem { /** * @param {number} clock * @param {number} len */ constructor(clock, len) { /** * @type {number} */ this.clock = clock; /** * @type {number} */ this.len = len; } } /** * We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed. * - When created in a transaction, it must only be accessed after sorting, and merging * - This DeleteSet is send to other clients * - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore * - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged. */ class DeleteSet { constructor() { /** * @type {Map>} */ this.clients = new Map(); } } /** * Iterate over all structs that the DeleteSet gc's. * * @param {Transaction} transaction * @param {DeleteSet} ds * @param {function(GC|Item):void} f * * @function */ const iterateDeletedStructs = (transaction, ds, f) => ds.clients.forEach((deletes, clientid) => { const structs = /** @type {Array} */ (transaction.doc.store.clients.get(clientid)); if (structs != null) { const lastStruct = structs[structs.length - 1]; const clockState = lastStruct.id.clock + lastStruct.length; for (let i = 0, del = deletes[i]; i < deletes.length && del.clock < clockState; del = deletes[++i]) { iterateStructs(transaction, structs, del.clock, del.len, f); } } }); /** * @param {Array} dis * @param {number} clock * @return {number|null} * * @private * @function */ const findIndexDS = (dis, clock) => { let left = 0; let right = dis.length - 1; while (left <= right) { const midindex = floor$1((left + right) / 2); const mid = dis[midindex]; const midclock = mid.clock; if (midclock <= clock) { if (clock < midclock + mid.len) { return midindex; } left = midindex + 1; } else { right = midindex - 1; } } return null; }; /** * @param {DeleteSet} ds * @param {ID} id * @return {boolean} * * @private * @function */ const isDeleted = (ds, id) => { const dis = ds.clients.get(id.client); return dis !== undefined && findIndexDS(dis, id.clock) !== null; }; /** * @param {DeleteSet} ds * * @private * @function */ const sortAndMergeDeleteSet = (ds) => { ds.clients.forEach((dels) => { dels.sort((a, b) => a.clock - b.clock); // merge items without filtering or splicing the array // i is the current pointer // j refers to the current insert position for the pointed item // try to merge dels[i] into dels[j-1] or set dels[j]=dels[i] let i, j; for (i = 1, j = 1; i < dels.length; i++) { const left = dels[j - 1]; const right = dels[i]; if (left.clock + left.len >= right.clock) { dels[j - 1] = new DeleteItem(left.clock, max$1(left.len, right.clock + right.len - left.clock)); } else { if (j < i) { dels[j] = right; } j++; } } dels.length = j; }); }; /** * @param {Array} dss * @return {DeleteSet} A fresh DeleteSet */ const mergeDeleteSets = (dss) => { const merged = new DeleteSet(); for (let dssI = 0; dssI < dss.length; dssI++) { dss[dssI].clients.forEach((delsLeft, client) => { if (!merged.clients.has(client)) { // Write all missing keys from current ds and all following. // If merged already contains `client` current ds has already been added. /** * @type {Array} */ const dels = delsLeft.slice(); for (let i = dssI + 1; i < dss.length; i++) { appendTo(dels, dss[i].clients.get(client) || []); } merged.clients.set(client, dels); } }); } sortAndMergeDeleteSet(merged); return merged; }; /** * @param {DeleteSet} ds * @param {number} client * @param {number} clock * @param {number} length * * @private * @function */ const addToDeleteSet = (ds, client, clock, length) => { setIfUndefined(ds.clients, client, () => /** @type {Array} */ ([])).push(new DeleteItem(clock, length)); }; const createDeleteSet = () => new DeleteSet(); /** * @param {StructStore} ss * @return {DeleteSet} Merged and sorted DeleteSet * * @private * @function */ const createDeleteSetFromStructStore = (ss) => { const ds = createDeleteSet(); ss.clients.forEach((structs, client) => { /** * @type {Array} */ const dsitems = []; for (let i = 0; i < structs.length; i++) { const struct = structs[i]; if (struct.deleted) { const clock = struct.id.clock; let len = struct.length; if (i + 1 < structs.length) { for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) { len += next.length; } } dsitems.push(new DeleteItem(clock, len)); } } if (dsitems.length > 0) { ds.clients.set(client, dsitems); } }); return ds; }; /** * @param {DSEncoderV1 | DSEncoderV2} encoder * @param {DeleteSet} ds * * @private * @function */ const writeDeleteSet = (encoder, ds) => { writeVarUint(encoder.restEncoder, ds.clients.size); // Ensure that the delete set is written in a deterministic order from(ds.clients.entries()) .sort((a, b) => b[0] - a[0]) .forEach(([client, dsitems]) => { encoder.resetDsCurVal(); writeVarUint(encoder.restEncoder, client); const len = dsitems.length; writeVarUint(encoder.restEncoder, len); for (let i = 0; i < len; i++) { const item = dsitems[i]; encoder.writeDsClock(item.clock); encoder.writeDsLen(item.len); } }); }; /** * @param {DSDecoderV1 | DSDecoderV2} decoder * @return {DeleteSet} * * @private * @function */ const readDeleteSet = (decoder) => { const ds = new DeleteSet(); const numClients = readVarUint(decoder.restDecoder); for (let i = 0; i < numClients; i++) { decoder.resetDsCurVal(); const client = readVarUint(decoder.restDecoder); const numberOfDeletes = readVarUint(decoder.restDecoder); if (numberOfDeletes > 0) { const dsField = setIfUndefined(ds.clients, client, () => /** @type {Array} */ ([])); for (let i = 0; i < numberOfDeletes; i++) { dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen())); } } } return ds; }; /** * @todo YDecoder also contains references to String and other Decoders. Would make sense to exchange YDecoder.toUint8Array for YDecoder.DsToUint8Array().. */ /** * @param {DSDecoderV1 | DSDecoderV2} decoder * @param {Transaction} transaction * @param {StructStore} store * @return {Uint8Array|null} Returns a v2 update containing all deletes that couldn't be applied yet; or null if all deletes were applied successfully. * * @private * @function */ const readAndApplyDeleteSet = (decoder, transaction, store) => { const unappliedDS = new DeleteSet(); const numClients = readVarUint(decoder.restDecoder); for (let i = 0; i < numClients; i++) { decoder.resetDsCurVal(); const client = readVarUint(decoder.restDecoder); const numberOfDeletes = readVarUint(decoder.restDecoder); const structs = store.clients.get(client) || []; const state = getState(store, client); for (let i = 0; i < numberOfDeletes; i++) { const clock = decoder.readDsClock(); const clockEnd = clock + decoder.readDsLen(); if (clock < state) { if (state < clockEnd) { addToDeleteSet(unappliedDS, client, state, clockEnd - state); } let index = findIndexSS(structs, clock); /** * We can ignore the case of GC and Delete structs, because we are going to skip them * @type {Item} */ // @ts-ignore let struct = structs[index]; // split the first item if necessary if (!struct.deleted && struct.id.clock < clock) { structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); index++; // increase we now want to use the next struct } while (index < structs.length) { // @ts-ignore struct = structs[index++]; if (struct.id.clock < clockEnd) { if (!struct.deleted) { if (clockEnd < struct.id.clock + struct.length) { structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock)); } struct.delete(transaction); } } else { break; } } } else { addToDeleteSet(unappliedDS, client, clock, clockEnd - clock); } } } if (unappliedDS.clients.size > 0) { const ds = new UpdateEncoderV2(); writeVarUint(ds.restEncoder, 0); // encode 0 structs writeDeleteSet(ds, unappliedDS); return ds.toUint8Array(); } return null; }; /** * @module Y */ const generateNewClientId = uint32; /** * @typedef {Object} DocOpts * @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true) * @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item. * @property {string} [DocOpts.guid] Define a globally unique identifier for this document * @property {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection. * @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well. * @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically. * @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load() */ /** * @typedef {Object} DocEvents * @property {function(Doc):void} DocEvents.destroy * @property {function(Doc):void} DocEvents.load * @property {function(boolean, Doc):void} DocEvents.sync * @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.update * @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.updateV2 * @property {function(Doc):void} DocEvents.beforeAllTransactions * @property {function(Transaction, Doc):void} DocEvents.beforeTransaction * @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls * @property {function(Transaction, Doc):void} DocEvents.afterTransaction * @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup * @property {function(Doc, Array):void} DocEvents.afterAllTransactions * @property {function({ loaded: Set, added: Set, removed: Set }, Doc, Transaction):void} DocEvents.subdocs */ /** * A Yjs instance handles the state of shared data. * @extends ObservableV2 */ class Doc extends ObservableV2 { /** * @param {DocOpts} opts configuration */ constructor({ guid = uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) { super(); this.gc = gc; this.gcFilter = gcFilter; this.clientID = generateNewClientId(); this.guid = guid; this.collectionid = collectionid; /** * @type {Map>>} */ this.share = new Map(); this.store = new StructStore(); /** * @type {Transaction | null} */ this._transaction = null; /** * @type {Array} */ this._transactionCleanups = []; /** * @type {Set} */ this.subdocs = new Set(); /** * If this document is a subdocument - a document integrated into another document - then _item is defined. * @type {Item?} */ this._item = null; this.shouldLoad = shouldLoad; this.autoLoad = autoLoad; this.meta = meta; /** * This is set to true when the persistence provider loaded the document from the database or when the `sync` event fires. * Note that not all providers implement this feature. Provider authors are encouraged to fire the `load` event when the doc content is loaded from the database. * * @type {boolean} */ this.isLoaded = false; /** * This is set to true when the connection provider has successfully synced with a backend. * Note that when using peer-to-peer providers this event may not provide very useful. * Also note that not all providers implement this feature. Provider authors are encouraged to fire * the `sync` event when the doc has been synced (with `true` as a parameter) or if connection is * lost (with false as a parameter). */ this.isSynced = false; this.isDestroyed = false; /** * Promise that resolves once the document has been loaded from a persistence provider. */ this.whenLoaded = create$6((resolve) => { this.on("load", () => { this.isLoaded = true; resolve(this); }); }); const provideSyncedPromise = () => create$6((resolve) => { /** * @param {boolean} isSynced */ const eventHandler = (isSynced) => { if (isSynced === undefined || isSynced === true) { this.off("sync", eventHandler); resolve(); } }; this.on("sync", eventHandler); }); this.on("sync", (isSynced) => { if (isSynced === false && this.isSynced) { this.whenSynced = provideSyncedPromise(); } this.isSynced = isSynced === undefined || isSynced === true; if (this.isSynced && !this.isLoaded) { this.emit("load", [this]); } }); /** * Promise that resolves once the document has been synced with a backend. * This promise is recreated when the connection is lost. * Note the documentation about the `isSynced` property. */ this.whenSynced = provideSyncedPromise(); } /** * Notify the parent document that you request to load data into this subdocument (if it is a subdocument). * * `load()` might be used in the future to request any provider to load the most current data. * * It is safe to call `load()` multiple times. */ load() { const item = this._item; if (item !== null && !this.shouldLoad) { transact( /** @type {any} */ (item.parent).doc, (transaction) => { transaction.subdocsLoaded.add(this); }, null, true, ); } this.shouldLoad = true; } getSubdocs() { return this.subdocs; } getSubdocGuids() { return new Set(from(this.subdocs).map((doc) => doc.guid)); } /** * Changes that happen inside of a transaction are bundled. This means that * the observer fires _after_ the transaction is finished and that all changes * that happened inside of the transaction are sent as one message to the * other peers. * * @template T * @param {function(Transaction):T} f The function that should be executed as a transaction * @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin * @return T * * @public */ transact(f, origin = null) { return transact(this, f, origin); } /** * Define a shared data type. * * Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result * and do not overwrite each other. I.e. * `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)` * * After this method is called, the type is also available on `ydoc.share.get(name)`. * * *Best Practices:* * Define all types right after the Y.Doc instance is created and store them in a separate object. * Also use the typed methods `getText(name)`, `getArray(name)`, .. * * @template {typeof AbstractType} Type * @example * const ydoc = new Y.Doc(..) * const appState = { * document: ydoc.getText('document') * comments: ydoc.getArray('comments') * } * * @param {string} name * @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ... * @return {InstanceType} The created type. Constructed with TypeConstructor * * @public */ get(name, TypeConstructor = /** @type {any} */ (AbstractType)) { const type = setIfUndefined(this.share, name, () => { // @ts-ignore const t = new TypeConstructor(); t._integrate(this, null); return t; }); const Constr = type.constructor; if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) { if (Constr === AbstractType) { // @ts-ignore const t = new TypeConstructor(); t._map = type._map; type._map.forEach( /** @param {Item?} n */ (n) => { for (; n !== null; n = n.left) { // @ts-ignore n.parent = t; } }, ); t._start = type._start; for (let n = t._start; n !== null; n = n.right) { n.parent = t; } t._length = type._length; this.share.set(name, t); t._integrate(this, null); return /** @type {InstanceType} */ (t); } else { throw new Error(`Type with the name ${name} has already been defined with a different constructor`); } } return /** @type {InstanceType} */ (type); } /** * @template T * @param {string} [name] * @return {YArray} * * @public */ getArray(name = "") { return /** @type {YArray} */ (this.get(name, YArray)); } /** * @param {string} [name] * @return {YText} * * @public */ getText(name = "") { return this.get(name, YText); } /** * @template T * @param {string} [name] * @return {YMap} * * @public */ getMap(name = "") { return /** @type {YMap} */ (this.get(name, YMap)); } /** * @param {string} [name] * @return {YXmlElement} * * @public */ getXmlElement(name = "") { return /** @type {YXmlElement<{[key:string]:string}>} */ (this.get(name, YXmlElement)); } /** * @param {string} [name] * @return {YXmlFragment} * * @public */ getXmlFragment(name = "") { return this.get(name, YXmlFragment); } /** * Converts the entire document into a js object, recursively traversing each yjs type * Doesn't log types that have not been defined (using ydoc.getType(..)). * * @deprecated Do not use this method and rather call toJSON directly on the shared types. * * @return {Object} */ toJSON() { /** * @type {Object} */ const doc = {}; this.share.forEach((value, key) => { doc[key] = value.toJSON(); }); return doc; } /** * Emit `destroy` event and unregister all event handlers. */ destroy() { this.isDestroyed = true; from(this.subdocs).forEach((subdoc) => subdoc.destroy()); const item = this._item; if (item !== null) { this._item = null; const content = /** @type {ContentDoc} */ (item.content); content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false }); content.doc._item = item; transact( /** @type {any} */ (item).parent.doc, (transaction) => { const doc = content.doc; if (!item.deleted) { transaction.subdocsAdded.add(doc); } transaction.subdocsRemoved.add(this); }, null, true, ); } // @ts-ignore this.emit("destroyed", [true]); // DEPRECATED! this.emit("destroy", [this]); super.destroy(); } } class DSDecoderV1 { /** * @param {decoding.Decoder} decoder */ constructor(decoder) { this.restDecoder = decoder; } resetDsCurVal() { // nop } /** * @return {number} */ readDsClock() { return readVarUint(this.restDecoder); } /** * @return {number} */ readDsLen() { return readVarUint(this.restDecoder); } } class UpdateDecoderV1 extends DSDecoderV1 { /** * @return {ID} */ readLeftID() { return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); } /** * @return {ID} */ readRightID() { return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder)); } /** * Read the next client id. * Use this in favor of readID whenever possible to reduce the number of objects created. */ readClient() { return readVarUint(this.restDecoder); } /** * @return {number} info An unsigned 8-bit integer */ readInfo() { return readUint8(this.restDecoder); } /** * @return {string} */ readString() { return readVarString(this.restDecoder); } /** * @return {boolean} isKey */ readParentInfo() { return readVarUint(this.restDecoder) === 1; } /** * @return {number} info An unsigned 8-bit integer */ readTypeRef() { return readVarUint(this.restDecoder); } /** * Write len of a struct - well suited for Opt RLE encoder. * * @return {number} len */ readLen() { return readVarUint(this.restDecoder); } /** * @return {any} */ readAny() { return readAny(this.restDecoder); } /** * @return {Uint8Array} */ readBuf() { return copyUint8Array(readVarUint8Array(this.restDecoder)); } /** * Legacy implementation uses JSON parse. We use any-decoding in v2. * * @return {any} */ readJSON() { return JSON.parse(readVarString(this.restDecoder)); } /** * @return {string} */ readKey() { return readVarString(this.restDecoder); } } class DSDecoderV2 { /** * @param {decoding.Decoder} decoder */ constructor(decoder) { /** * @private */ this.dsCurrVal = 0; this.restDecoder = decoder; } resetDsCurVal() { this.dsCurrVal = 0; } /** * @return {number} */ readDsClock() { this.dsCurrVal += readVarUint(this.restDecoder); return this.dsCurrVal; } /** * @return {number} */ readDsLen() { const diff = readVarUint(this.restDecoder) + 1; this.dsCurrVal += diff; return diff; } } class UpdateDecoderV2 extends DSDecoderV2 { /** * @param {decoding.Decoder} decoder */ constructor(decoder) { super(decoder); /** * List of cached keys. If the keys[id] does not exist, we read a new key * from stringEncoder and push it to keys. * * @type {Array} */ this.keys = []; readVarUint(decoder); // read feature flag - currently unused this.keyClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); this.clientDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); this.leftClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); this.rightClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder)); this.infoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); this.stringDecoder = new StringDecoder(readVarUint8Array(decoder)); this.parentInfoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8); this.typeRefDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); this.lenDecoder = new UintOptRleDecoder(readVarUint8Array(decoder)); } /** * @return {ID} */ readLeftID() { return new ID(this.clientDecoder.read(), this.leftClockDecoder.read()); } /** * @return {ID} */ readRightID() { return new ID(this.clientDecoder.read(), this.rightClockDecoder.read()); } /** * Read the next client id. * Use this in favor of readID whenever possible to reduce the number of objects created. */ readClient() { return this.clientDecoder.read(); } /** * @return {number} info An unsigned 8-bit integer */ readInfo() { return /** @type {number} */ (this.infoDecoder.read()); } /** * @return {string} */ readString() { return this.stringDecoder.read(); } /** * @return {boolean} */ readParentInfo() { return this.parentInfoDecoder.read() === 1; } /** * @return {number} An unsigned 8-bit integer */ readTypeRef() { return this.typeRefDecoder.read(); } /** * Write len of a struct - well suited for Opt RLE encoder. * * @return {number} */ readLen() { return this.lenDecoder.read(); } /** * @return {any} */ readAny() { return readAny(this.restDecoder); } /** * @return {Uint8Array} */ readBuf() { return readVarUint8Array(this.restDecoder); } /** * This is mainly here for legacy purposes. * * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. * * @return {any} */ readJSON() { return readAny(this.restDecoder); } /** * @return {string} */ readKey() { const keyClock = this.keyClockDecoder.read(); if (keyClock < this.keys.length) { return this.keys[keyClock]; } else { const key = this.stringDecoder.read(); this.keys.push(key); return key; } } } class DSEncoderV1 { constructor() { this.restEncoder = createEncoder(); } toUint8Array() { return toUint8Array(this.restEncoder); } resetDsCurVal() { // nop } /** * @param {number} clock */ writeDsClock(clock) { writeVarUint(this.restEncoder, clock); } /** * @param {number} len */ writeDsLen(len) { writeVarUint(this.restEncoder, len); } } class UpdateEncoderV1 extends DSEncoderV1 { /** * @param {ID} id */ writeLeftID(id) { writeVarUint(this.restEncoder, id.client); writeVarUint(this.restEncoder, id.clock); } /** * @param {ID} id */ writeRightID(id) { writeVarUint(this.restEncoder, id.client); writeVarUint(this.restEncoder, id.clock); } /** * Use writeClient and writeClock instead of writeID if possible. * @param {number} client */ writeClient(client) { writeVarUint(this.restEncoder, client); } /** * @param {number} info An unsigned 8-bit integer */ writeInfo(info) { writeUint8(this.restEncoder, info); } /** * @param {string} s */ writeString(s) { writeVarString(this.restEncoder, s); } /** * @param {boolean} isYKey */ writeParentInfo(isYKey) { writeVarUint(this.restEncoder, isYKey ? 1 : 0); } /** * @param {number} info An unsigned 8-bit integer */ writeTypeRef(info) { writeVarUint(this.restEncoder, info); } /** * Write len of a struct - well suited for Opt RLE encoder. * * @param {number} len */ writeLen(len) { writeVarUint(this.restEncoder, len); } /** * @param {any} any */ writeAny(any) { writeAny(this.restEncoder, any); } /** * @param {Uint8Array} buf */ writeBuf(buf) { writeVarUint8Array(this.restEncoder, buf); } /** * @param {any} embed */ writeJSON(embed) { writeVarString(this.restEncoder, JSON.stringify(embed)); } /** * @param {string} key */ writeKey(key) { writeVarString(this.restEncoder, key); } } class DSEncoderV2 { constructor() { this.restEncoder = createEncoder(); // encodes all the rest / non-optimized this.dsCurrVal = 0; } toUint8Array() { return toUint8Array(this.restEncoder); } resetDsCurVal() { this.dsCurrVal = 0; } /** * @param {number} clock */ writeDsClock(clock) { const diff = clock - this.dsCurrVal; this.dsCurrVal = clock; writeVarUint(this.restEncoder, diff); } /** * @param {number} len */ writeDsLen(len) { if (len === 0) { unexpectedCase(); } writeVarUint(this.restEncoder, len - 1); this.dsCurrVal += len; } } class UpdateEncoderV2 extends DSEncoderV2 { constructor() { super(); /** * @type {Map} */ this.keyMap = new Map(); /** * Refers to the next unique key-identifier to me used. * See writeKey method for more information. * * @type {number} */ this.keyClock = 0; this.keyClockEncoder = new IntDiffOptRleEncoder(); this.clientEncoder = new UintOptRleEncoder(); this.leftClockEncoder = new IntDiffOptRleEncoder(); this.rightClockEncoder = new IntDiffOptRleEncoder(); this.infoEncoder = new RleEncoder(writeUint8); this.stringEncoder = new StringEncoder(); this.parentInfoEncoder = new RleEncoder(writeUint8); this.typeRefEncoder = new UintOptRleEncoder(); this.lenEncoder = new UintOptRleEncoder(); } toUint8Array() { const encoder = createEncoder(); writeVarUint(encoder, 0); // this is a feature flag that we might use in the future writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array()); writeVarUint8Array(encoder, this.clientEncoder.toUint8Array()); writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array()); writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array()); writeVarUint8Array(encoder, toUint8Array(this.infoEncoder)); writeVarUint8Array(encoder, this.stringEncoder.toUint8Array()); writeVarUint8Array(encoder, toUint8Array(this.parentInfoEncoder)); writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array()); writeVarUint8Array(encoder, this.lenEncoder.toUint8Array()); // @note The rest encoder is appended! (note the missing var) writeUint8Array(encoder, toUint8Array(this.restEncoder)); return toUint8Array(encoder); } /** * @param {ID} id */ writeLeftID(id) { this.clientEncoder.write(id.client); this.leftClockEncoder.write(id.clock); } /** * @param {ID} id */ writeRightID(id) { this.clientEncoder.write(id.client); this.rightClockEncoder.write(id.clock); } /** * @param {number} client */ writeClient(client) { this.clientEncoder.write(client); } /** * @param {number} info An unsigned 8-bit integer */ writeInfo(info) { this.infoEncoder.write(info); } /** * @param {string} s */ writeString(s) { this.stringEncoder.write(s); } /** * @param {boolean} isYKey */ writeParentInfo(isYKey) { this.parentInfoEncoder.write(isYKey ? 1 : 0); } /** * @param {number} info An unsigned 8-bit integer */ writeTypeRef(info) { this.typeRefEncoder.write(info); } /** * Write len of a struct - well suited for Opt RLE encoder. * * @param {number} len */ writeLen(len) { this.lenEncoder.write(len); } /** * @param {any} any */ writeAny(any) { writeAny(this.restEncoder, any); } /** * @param {Uint8Array} buf */ writeBuf(buf) { writeVarUint8Array(this.restEncoder, buf); } /** * This is mainly here for legacy purposes. * * Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder. * * @param {any} embed */ writeJSON(embed) { writeAny(this.restEncoder, embed); } /** * Property keys are often reused. For example, in y-prosemirror the key `bold` might * occur very often. For a 3d application, the key `position` might occur very often. * * We cache these keys in a Map and refer to them via a unique number. * * @param {string} key */ writeKey(key) { const clock = this.keyMap.get(key); if (clock === undefined) { /** * @todo uncomment to introduce this feature finally * * Background. The ContentFormat object was always encoded using writeKey, but the decoder used to use readString. * Furthermore, I forgot to set the keyclock. So everything was working fine. * * However, this feature here is basically useless as it is not being used (it actually only consumes extra memory). * * I don't know yet how to reintroduce this feature.. * * Older clients won't be able to read updates when we reintroduce this feature. So this should probably be done using a flag. * */ // this.keyMap.set(key, this.keyClock) this.keyClockEncoder.write(this.keyClock++); this.stringEncoder.write(key); } else { this.keyClockEncoder.write(clock); } } } /** * @module encoding */ /* * We use the first five bits in the info flag for determining the type of the struct. * * 0: GC * 1: Item with Deleted content * 2: Item with JSON content * 3: Item with Binary content * 4: Item with String content * 5: Item with Embed content (for richtext content) * 6: Item with Format content (a formatting marker for richtext content) * 7: Item with Type */ /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Array} structs All structs by `client` * @param {number} client * @param {number} clock write structs starting with `ID(client,clock)` * * @function */ const writeStructs = (encoder, structs, client, clock) => { // write first id clock = max$1(clock, structs[0].id.clock); // make sure the first id exists const startNewStructs = findIndexSS(structs, clock); // write # encoded structs writeVarUint(encoder.restEncoder, structs.length - startNewStructs); encoder.writeClient(client); writeVarUint(encoder.restEncoder, clock); const firstStruct = structs[startNewStructs]; // write first struct with an offset firstStruct.write(encoder, clock - firstStruct.id.clock); for (let i = startNewStructs + 1; i < structs.length; i++) { structs[i].write(encoder, 0); } }; /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {StructStore} store * @param {Map} _sm * * @private * @function */ const writeClientsStructs = (encoder, store, _sm) => { // we filter all valid _sm entries into sm const sm = new Map(); _sm.forEach((clock, client) => { // only write if new structs are available if (getState(store, client) > clock) { sm.set(client, clock); } }); getStateVector(store).forEach((_clock, client) => { if (!_sm.has(client)) { sm.set(client, 0); } }); // write # states that were updated writeVarUint(encoder.restEncoder, sm.size); // Write items with higher client ids first // This heavily improves the conflict algorithm. from(sm.entries()) .sort((a, b) => b[0] - a[0]) .forEach(([client, clock]) => { writeStructs(encoder, /** @type {Array} */ (store.clients.get(client)), client, clock); }); }; /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from. * @param {Doc} doc * @return {Map }>} * * @private * @function */ const readClientsStructRefs = (decoder, doc) => { /** * @type {Map }>} */ const clientRefs = create$9(); const numOfStateUpdates = readVarUint(decoder.restDecoder); for (let i = 0; i < numOfStateUpdates; i++) { const numberOfStructs = readVarUint(decoder.restDecoder); /** * @type {Array} */ const refs = new Array(numberOfStructs); const client = decoder.readClient(); let clock = readVarUint(decoder.restDecoder); // const start = performance.now() clientRefs.set(client, { i: 0, refs }); for (let i = 0; i < numberOfStructs; i++) { const info = decoder.readInfo(); switch (BITS5 & info) { case 0: { // GC const len = decoder.readLen(); refs[i] = new GC(createID(client, clock), len); clock += len; break; } case 10: { // Skip Struct (nothing to apply) // @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing. const len = readVarUint(decoder.restDecoder); refs[i] = new Skip(createID(client, clock), len); clock += len; break; } default: { // Item with content /** * The optimized implementation doesn't use any variables because inlining variables is faster. * Below a non-optimized version is shown that implements the basic algorithm with * a few comments */ const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; // If parent = null and neither left nor right are defined, then we know that `parent` is child of `y` // and we read the next string as parentYKey. // It indicates how we store/retrieve parent from `y.share` // @type {string|null} const struct = new Item$1( createID(client, clock), null, // left (info & BIT8) === BIT8 ? decoder.readLeftID() : null, // origin null, // right (info & BIT7) === BIT7 ? decoder.readRightID() : null, // right origin cantCopyParentInfo ? decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID() : null, // parent cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, // parentSub readItemContent(decoder, info), // item content ); /* A non-optimized implementation of the above algorithm: // The item that was originally to the left of this item. const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null // The item that was originally to the right of this item. const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0 const hasParentYKey = cantCopyParentInfo ? decoder.readParentInfo() : false // If parent = null and neither left nor right are defined, then we know that `parent` is child of `y` // and we read the next string as parentYKey. // It indicates how we store/retrieve parent from `y.share` // @type {string|null} const parentYKey = cantCopyParentInfo && hasParentYKey ? decoder.readString() : null const struct = new Item( createID(client, clock), null, // left origin, // origin null, // right rightOrigin, // right origin cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub readItemContent(decoder, info) // item content ) */ refs[i] = struct; clock += struct.length; } } } // console.log('time to read: ', performance.now() - start) // @todo remove } return clientRefs; }; /** * Resume computing structs generated by struct readers. * * While there is something to do, we integrate structs in this order * 1. top element on stack, if stack is not empty * 2. next element from current struct reader (if empty, use next struct reader) * * If struct causally depends on another struct (ref.missing), we put next reader of * `ref.id.client` on top of stack. * * At some point we find a struct that has no causal dependencies, * then we start emptying the stack. * * It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2) * depends on struct3 (from client1). Therefore the max stack size is equal to `structReaders.length`. * * This method is implemented in a way so that we can resume computation if this update * causally depends on another update. * * @param {Transaction} transaction * @param {StructStore} store * @param {Map} clientsStructRefs * @return { null | { update: Uint8Array, missing: Map } } * * @private * @function */ const integrateStructs = (transaction, store, clientsStructRefs) => { /** * @type {Array} */ const stack = []; // sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user. let clientsStructRefsIds = from(clientsStructRefs.keys()).sort((a, b) => a - b); if (clientsStructRefsIds.length === 0) { return null; } const getNextStructTarget = () => { if (clientsStructRefsIds.length === 0) { return null; } let nextStructsTarget = /** @type {{i:number,refs:Array}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1])); while (nextStructsTarget.refs.length === nextStructsTarget.i) { clientsStructRefsIds.pop(); if (clientsStructRefsIds.length > 0) { nextStructsTarget = /** @type {{i:number,refs:Array}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1])); } else { return null; } } return nextStructsTarget; }; let curStructsTarget = getNextStructTarget(); if (curStructsTarget === null) { return null; } /** * @type {StructStore} */ const restStructs = new StructStore(); const missingSV = new Map(); /** * @param {number} client * @param {number} clock */ const updateMissingSv = (client, clock) => { const mclock = missingSV.get(client); if (mclock == null || mclock > clock) { missingSV.set(client, clock); } }; /** * @type {GC|Item} */ let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++]; // caching the state because it is used very often const state = new Map(); const addStackToRestSS = () => { for (const item of stack) { const client = item.id.client; const inapplicableItems = clientsStructRefs.get(client); if (inapplicableItems) { // decrement because we weren't able to apply previous operation inapplicableItems.i--; restStructs.clients.set(client, inapplicableItems.refs.slice(inapplicableItems.i)); clientsStructRefs.delete(client); inapplicableItems.i = 0; inapplicableItems.refs = []; } else { // item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue restStructs.clients.set(client, [item]); } // remove client from clientsStructRefsIds to prevent users from applying the same update again clientsStructRefsIds = clientsStructRefsIds.filter((c) => c !== client); } stack.length = 0; }; // iterate over all struct readers until we are done while (true) { if (stackHead.constructor !== Skip) { const localClock = setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client)); const offset = localClock - stackHead.id.clock; if (offset < 0) { // update from the same client is missing stack.push(stackHead); updateMissingSv(stackHead.id.client, stackHead.id.clock - 1); // hid a dead wall, add all items from stack to restSS addStackToRestSS(); } else { const missing = stackHead.getMissing(transaction, store); if (missing !== null) { stack.push(stackHead); // get the struct reader that has the missing struct /** * @type {{ refs: Array, i: number }} */ const structRefs = clientsStructRefs.get(/** @type {number} */ (missing)) || { refs: [], i: 0 }; if (structRefs.refs.length === structRefs.i) { // This update message causally depends on another update message that doesn't exist yet updateMissingSv(/** @type {number} */ (missing), getState(store, missing)); addStackToRestSS(); } else { stackHead = structRefs.refs[structRefs.i++]; continue; } } else if (offset === 0 || offset < stackHead.length) { // all fine, apply the stackhead stackHead.integrate(transaction, offset); state.set(stackHead.id.client, stackHead.id.clock + stackHead.length); } } } // iterate to next stackHead if (stack.length > 0) { stackHead = /** @type {GC|Item} */ (stack.pop()); } else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) { stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++]); } else { curStructsTarget = getNextStructTarget(); if (curStructsTarget === null) { // we are done! break; } else { stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++]); } } } if (restStructs.clients.size > 0) { const encoder = new UpdateEncoderV2(); writeClientsStructs(encoder, restStructs, new Map()); // write empty deleteset // writeDeleteSet(encoder, new DeleteSet()) writeVarUint(encoder.restEncoder, 0); // => no need for an extra function call, just write 0 deletes return { missing: missingSV, update: encoder.toUint8Array() }; } return null; }; /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Transaction} transaction * * @private * @function */ const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState); /** * Read and apply a document update. * * This function has the same effect as `applyUpdate` but accepts a decoder. * * @param {decoding.Decoder} decoder * @param {Doc} ydoc * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))` * @param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder] * * @function */ const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) => transact( ydoc, (transaction) => { // force that transaction.local is set to non-local transaction.local = false; let retry = false; const doc = transaction.doc; const store = doc.store; // let start = performance.now() const ss = readClientsStructRefs(structDecoder, doc); // console.log('time to read structs: ', performance.now() - start) // @todo remove // start = performance.now() // console.log('time to merge: ', performance.now() - start) // @todo remove // start = performance.now() const restStructs = integrateStructs(transaction, store, ss); const pending = store.pendingStructs; if (pending) { // check if we can apply something for (const [client, clock] of pending.missing) { if (clock < getState(store, client)) { retry = true; break; } } if (restStructs) { // merge restStructs into store.pending for (const [client, clock] of restStructs.missing) { const mclock = pending.missing.get(client); if (mclock == null || mclock > clock) { pending.missing.set(client, clock); } } pending.update = mergeUpdatesV2([pending.update, restStructs.update]); } } else { store.pendingStructs = restStructs; } // console.log('time to integrate: ', performance.now() - start) // @todo remove // start = performance.now() const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store); if (store.pendingDs) { // @todo we could make a lower-bound state-vector check as we do above const pendingDSUpdate = new UpdateDecoderV2(createDecoder(store.pendingDs)); readVarUint(pendingDSUpdate.restDecoder); // read 0 structs, because we only encode deletes in pendingdsupdate const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store); if (dsRest && dsRest2) { // case 1: ds1 != null && ds2 != null store.pendingDs = mergeUpdatesV2([dsRest, dsRest2]); } else { // case 2: ds1 != null // case 3: ds2 != null // case 4: ds1 == null && ds2 == null store.pendingDs = dsRest || dsRest2; } } else { // Either dsRest == null && pendingDs == null OR dsRest != null store.pendingDs = dsRest; } // console.log('time to cleanup: ', performance.now() - start) // @todo remove // start = performance.now() // console.log('time to resume delete readers: ', performance.now() - start) // @todo remove // start = performance.now() if (retry) { const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update; store.pendingStructs = null; applyUpdateV2(transaction.doc, update); } }, transactionOrigin, false, ); /** * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. * * This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder. * * @param {Doc} ydoc * @param {Uint8Array} update * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))` * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] * * @function */ const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => { const decoder = createDecoder(update); readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder)); }; /** * Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`. * * This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder. * * @param {Doc} ydoc * @param {Uint8Array} update * @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))` * * @function */ const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1); /** * Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will * only write the operations that are missing. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Doc} doc * @param {Map} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs * * @function */ const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => { writeClientsStructs(encoder, doc.store, targetStateVector); writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store)); }; /** * Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will * only write the operations that are missing. * * Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder * * @param {Doc} doc * @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs * @param {UpdateEncoderV1 | UpdateEncoderV2} [encoder] * @return {Uint8Array} * * @function */ const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => { const targetStateVector = decodeStateVector(encodedTargetStateVector); writeStateAsUpdate(encoder, doc, targetStateVector); const updates = [encoder.toUint8Array()]; // also add the pending updates (if there are any) if (doc.store.pendingDs) { updates.push(doc.store.pendingDs); } if (doc.store.pendingStructs) { updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector)); } if (updates.length > 1) { if (encoder.constructor === UpdateEncoderV1) { return mergeUpdates(updates.map((update, i) => (i === 0 ? update : convertUpdateFormatV2ToV1(update)))); } else if (encoder.constructor === UpdateEncoderV2) { return mergeUpdatesV2(updates); } } return updates[0]; }; /** * Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will * only write the operations that are missing. * * Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder * * @param {Doc} doc * @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs * @return {Uint8Array} * * @function */ const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new UpdateEncoderV1()); /** * Read state vector from Decoder and return as Map * * @param {DSDecoderV1 | DSDecoderV2} decoder * @return {Map} Maps `client` to the number next expected `clock` from that client. * * @function */ const readStateVector = (decoder) => { const ss = new Map(); const ssLength = readVarUint(decoder.restDecoder); for (let i = 0; i < ssLength; i++) { const client = readVarUint(decoder.restDecoder); const clock = readVarUint(decoder.restDecoder); ss.set(client, clock); } return ss; }; /** * Read decodedState and return State as Map. * * @param {Uint8Array} decodedState * @return {Map} Maps `client` to the number next expected `clock` from that client. * * @function */ // export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState))) /** * Read decodedState and return State as Map. * * @param {Uint8Array} decodedState * @return {Map} Maps `client` to the number next expected `clock` from that client. * * @function */ const decodeStateVector = (decodedState) => readStateVector(new DSDecoderV1(createDecoder(decodedState))); /** * @param {DSEncoderV1 | DSEncoderV2} encoder * @param {Map} sv * @function */ const writeStateVector = (encoder, sv) => { writeVarUint(encoder.restEncoder, sv.size); from(sv.entries()) .sort((a, b) => b[0] - a[0]) .forEach(([client, clock]) => { writeVarUint(encoder.restEncoder, client); // @todo use a special client decoder that is based on mapping writeVarUint(encoder.restEncoder, clock); }); return encoder; }; /** * @param {DSEncoderV1 | DSEncoderV2} encoder * @param {Doc} doc * * @function */ const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store)); /** * Encode State as Uint8Array. * * @param {Doc|Map} doc * @param {DSEncoderV1 | DSEncoderV2} [encoder] * @return {Uint8Array} * * @function */ const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => { if (doc instanceof Map) { writeStateVector(encoder, doc); } else { writeDocumentStateVector(encoder, doc); } return encoder.toUint8Array(); }; /** * Encode State as Uint8Array. * * @param {Doc|Map} doc * @return {Uint8Array} * * @function */ const encodeStateVector = (doc) => encodeStateVectorV2(doc, new DSEncoderV1()); /** * General event handler implementation. * * @template ARG0, ARG1 * * @private */ class EventHandler { constructor() { /** * @type {Array} */ this.l = []; } } /** * @template ARG0,ARG1 * @returns {EventHandler} * * @private * @function */ const createEventHandler = () => new EventHandler(); /** * Adds an event listener that is called when * {@link EventHandler#callEventListeners} is called. * * @template ARG0,ARG1 * @param {EventHandler} eventHandler * @param {function(ARG0,ARG1):void} f The event handler. * * @private * @function */ const addEventHandlerListener = (eventHandler, f) => eventHandler.l.push(f); /** * Removes an event listener. * * @template ARG0,ARG1 * @param {EventHandler} eventHandler * @param {function(ARG0,ARG1):void} f The event handler that was added with * {@link EventHandler#addEventListener} * * @private * @function */ const removeEventHandlerListener = (eventHandler, f) => { const l = eventHandler.l; const len = l.length; eventHandler.l = l.filter((g) => f !== g); if (len === eventHandler.l.length) { console.error("[yjs] Tried to remove event handler that doesn't exist."); } }; /** * Call all event listeners that were added via * {@link EventHandler#addEventListener}. * * @template ARG0,ARG1 * @param {EventHandler} eventHandler * @param {ARG0} arg0 * @param {ARG1} arg1 * * @private * @function */ const callEventHandlerListeners = (eventHandler, arg0, arg1) => callAll(eventHandler.l, [arg0, arg1]); class ID { /** * @param {number} client client id * @param {number} clock unique per client id, continuous number */ constructor(client, clock) { /** * Client id * @type {number} */ this.client = client; /** * unique per client id, continuous number * @type {number} */ this.clock = clock; } } /** * @param {ID | null} a * @param {ID | null} b * @return {boolean} * * @function */ const compareIDs = (a, b) => a === b || (a !== null && b !== null && a.client === b.client && a.clock === b.clock); /** * @param {number} client * @param {number} clock * * @private * @function */ const createID = (client, clock) => new ID(client, clock); /** * The top types are mapped from y.share.get(keyname) => type. * `type` does not store any information about the `keyname`. * This function finds the correct `keyname` for `type` and throws otherwise. * * @param {AbstractType} type * @return {string} * * @private * @function */ const findRootTypeKey = (type) => { // @ts-ignore _y must be defined, otherwise unexpected case for (const [key, value] of type.doc.share.entries()) { if (value === type) { return key; } } throw unexpectedCase(); }; /** * Check if `parent` is a parent of `child`. * * @param {AbstractType} parent * @param {Item|null} child * @return {Boolean} Whether `parent` is a parent of `child`. * * @private * @function */ const isParentOf = (parent, child) => { while (child !== null) { if (child.parent === parent) { return true; } child = /** @type {AbstractType} */ (child.parent)._item; } return false; }; /** * A relative position is based on the Yjs model and is not affected by document changes. * E.g. If you place a relative position before a certain character, it will always point to this character. * If you place a relative position at the end of a type, it will always point to the end of the type. * * A numeric position is often unsuited for user selections, because it does not change when content is inserted * before or after. * * ```Insert(0, 'x')('a|bc') = 'xa|bc'``` Where | is the relative position. * * One of the properties must be defined. * * @example * // Current cursor position is at position 10 * const relativePosition = createRelativePositionFromIndex(yText, 10) * // modify yText * yText.insert(0, 'abc') * yText.delete(3, 10) * // Compute the cursor position * const absolutePosition = createAbsolutePositionFromRelativePosition(y, relativePosition) * absolutePosition.type === yText // => true * console.log('cursor location is ' + absolutePosition.index) // => cursor location is 3 * */ class RelativePosition { /** * @param {ID|null} type * @param {string|null} tname * @param {ID|null} item * @param {number} assoc */ constructor(type, tname, item, assoc = 0) { /** * @type {ID|null} */ this.type = type; /** * @type {string|null} */ this.tname = tname; /** * @type {ID | null} */ this.item = item; /** * A relative position is associated to a specific character. By default * assoc >= 0, the relative position is associated to the character * after the meant position. * I.e. position 1 in 'ab' is associated to character 'b'. * * If assoc < 0, then the relative position is associated to the character * before the meant position. * * @type {number} */ this.assoc = assoc; } } /** * @param {any} json * @return {RelativePosition} * * @function */ const createRelativePositionFromJSON = (json) => new RelativePosition( json.type == null ? null : createID(json.type.client, json.type.clock), json.tname ?? null, json.item == null ? null : createID(json.item.client, json.item.clock), json.assoc == null ? 0 : json.assoc, ); class AbsolutePosition { /** * @param {AbstractType} type * @param {number} index * @param {number} [assoc] */ constructor(type, index, assoc = 0) { /** * @type {AbstractType} */ this.type = type; /** * @type {number} */ this.index = index; this.assoc = assoc; } } /** * @param {AbstractType} type * @param {number} index * @param {number} [assoc] * * @function */ const createAbsolutePosition = (type, index, assoc = 0) => new AbsolutePosition(type, index, assoc); /** * @param {AbstractType} type * @param {ID|null} item * @param {number} [assoc] * * @function */ const createRelativePosition$1 = (type, item, assoc) => { let typeid = null; let tname = null; if (type._item === null) { tname = findRootTypeKey(type); } else { typeid = createID(type._item.id.client, type._item.id.clock); } return new RelativePosition(typeid, tname, item, assoc); }; /** * Create a relativePosition based on a absolute position. * * @param {AbstractType} type The base type (e.g. YText or YArray). * @param {number} index The absolute position. * @param {number} [assoc] * @return {RelativePosition} * * @function */ const createRelativePositionFromTypeIndex = (type, index, assoc = 0) => { let t = type._start; if (assoc < 0) { // associated to the left character or the beginning of a type, increment index if possible. if (index === 0) { return createRelativePosition$1(type, null, assoc); } index--; } while (t !== null) { if (!t.deleted && t.countable) { if (t.length > index) { // case 1: found position somewhere in the linked list return createRelativePosition$1(type, createID(t.id.client, t.id.clock + index), assoc); } index -= t.length; } if (t.right === null && assoc < 0) { // left-associated position, return last available id return createRelativePosition$1(type, t.lastId, assoc); } t = t.right; } return createRelativePosition$1(type, null, assoc); }; /** * @param {StructStore} store * @param {ID} id */ const getItemWithOffset = (store, id) => { const item = getItem(store, id); const diff = id.clock - item.id.clock; return { item, diff, }; }; /** * Transform a relative position to an absolute position. * * If you want to share the relative position with other users, you should set * `followUndoneDeletions` to false to get consistent results across all clients. * * When calculating the absolute position, we try to follow the "undone deletions". This yields * better results for the user who performed undo. However, only the user who performed the undo * will get the better results, the other users don't know which operations recreated a deleted * range of content. There is more information in this ticket: https://github.com/yjs/yjs/issues/638 * * @param {RelativePosition} rpos * @param {Doc} doc * @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638 * @return {AbsolutePosition|null} * * @function */ const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true) => { const store = doc.store; const rightID = rpos.item; const typeID = rpos.type; const tname = rpos.tname; const assoc = rpos.assoc; let type = null; let index = 0; if (rightID !== null) { if (getState(store, rightID.client) <= rightID.clock) { return null; } const res = followUndoneDeletions ? followRedone(store, rightID) : getItemWithOffset(store, rightID); const right = res.item; if (!(right instanceof Item$1)) { return null; } type = /** @type {AbstractType} */ (right.parent); if (type._item === null || !type._item.deleted) { index = right.deleted || !right.countable ? 0 : res.diff + (assoc >= 0 ? 0 : 1); // adjust position based on left association if necessary let n = right.left; while (n !== null) { if (!n.deleted && n.countable) { index += n.length; } n = n.left; } } } else { if (tname !== null) { type = doc.get(tname); } else if (typeID !== null) { if (getState(store, typeID.client) <= typeID.clock) { // type does not exist yet return null; } const { item } = followUndoneDeletions ? followRedone(store, typeID) : { item: getItem(store, typeID) }; if (item instanceof Item$1 && item.content instanceof ContentType) { type = item.content.type; } else { // struct is garbage collected return null; } } else { throw unexpectedCase(); } if (assoc >= 0) { index = type._length; } else { index = 0; } } return createAbsolutePosition(type, index, rpos.assoc); }; /** * @param {RelativePosition|null} a * @param {RelativePosition|null} b * @return {boolean} * * @function */ const compareRelativePositions = (a, b) => a === b || (a !== null && b !== null && a.tname === b.tname && compareIDs(a.item, b.item) && compareIDs(a.type, b.type) && a.assoc === b.assoc); class Snapshot { /** * @param {DeleteSet} ds * @param {Map} sv state map */ constructor(ds, sv) { /** * @type {DeleteSet} */ this.ds = ds; /** * State Map * @type {Map} */ this.sv = sv; } } /** * @param {DeleteSet} ds * @param {Map} sm * @return {Snapshot} */ const createSnapshot = (ds, sm) => new Snapshot(ds, sm); /** * @param {Doc} doc * @return {Snapshot} */ const snapshot = (doc) => createSnapshot(createDeleteSetFromStructStore(doc.store), getStateVector(doc.store)); /** * @param {Item} item * @param {Snapshot|undefined} snapshot * * @protected * @function */ const isVisible$1 = (item, snapshot) => snapshot === undefined ? !item.deleted : snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id); /** * @param {Transaction} transaction * @param {Snapshot} snapshot */ const splitSnapshotAffectedStructs = (transaction, snapshot) => { const meta = setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, create$8); const store = transaction.doc.store; // check if we already split for this snapshot if (!meta.has(snapshot)) { snapshot.sv.forEach((clock, client) => { if (clock < getState(store, client)) { getItemCleanStart(transaction, createID(client, clock)); } }); iterateDeletedStructs(transaction, snapshot.ds, (_item) => {}); meta.add(snapshot); } }; class StructStore { constructor() { /** * @type {Map>} */ this.clients = new Map(); /** * @type {null | { missing: Map, update: Uint8Array }} */ this.pendingStructs = null; /** * @type {null | Uint8Array} */ this.pendingDs = null; } } /** * Return the states as a Map. * Note that clock refers to the next expected clock id. * * @param {StructStore} store * @return {Map} * * @public * @function */ const getStateVector = (store) => { const sm = new Map(); store.clients.forEach((structs, client) => { const struct = structs[structs.length - 1]; sm.set(client, struct.id.clock + struct.length); }); return sm; }; /** * @param {StructStore} store * @param {number} client * @return {number} * * @public * @function */ const getState = (store, client) => { const structs = store.clients.get(client); if (structs === undefined) { return 0; } const lastStruct = structs[structs.length - 1]; return lastStruct.id.clock + lastStruct.length; }; /** * @param {StructStore} store * @param {GC|Item} struct * * @private * @function */ const addStruct = (store, struct) => { let structs = store.clients.get(struct.id.client); if (structs === undefined) { structs = []; store.clients.set(struct.id.client, structs); } else { const lastStruct = structs[structs.length - 1]; if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) { throw unexpectedCase(); } } structs.push(struct); }; /** * Perform a binary search on a sorted array * @param {Array} structs * @param {number} clock * @return {number} * * @private * @function */ const findIndexSS = (structs, clock) => { let left = 0; let right = structs.length - 1; let mid = structs[right]; let midclock = mid.id.clock; if (midclock === clock) { return right; } // @todo does it even make sense to pivot the search? // If a good split misses, it might actually increase the time to find the correct item. // Currently, the only advantage is that search with pivoting might find the item on the first try. let midindex = floor$1((clock / (midclock + mid.length - 1)) * right); // pivoting the search while (left <= right) { mid = structs[midindex]; midclock = mid.id.clock; if (midclock <= clock) { if (clock < midclock + mid.length) { return midindex; } left = midindex + 1; } else { right = midindex - 1; } midindex = floor$1((left + right) / 2); } // Always check state before looking for a struct in StructStore // Therefore the case of not finding a struct is unexpected throw unexpectedCase(); }; /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * * @param {StructStore} store * @param {ID} id * @return {GC|Item} * * @private * @function */ const find$1 = (store, id) => { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(id.client); return structs[findIndexSS(structs, id.clock)]; }; /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * @private * @function */ const getItem = /** @type {function(StructStore,ID):Item} */ (find$1); /** * @param {Transaction} transaction * @param {Array} structs * @param {number} clock */ const findIndexCleanStart = (transaction, structs, clock) => { const index = findIndexSS(structs, clock); const struct = structs[index]; if (struct.id.clock < clock && struct instanceof Item$1) { structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock)); return index + 1; } return index; }; /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * * @param {Transaction} transaction * @param {ID} id * @return {Item} * * @private * @function */ const getItemCleanStart = (transaction, id) => { const structs = /** @type {Array} */ (transaction.doc.store.clients.get(id.client)); return structs[findIndexCleanStart(transaction, structs, id.clock)]; }; /** * Expects that id is actually in store. This function throws or is an infinite loop otherwise. * * @param {Transaction} transaction * @param {StructStore} store * @param {ID} id * @return {Item} * * @private * @function */ const getItemCleanEnd = (transaction, store, id) => { /** * @type {Array} */ // @ts-ignore const structs = store.clients.get(id.client); const index = findIndexSS(structs, id.clock); const struct = structs[index]; if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) { structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1)); } return struct; }; /** * Replace `item` with `newitem` in store * @param {StructStore} store * @param {GC|Item} struct * @param {GC|Item} newStruct * * @private * @function */ const replaceStruct = (store, struct, newStruct) => { const structs = /** @type {Array} */ (store.clients.get(struct.id.client)); structs[findIndexSS(structs, struct.id.clock)] = newStruct; }; /** * Iterate over a range of structs * * @param {Transaction} transaction * @param {Array} structs * @param {number} clockStart Inclusive start * @param {number} len * @param {function(GC|Item):void} f * * @function */ const iterateStructs = (transaction, structs, clockStart, len, f) => { if (len === 0) { return; } const clockEnd = clockStart + len; let index = findIndexCleanStart(transaction, structs, clockStart); let struct; do { struct = structs[index++]; if (clockEnd < struct.id.clock + struct.length) { findIndexCleanStart(transaction, structs, clockEnd); } f(struct); } while (index < structs.length && structs[index].id.clock < clockEnd); }; /** * A transaction is created for every change on the Yjs model. It is possible * to bundle changes on the Yjs model in a single transaction to * minimize the number on messages sent and the number of observer calls. * If possible the user of this library should bundle as many changes as * possible. Here is an example to illustrate the advantages of bundling: * * @example * const ydoc = new Y.Doc() * const map = ydoc.getMap('map') * // Log content when change is triggered * map.observe(() => { * console.log('change triggered') * }) * // Each change on the map type triggers a log message: * map.set('a', 0) // => "change triggered" * map.set('b', 0) // => "change triggered" * // When put in a transaction, it will trigger the log after the transaction: * ydoc.transact(() => { * map.set('a', 1) * map.set('b', 1) * }) // => "change triggered" * * @public */ class Transaction { /** * @param {Doc} doc * @param {any} origin * @param {boolean} local */ constructor(doc, origin, local) { /** * The Yjs instance. * @type {Doc} */ this.doc = doc; /** * Describes the set of deleted items by ids * @type {DeleteSet} */ this.deleteSet = new DeleteSet(); /** * Holds the state before the transaction started. * @type {Map} */ this.beforeState = getStateVector(doc.store); /** * Holds the state after the transaction. * @type {Map} */ this.afterState = new Map(); /** * All types that were directly modified (property added or child * inserted/deleted). New types are not included in this Set. * Maps from type to parentSubs (`item.parentSub = null` for YArray) * @type {Map>,Set>} */ this.changed = new Map(); /** * Stores the events for the types that observe also child elements. * It is mainly used by `observeDeep`. * @type {Map>,Array>>} */ this.changedParentTypes = new Map(); /** * @type {Array} */ this._mergeStructs = []; /** * @type {any} */ this.origin = origin; /** * Stores meta information on the transaction * @type {Map} */ this.meta = new Map(); /** * Whether this change originates from this doc. * @type {boolean} */ this.local = local; /** * @type {Set} */ this.subdocsAdded = new Set(); /** * @type {Set} */ this.subdocsRemoved = new Set(); /** * @type {Set} */ this.subdocsLoaded = new Set(); /** * @type {boolean} */ this._needFormattingCleanup = false; } } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {Transaction} transaction * @return {boolean} Whether data was written. */ const writeUpdateMessageFromTransaction = (encoder, transaction) => { if (transaction.deleteSet.clients.size === 0 && !any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) { return false; } sortAndMergeDeleteSet(transaction.deleteSet); writeStructsFromTransaction(encoder, transaction); writeDeleteSet(encoder, transaction.deleteSet); return true; }; /** * If `type.parent` was added in current transaction, `type` technically * did not change, it was just added and we should not fire events for `type`. * * @param {Transaction} transaction * @param {AbstractType>} type * @param {string|null} parentSub */ const addChangedTypeToTransaction = (transaction, type, parentSub) => { const item = type._item; if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) { setIfUndefined(transaction.changed, type, create$8).add(parentSub); } }; /** * @param {Array} structs * @param {number} pos * @return {number} # of merged structs */ const tryToMergeWithLefts = (structs, pos) => { let right = structs[pos]; let left = structs[pos - 1]; let i = pos; for (; i > 0; right = left, left = structs[--i - 1]) { if (left.deleted === right.deleted && left.constructor === right.constructor) { if (left.mergeWith(right)) { if (right instanceof Item$1 && right.parentSub !== null && /** @type {AbstractType} */ (right.parent)._map.get(right.parentSub) === right) { /** @type {AbstractType} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left)); } continue; } } break; } const merged = pos - i; if (merged) { // remove all merged structs from the array structs.splice(pos + 1 - merged, merged); } return merged; }; /** * @param {DeleteSet} ds * @param {StructStore} store * @param {function(Item):boolean} gcFilter */ const tryGcDeleteSet = (ds, store, gcFilter) => { for (const [client, deleteItems] of ds.clients.entries()) { const structs = /** @type {Array} */ (store.clients.get(client)); for (let di = deleteItems.length - 1; di >= 0; di--) { const deleteItem = deleteItems[di]; const endDeleteItemClock = deleteItem.clock + deleteItem.len; for (let si = findIndexSS(structs, deleteItem.clock), struct = structs[si]; si < structs.length && struct.id.clock < endDeleteItemClock; struct = structs[++si]) { const struct = structs[si]; if (deleteItem.clock + deleteItem.len <= struct.id.clock) { break; } if (struct instanceof Item$1 && struct.deleted && !struct.keep && gcFilter(struct)) { struct.gc(store, false); } } } } }; /** * @param {DeleteSet} ds * @param {StructStore} store */ const tryMergeDeleteSet = (ds, store) => { // try to merge deleted / gc'd items // merge from right to left for better efficiency and so we don't miss any merge targets ds.clients.forEach((deleteItems, client) => { const structs = /** @type {Array} */ (store.clients.get(client)); for (let di = deleteItems.length - 1; di >= 0; di--) { const deleteItem = deleteItems[di]; // start with merging the item next to the last deleted item const mostRightIndexToCheck = min$1(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1)); for (let si = mostRightIndexToCheck, struct = structs[si]; si > 0 && struct.id.clock >= deleteItem.clock; struct = structs[si]) { si -= 1 + tryToMergeWithLefts(structs, si); } } }); }; /** * @param {Array} transactionCleanups * @param {number} i */ const cleanupTransactions = (transactionCleanups, i) => { if (i < transactionCleanups.length) { const transaction = transactionCleanups[i]; const doc = transaction.doc; const store = doc.store; const ds = transaction.deleteSet; const mergeStructs = transaction._mergeStructs; try { sortAndMergeDeleteSet(ds); transaction.afterState = getStateVector(transaction.doc.store); doc.emit("beforeObserverCalls", [transaction, doc]); /** * An array of event callbacks. * * Each callback is called even if the other ones throw errors. * * @type {Array} */ const fs = []; // observe events on changed types transaction.changed.forEach((subs, itemtype) => fs.push(() => { if (itemtype._item === null || !itemtype._item.deleted) { itemtype._callObserver(transaction, subs); } }), ); fs.push(() => { // deep observe events transaction.changedParentTypes.forEach((events, type) => { // We need to think about the possibility that the user transforms the // Y.Doc in the event. if (type._dEH.l.length > 0 && (type._item === null || !type._item.deleted)) { events = events.filter((event) => event.target._item === null || !event.target._item.deleted); events.forEach((event) => { event.currentTarget = type; // path is relative to the current target event._path = null; }); // sort events by path length so that top-level events are fired first. events.sort((event1, event2) => event1.path.length - event2.path.length); fs.push(() => { // We don't need to check for events.length // because we know it has at least one element callEventHandlerListeners(type._dEH, events, transaction); }); } }); fs.push(() => doc.emit("afterTransaction", [transaction, doc])); fs.push(() => { if (transaction._needFormattingCleanup) { cleanupYTextAfterTransaction(transaction); } }); }); callAll(fs, []); } finally { // Replace deleted items with ItemDeleted / GC. // This is where content is actually remove from the Yjs Doc. if (doc.gc) { tryGcDeleteSet(ds, store, doc.gcFilter); } tryMergeDeleteSet(ds, store); // on all affected store.clients props, try to merge transaction.afterState.forEach((clock, client) => { const beforeClock = transaction.beforeState.get(client) || 0; if (beforeClock !== clock) { const structs = /** @type {Array} */ (store.clients.get(client)); // we iterate from right to left so we can safely remove entries const firstChangePos = max$1(findIndexSS(structs, beforeClock), 1); for (let i = structs.length - 1; i >= firstChangePos; ) { i -= 1 + tryToMergeWithLefts(structs, i); } } }); // try to merge mergeStructs // @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left // but at the moment DS does not handle duplicates for (let i = mergeStructs.length - 1; i >= 0; i--) { const { client, clock } = mergeStructs[i].id; const structs = /** @type {Array} */ (store.clients.get(client)); const replacedStructPos = findIndexSS(structs, clock); if (replacedStructPos + 1 < structs.length) { if (tryToMergeWithLefts(structs, replacedStructPos + 1) > 1) { continue; // no need to perform next check, both are already merged } } if (replacedStructPos > 0) { tryToMergeWithLefts(structs, replacedStructPos); } } if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) { print(ORANGE, BOLD, "[yjs] ", UNBOLD, RED, "Changed the client-id because another client seems to be using it."); doc.clientID = generateNewClientId(); } // @todo Merge all the transactions into one and provide send the data as a single update message doc.emit("afterTransactionCleanup", [transaction, doc]); if (doc._observers.has("update")) { const encoder = new UpdateEncoderV1(); const hasContent = writeUpdateMessageFromTransaction(encoder, transaction); if (hasContent) { doc.emit("update", [encoder.toUint8Array(), transaction.origin, doc, transaction]); } } if (doc._observers.has("updateV2")) { const encoder = new UpdateEncoderV2(); const hasContent = writeUpdateMessageFromTransaction(encoder, transaction); if (hasContent) { doc.emit("updateV2", [encoder.toUint8Array(), transaction.origin, doc, transaction]); } } const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction; if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) { subdocsAdded.forEach((subdoc) => { subdoc.clientID = doc.clientID; if (subdoc.collectionid == null) { subdoc.collectionid = doc.collectionid; } doc.subdocs.add(subdoc); }); subdocsRemoved.forEach((subdoc) => doc.subdocs.delete(subdoc)); doc.emit("subdocs", [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc, transaction]); subdocsRemoved.forEach((subdoc) => subdoc.destroy()); } if (transactionCleanups.length <= i + 1) { doc._transactionCleanups = []; doc.emit("afterAllTransactions", [doc, transactionCleanups]); } else { cleanupTransactions(transactionCleanups, i + 1); } } } }; /** * Implements the functionality of `y.transact(()=>{..})` * * @template T * @param {Doc} doc * @param {function(Transaction):T} f * @param {any} [origin=true] * @return {T} * * @function */ const transact = (doc, f, origin = null, local = true) => { const transactionCleanups = doc._transactionCleanups; let initialCall = false; /** * @type {any} */ let result = null; if (doc._transaction === null) { initialCall = true; doc._transaction = new Transaction(doc, origin, local); transactionCleanups.push(doc._transaction); if (transactionCleanups.length === 1) { doc.emit("beforeAllTransactions", [doc]); } doc.emit("beforeTransaction", [doc._transaction, doc]); } try { result = f(doc._transaction); } finally { if (initialCall) { const finishCleanup = doc._transaction === transactionCleanups[0]; doc._transaction = null; if (finishCleanup) { // The first transaction ended, now process observer calls. // Observer call may create new transactions for which we need to call the observers and do cleanup. // We don't want to nest these calls, so we execute these calls one after // another. // Also we need to ensure that all cleanups are called, even if the // observes throw errors. // This file is full of hacky try {} finally {} blocks to ensure that an // event can throw errors and also that the cleanup is called. cleanupTransactions(transactionCleanups, 0); } } } return result; }; class StackItem { /** * @param {DeleteSet} deletions * @param {DeleteSet} insertions */ constructor(deletions, insertions) { this.insertions = insertions; this.deletions = deletions; /** * Use this to save and restore metadata like selection range */ this.meta = new Map(); } } /** * @param {Transaction} tr * @param {UndoManager} um * @param {StackItem} stackItem */ const clearUndoManagerStackItem = (tr, um, stackItem) => { iterateDeletedStructs(tr, stackItem.deletions, (item) => { if (item instanceof Item$1 && um.scope.some((type) => type === tr.doc || isParentOf(/** @type {AbstractType} */ (type), item))) { keepItem(item, false); } }); }; /** * @param {UndoManager} undoManager * @param {Array} stack * @param {'undo'|'redo'} eventType * @return {StackItem?} */ const popStackItem = (undoManager, stack, eventType) => { /** * Keep a reference to the transaction so we can fire the event with the changedParentTypes * @type {any} */ let _tr = null; const doc = undoManager.doc; const scope = undoManager.scope; transact( doc, (transaction) => { while (stack.length > 0 && undoManager.currStackItem === null) { const store = doc.store; const stackItem = /** @type {StackItem} */ (stack.pop()); /** * @type {Set} */ const itemsToRedo = new Set(); /** * @type {Array} */ const itemsToDelete = []; let performedChange = false; iterateDeletedStructs(transaction, stackItem.insertions, (struct) => { if (struct instanceof Item$1) { if (struct.redone !== null) { let { item, diff } = followRedone(store, struct.id); if (diff > 0) { item = getItemCleanStart(transaction, createID(item.id.client, item.id.clock + diff)); } struct = item; } if (!struct.deleted && scope.some((type) => type === transaction.doc || isParentOf(/** @type {AbstractType} */ (type), /** @type {Item} */ (struct)))) { itemsToDelete.push(struct); } } }); iterateDeletedStructs(transaction, stackItem.deletions, (struct) => { if ( struct instanceof Item$1 && scope.some((type) => type === transaction.doc || isParentOf(/** @type {AbstractType} */ (type), struct)) && // Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval. !isDeleted(stackItem.insertions, struct.id) ) { itemsToRedo.add(struct); } }); itemsToRedo.forEach((struct) => { performedChange = redoItem(transaction, struct, itemsToRedo, stackItem.insertions, undoManager.ignoreRemoteMapChanges, undoManager) !== null || performedChange; }); // We want to delete in reverse order so that children are deleted before // parents, so we have more information available when items are filtered. for (let i = itemsToDelete.length - 1; i >= 0; i--) { const item = itemsToDelete[i]; if (undoManager.deleteFilter(item)) { item.delete(transaction); performedChange = true; } } undoManager.currStackItem = performedChange ? stackItem : null; } transaction.changed.forEach((subProps, type) => { // destroy search marker if necessary if (subProps.has(null) && type._searchMarker) { type._searchMarker.length = 0; } }); _tr = transaction; }, undoManager, ); const res = undoManager.currStackItem; if (res != null) { const changedParentTypes = _tr.changedParentTypes; undoManager.emit("stack-item-popped", [{ stackItem: res, type: eventType, changedParentTypes, origin: undoManager }, undoManager]); undoManager.currStackItem = null; } return res; }; /** * @typedef {Object} UndoManagerOptions * @property {number} [UndoManagerOptions.captureTimeout=500] * @property {function(Transaction):boolean} [UndoManagerOptions.captureTransaction] Do not capture changes of a Transaction if result false. * @property {function(Item):boolean} [UndoManagerOptions.deleteFilter=()=>true] Sometimes * it is necessary to filter what an Undo/Redo operation can delete. If this * filter returns false, the type/item won't be deleted even it is in the * undo/redo scope. * @property {Set} [UndoManagerOptions.trackedOrigins=new Set([null])] * @property {boolean} [ignoreRemoteMapChanges] Experimental. By default, the UndoManager will never overwrite remote changes. Enable this property to enable overwriting remote changes on key-value changes (Y.Map, properties on Y.Xml, etc..). * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty. */ /** * @typedef {Object} StackItemEvent * @property {StackItem} StackItemEvent.stackItem * @property {any} StackItemEvent.origin * @property {'undo'|'redo'} StackItemEvent.type * @property {Map>,Array>>} StackItemEvent.changedParentTypes */ /** * Fires 'stack-item-added' event when a stack item was added to either the undo- or * the redo-stack. You may store additional stack information via the * metadata property on `event.stackItem.meta` (it is a `Map` of metadata properties). * Fires 'stack-item-popped' event when a stack item was popped from either the * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`. * * @extends {ObservableV2<{'stack-item-added':function(StackItemEvent, UndoManager):void, 'stack-item-popped': function(StackItemEvent, UndoManager):void, 'stack-cleared': function({ undoStackCleared: boolean, redoStackCleared: boolean }):void, 'stack-item-updated': function(StackItemEvent, UndoManager):void }>} */ class UndoManager extends ObservableV2 { /** * @param {Doc|AbstractType|Array>} typeScope Limits the scope of the UndoManager. If this is set to a ydoc instance, all changes on that ydoc will be undone. If set to a specific type, only changes on that type or its children will be undone. Also accepts an array of types. * @param {UndoManagerOptions} options */ constructor( typeScope, { captureTimeout = 500, captureTransaction = (_tr) => true, deleteFilter = () => true, trackedOrigins = new Set([null]), ignoreRemoteMapChanges = false, doc = /** @type {Doc} */ ( isArray(typeScope) ? typeScope[0].doc : typeScope instanceof Doc ? typeScope : typeScope.doc ), } = {}, ) { super(); /** * @type {Array | Doc>} */ this.scope = []; this.doc = doc; this.addToScope(typeScope); this.deleteFilter = deleteFilter; trackedOrigins.add(this); this.trackedOrigins = trackedOrigins; this.captureTransaction = captureTransaction; /** * @type {Array} */ this.undoStack = []; /** * @type {Array} */ this.redoStack = []; /** * Whether the client is currently undoing (calling UndoManager.undo) * * @type {boolean} */ this.undoing = false; this.redoing = false; /** * The currently popped stack item if UndoManager.undoing or UndoManager.redoing * * @type {StackItem|null} */ this.currStackItem = null; this.lastChange = 0; this.ignoreRemoteMapChanges = ignoreRemoteMapChanges; this.captureTimeout = captureTimeout; /** * @param {Transaction} transaction */ this.afterTransactionHandler = (transaction) => { // Only track certain transactions if ( !this.captureTransaction(transaction) || !this.scope.some((type) => transaction.changedParentTypes.has(/** @type {AbstractType} */ (type)) || type === this.doc) || (!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor))) ) { return; } const undoing = this.undoing; const redoing = this.redoing; const stack = undoing ? this.redoStack : this.undoStack; if (undoing) { this.stopCapturing(); // next undo should not be appended to last stack item } else if (!redoing) { // neither undoing nor redoing: delete redoStack this.clear(false, true); } const insertions = new DeleteSet(); transaction.afterState.forEach((endClock, client) => { const startClock = transaction.beforeState.get(client) || 0; const len = endClock - startClock; if (len > 0) { addToDeleteSet(insertions, client, startClock, len); } }); const now = getUnixTime(); let didAdd = false; if (this.lastChange > 0 && now - this.lastChange < this.captureTimeout && stack.length > 0 && !undoing && !redoing) { // append change to last stack op const lastOp = stack[stack.length - 1]; lastOp.deletions = mergeDeleteSets([lastOp.deletions, transaction.deleteSet]); lastOp.insertions = mergeDeleteSets([lastOp.insertions, insertions]); } else { // create a new stack op stack.push(new StackItem(transaction.deleteSet, insertions)); didAdd = true; } if (!undoing && !redoing) { this.lastChange = now; } // make sure that deleted structs are not gc'd iterateDeletedStructs( transaction, transaction.deleteSet, /** @param {Item|GC} item */ (item) => { if (item instanceof Item$1 && this.scope.some((type) => type === transaction.doc || isParentOf(/** @type {AbstractType} */ (type), item))) { keepItem(item, true); } }, ); /** * @type {[StackItemEvent, UndoManager]} */ const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? "redo" : "undo", changedParentTypes: transaction.changedParentTypes }, this]; if (didAdd) { this.emit("stack-item-added", changeEvent); } else { this.emit("stack-item-updated", changeEvent); } }; this.doc.on("afterTransaction", this.afterTransactionHandler); this.doc.on("destroy", () => { this.destroy(); }); } /** * Extend the scope. * * @param {Array | Doc> | AbstractType | Doc} ytypes */ addToScope(ytypes) { const tmpSet = new Set(this.scope); ytypes = isArray(ytypes) ? ytypes : [ytypes]; ytypes.forEach((ytype) => { if (!tmpSet.has(ytype)) { tmpSet.add(ytype); if (ytype instanceof AbstractType ? ytype.doc !== this.doc : ytype !== this.doc) warn("[yjs#509] Not same Y.Doc"); // use MultiDocUndoManager instead. also see https://github.com/yjs/yjs/issues/509 this.scope.push(ytype); } }); } /** * @param {any} origin */ addTrackedOrigin(origin) { this.trackedOrigins.add(origin); } /** * @param {any} origin */ removeTrackedOrigin(origin) { this.trackedOrigins.delete(origin); } clear(clearUndoStack = true, clearRedoStack = true) { if ((clearUndoStack && this.canUndo()) || (clearRedoStack && this.canRedo())) { this.doc.transact((tr) => { if (clearUndoStack) { this.undoStack.forEach((item) => clearUndoManagerStackItem(tr, this, item)); this.undoStack = []; } if (clearRedoStack) { this.redoStack.forEach((item) => clearUndoManagerStackItem(tr, this, item)); this.redoStack = []; } this.emit("stack-cleared", [{ undoStackCleared: clearUndoStack, redoStackCleared: clearRedoStack }]); }); } } /** * UndoManager merges Undo-StackItem if they are created within time-gap * smaller than `options.captureTimeout`. Call `um.stopCapturing()` so that the next * StackItem won't be merged. * * * @example * // without stopCapturing * ytext.insert(0, 'a') * ytext.insert(1, 'b') * um.undo() * ytext.toString() // => '' (note that 'ab' was removed) * // with stopCapturing * ytext.insert(0, 'a') * um.stopCapturing() * ytext.insert(0, 'b') * um.undo() * ytext.toString() // => 'a' (note that only 'b' was removed) * */ stopCapturing() { this.lastChange = 0; } /** * Undo last changes on type. * * @return {StackItem?} Returns StackItem if a change was applied */ undo() { this.undoing = true; let res; try { res = popStackItem(this, this.undoStack, "undo"); } finally { this.undoing = false; } return res; } /** * Redo last undo operation. * * @return {StackItem?} Returns StackItem if a change was applied */ redo() { this.redoing = true; let res; try { res = popStackItem(this, this.redoStack, "redo"); } finally { this.redoing = false; } return res; } /** * Are undo steps available? * * @return {boolean} `true` if undo is possible */ canUndo() { return this.undoStack.length > 0; } /** * Are redo steps available? * * @return {boolean} `true` if redo is possible */ canRedo() { return this.redoStack.length > 0; } destroy() { this.trackedOrigins.delete(this); this.doc.off("afterTransaction", this.afterTransactionHandler); super.destroy(); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder */ function* lazyStructReaderGenerator(decoder) { const numOfStateUpdates = readVarUint(decoder.restDecoder); for (let i = 0; i < numOfStateUpdates; i++) { const numberOfStructs = readVarUint(decoder.restDecoder); const client = decoder.readClient(); let clock = readVarUint(decoder.restDecoder); for (let i = 0; i < numberOfStructs; i++) { const info = decoder.readInfo(); // @todo use switch instead of ifs if (info === 10) { const len = readVarUint(decoder.restDecoder); yield new Skip(createID(client, clock), len); clock += len; } else if ((BITS5 & info) !== 0) { const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0; // If parent = null and neither left nor right are defined, then we know that `parent` is child of `y` // and we read the next string as parentYKey. // It indicates how we store/retrieve parent from `y.share` // @type {string|null} const struct = new Item$1( createID(client, clock), null, // left (info & BIT8) === BIT8 ? decoder.readLeftID() : null, // origin null, // right (info & BIT7) === BIT7 ? decoder.readRightID() : null, // right origin // @ts-ignore Force writing a string here. cantCopyParentInfo ? decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID() : null, // parent cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, // parentSub readItemContent(decoder, info), // item content ); yield struct; clock += struct.length; } else { const len = decoder.readLen(); yield new GC(createID(client, clock), len); clock += len; } } } } class LazyStructReader { /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {boolean} filterSkips */ constructor(decoder, filterSkips) { this.gen = lazyStructReaderGenerator(decoder); /** * @type {null | Item | Skip | GC} */ this.curr = null; this.done = false; this.filterSkips = filterSkips; this.next(); } /** * @return {Item | GC | Skip |null} */ next() { // ignore "Skip" structs do { this.curr = this.gen.next().value || null; } while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip); return this.curr; } } class LazyStructWriter { /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ constructor(encoder) { this.currClient = 0; this.startClock = 0; this.written = 0; this.encoder = encoder; /** * We want to write operations lazily, but also we need to know beforehand how many operations we want to write for each client. * * This kind of meta-information (#clients, #structs-per-client-written) is written to the restEncoder. * * We fragment the restEncoder and store a slice of it per-client until we know how many clients there are. * When we flush (toUint8Array) we write the restEncoder using the fragments and the meta-information. * * @type {Array<{ written: number, restEncoder: Uint8Array }>} */ this.clientStructs = []; } } /** * @param {Array} updates * @return {Uint8Array} */ const mergeUpdates = (updates) => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1); /** * This method is intended to slice any kind of struct and retrieve the right part. * It does not handle side-effects, so it should only be used by the lazy-encoder. * * @param {Item | GC | Skip} left * @param {number} diff * @return {Item | GC} */ const sliceStruct = (left, diff) => { if (left.constructor === GC) { const { client, clock } = left.id; return new GC(createID(client, clock + diff), left.length - diff); } else if (left.constructor === Skip) { const { client, clock } = left.id; return new Skip(createID(client, clock + diff), left.length - diff); } else { const leftItem = /** @type {Item} */ (left); const { client, clock } = leftItem.id; return new Item$1(createID(client, clock + diff), null, createID(client, clock + diff - 1), null, leftItem.rightOrigin, leftItem.parent, leftItem.parentSub, leftItem.content.splice(diff)); } }; /** * * This function works similarly to `readUpdateV2`. * * @param {Array} updates * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder] * @return {Uint8Array} */ const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { if (updates.length === 1) { return updates[0]; } const updateDecoders = updates.map((update) => new YDecoder(createDecoder(update))); let lazyStructDecoders = updateDecoders.map((decoder) => new LazyStructReader(decoder, true)); /** * @todo we don't need offset because we always slice before * @type {null | { struct: Item | GC | Skip, offset: number }} */ let currWrite = null; const updateEncoder = new YEncoder(); // write structs lazily const lazyStructEncoder = new LazyStructWriter(updateEncoder); // Note: We need to ensure that all lazyStructDecoders are fully consumed // Note: Should merge document updates whenever possible - even from different updates // Note: Should handle that some operations cannot be applied yet () while (true) { // Write higher clients first ⇒ sort by clientID & clock and remove decoders without content lazyStructDecoders = lazyStructDecoders.filter((dec) => dec.curr !== null); lazyStructDecoders.sort( /** @type {function(any,any):number} */ (dec1, dec2) => { if (dec1.curr.id.client === dec2.curr.id.client) { const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock; if (clockDiff === 0) { // @todo remove references to skip since the structDecoders must filter Skips. return ( dec1.curr.constructor === dec2.curr.constructor ? 0 : dec1.curr.constructor === Skip ? 1 : -1 ); // we are filtering skips anyway. } else { return clockDiff; } } else { return dec2.curr.id.client - dec1.curr.id.client; } }, ); if (lazyStructDecoders.length === 0) { break; } const currDecoder = lazyStructDecoders[0]; // write from currDecoder until the next operation is from another client or if filler-struct // then we need to reorder the decoders and find the next operation to write const firstClient = /** @type {Item | GC} */ (currDecoder.curr).id.client; if (currWrite !== null) { let curr = /** @type {Item | GC | null} */ (currDecoder.curr); let iterated = false; // iterate until we find something that we haven't written already // remember: first the high client-ids are written while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) { curr = currDecoder.next(); iterated = true; } if ( curr === null || // current decoder is empty curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient` (iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates ) { continue; } if (firstClient !== currWrite.struct.id.client) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); currWrite = { struct: curr, offset: 0 }; currDecoder.next(); } else { if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) { // @todo write currStruct & set currStruct = Skip(clock = currStruct.id.clock + currStruct.length, length = curr.id.clock - self.clock) if (currWrite.struct.constructor === Skip) { // extend existing skip currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock; } else { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length; /** * @type {Skip} */ const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff); currWrite = { struct, offset: 0 }; } } else { // if (currWrite.struct.id.clock + currWrite.struct.length >= curr.id.clock) { const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock; if (diff > 0) { if (currWrite.struct.constructor === Skip) { // prefer to slice Skip because the other struct might contain more information currWrite.struct.length -= diff; } else { curr = sliceStruct(curr, diff); } } if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); currWrite = { struct: curr, offset: 0 }; currDecoder.next(); } } } } else { currWrite = { struct: /** @type {Item | GC} */ (currDecoder.curr), offset: 0 }; currDecoder.next(); } for ( let next = currDecoder.curr; next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip; next = currDecoder.next() ) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); currWrite = { struct: next, offset: 0 }; } } if (currWrite !== null) { writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset); currWrite = null; } finishLazyStructWriting(lazyStructEncoder); const dss = updateDecoders.map((decoder) => readDeleteSet(decoder)); const ds = mergeDeleteSets(dss); writeDeleteSet(updateEncoder, ds); return updateEncoder.toUint8Array(); }; /** * @param {Uint8Array} update * @param {Uint8Array} sv * @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder] * @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder] */ const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => { const state = decodeStateVector(sv); const encoder = new YEncoder(); const lazyStructWriter = new LazyStructWriter(encoder); const decoder = new YDecoder(createDecoder(update)); const reader = new LazyStructReader(decoder, false); while (reader.curr) { const curr = reader.curr; const currClient = curr.id.client; const svClock = state.get(currClient) || 0; if (reader.curr.constructor === Skip) { // the first written struct shouldn't be a skip reader.next(); continue; } if (curr.id.clock + curr.length > svClock) { writeStructToLazyStructWriter(lazyStructWriter, curr, max$1(svClock - curr.id.clock, 0)); reader.next(); while (reader.curr && reader.curr.id.client === currClient) { writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0); reader.next(); } } else { // read until something new comes up while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) { reader.next(); } } } finishLazyStructWriting(lazyStructWriter); // write ds const ds = readDeleteSet(decoder); writeDeleteSet(encoder, ds); return encoder.toUint8Array(); }; /** * @param {LazyStructWriter} lazyWriter */ const flushLazyStructWriter = (lazyWriter) => { if (lazyWriter.written > 0) { lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: toUint8Array(lazyWriter.encoder.restEncoder) }); lazyWriter.encoder.restEncoder = createEncoder(); lazyWriter.written = 0; } }; /** * @param {LazyStructWriter} lazyWriter * @param {Item | GC} struct * @param {number} offset */ const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => { // flush curr if we start another client if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) { flushLazyStructWriter(lazyWriter); } if (lazyWriter.written === 0) { lazyWriter.currClient = struct.id.client; // write next client lazyWriter.encoder.writeClient(struct.id.client); // write startClock writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset); } struct.write(lazyWriter.encoder, offset); lazyWriter.written++; }; /** * Call this function when we collected all parts and want to * put all the parts together. After calling this method, * you can continue using the UpdateEncoder. * * @param {LazyStructWriter} lazyWriter */ const finishLazyStructWriting = (lazyWriter) => { flushLazyStructWriter(lazyWriter); // this is a fresh encoder because we called flushCurr const restEncoder = lazyWriter.encoder.restEncoder; /** * Now we put all the fragments together. * This works similarly to `writeClientsStructs` */ // write # states that were updated - i.e. the clients writeVarUint(restEncoder, lazyWriter.clientStructs.length); for (let i = 0; i < lazyWriter.clientStructs.length; i++) { const partStructs = lazyWriter.clientStructs[i]; /** * Works similarly to `writeStructs` */ // write # encoded structs writeVarUint(restEncoder, partStructs.written); // write the rest of the fragment writeUint8Array(restEncoder, partStructs.restEncoder); } }; /** * @param {Uint8Array} update * @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer * @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder * @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder */ const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => { const updateDecoder = new YDecoder(createDecoder(update)); const lazyDecoder = new LazyStructReader(updateDecoder, false); const updateEncoder = new YEncoder(); const lazyWriter = new LazyStructWriter(updateEncoder); for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) { writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0); } finishLazyStructWriting(lazyWriter); const ds = readDeleteSet(updateDecoder); writeDeleteSet(updateEncoder, ds); return updateEncoder.toUint8Array(); }; /** * @param {Uint8Array} update */ const convertUpdateFormatV2ToV1 = (update) => convertUpdateFormat(update, id, UpdateDecoderV2, UpdateEncoderV1); const errorComputeChanges = "You must not compute changes after the event-handler fired."; /** * @template {AbstractType} T * YEvent describes the changes on a YType. */ class YEvent { /** * @param {T} target The changed type. * @param {Transaction} transaction */ constructor(target, transaction) { /** * The type on which this event was created on. * @type {T} */ this.target = target; /** * The current target on which the observe callback is called. * @type {AbstractType} */ this.currentTarget = target; /** * The transaction that triggered this event. * @type {Transaction} */ this.transaction = transaction; /** * @type {Object|null} */ this._changes = null; /** * @type {null | Map} */ this._keys = null; /** * @type {null | Array<{ insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object }>} */ this._delta = null; /** * @type {Array|null} */ this._path = null; } /** * Computes the path from `y` to the changed type. * * @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with. * * The following property holds: * @example * let type = y * event.path.forEach(dir => { * type = type.get(dir) * }) * type === event.target // => true */ get path() { return this._path || (this._path = getPathTo(this.currentTarget, this.target)); } /** * Check if a struct is deleted by this event. * * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. * * @param {AbstractStruct} struct * @return {boolean} */ deletes(struct) { return isDeleted(this.transaction.deleteSet, struct.id); } /** * @type {Map} */ get keys() { if (this._keys === null) { if (this.transaction.doc._transactionCleanups.length === 0) { throw create$7(errorComputeChanges); } const keys = new Map(); const target = this.target; const changed = /** @type Set */ (this.transaction.changed.get(target)); changed.forEach((key) => { if (key !== null) { const item = /** @type {Item} */ (target._map.get(key)); /** * @type {'delete' | 'add' | 'update'} */ let action; let oldValue; if (this.adds(item)) { let prev = item.left; while (prev !== null && this.adds(prev)) { prev = prev.left; } if (this.deletes(item)) { if (prev !== null && this.deletes(prev)) { action = "delete"; oldValue = last(prev.content.getContent()); } else { return; } } else { if (prev !== null && this.deletes(prev)) { action = "update"; oldValue = last(prev.content.getContent()); } else { action = "add"; oldValue = undefined; } } } else { if (this.deletes(item)) { action = "delete"; oldValue = last(/** @type {Item} */ item.content.getContent()); } else { return; // nop } } keys.set(key, { action, oldValue }); } }); this._keys = keys; } return this._keys; } /** * This is a computed property. Note that this can only be safely computed during the * event call. Computing this property after other changes happened might result in * unexpected behavior (incorrect computation of deltas). A safe way to collect changes * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. * * @type {Array<{insert?: string | Array | object | AbstractType, retain?: number, delete?: number, attributes?: Object}>} */ get delta() { return this.changes.delta; } /** * Check if a struct is added by this event. * * In contrast to change.deleted, this method also returns true if the struct was added and then deleted. * * @param {AbstractStruct} struct * @return {boolean} */ adds(struct) { return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0); } /** * This is a computed property. Note that this can only be safely computed during the * event call. Computing this property after other changes happened might result in * unexpected behavior (incorrect computation of deltas). A safe way to collect changes * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. * * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} */ get changes() { let changes = this._changes; if (changes === null) { if (this.transaction.doc._transactionCleanups.length === 0) { throw create$7(errorComputeChanges); } const target = this.target; const added = create$8(); const deleted = create$8(); /** * @type {Array<{insert:Array}|{delete:number}|{retain:number}>} */ const delta = []; changes = { added, deleted, delta, keys: this.keys, }; const changed = /** @type Set */ (this.transaction.changed.get(target)); if (changed.has(null)) { /** * @type {any} */ let lastOp = null; const packOp = () => { if (lastOp) { delta.push(lastOp); } }; for (let item = target._start; item !== null; item = item.right) { if (item.deleted) { if (this.deletes(item) && !this.adds(item)) { if (lastOp === null || lastOp.delete === undefined) { packOp(); lastOp = { delete: 0 }; } lastOp.delete += item.length; deleted.add(item); } // else nop } else { if (this.adds(item)) { if (lastOp === null || lastOp.insert === undefined) { packOp(); lastOp = { insert: [] }; } lastOp.insert = lastOp.insert.concat(item.content.getContent()); added.add(item); } else { if (lastOp === null || lastOp.retain === undefined) { packOp(); lastOp = { retain: 0 }; } lastOp.retain += item.length; } } } if (lastOp !== null && lastOp.retain === undefined) { packOp(); } } this._changes = changes; } return /** @type {any} */ (changes); } } /** * Compute the path from this type to the specified target. * * @example * // `child` should be accessible via `type.get(path[0]).get(path[1])..` * const path = type.getPathTo(child) * // assuming `type instanceof YArray` * console.log(path) // might look like => [2, 'key1'] * child === type.get(path[0]).get(path[1]) * * @param {AbstractType} parent * @param {AbstractType} child target * @return {Array} Path to the target * * @private * @function */ const getPathTo = (parent, child) => { const path = []; while (child._item !== null && child !== parent) { if (child._item.parentSub !== null) { // parent is map-ish path.unshift(child._item.parentSub); } else { // parent is array-ish let i = 0; let c = /** @type {AbstractType} */ (child._item.parent)._start; while (c !== child._item && c !== null) { if (!c.deleted && c.countable) { i += c.length; } c = c.right; } path.unshift(i); } child = /** @type {AbstractType} */ (child._item.parent); } return path; }; /** * https://docs.yjs.dev/getting-started/working-with-shared-types#caveats */ const warnPrematureAccess = () => { warn("Invalid access: Add Yjs type to a document before reading data."); }; const maxSearchMarker = 80; /** * A unique timestamp that identifies each marker. * * Time is relative,.. this is more like an ever-increasing clock. * * @type {number} */ let globalSearchMarkerTimestamp = 0; class ArraySearchMarker { /** * @param {Item} p * @param {number} index */ constructor(p, index) { p.marker = true; this.p = p; this.index = index; this.timestamp = globalSearchMarkerTimestamp++; } } /** * @param {ArraySearchMarker} marker */ const refreshMarkerTimestamp = (marker) => { marker.timestamp = globalSearchMarkerTimestamp++; }; /** * This is rather complex so this function is the only thing that should overwrite a marker * * @param {ArraySearchMarker} marker * @param {Item} p * @param {number} index */ const overwriteMarker = (marker, p, index) => { marker.p.marker = false; marker.p = p; p.marker = true; marker.index = index; marker.timestamp = globalSearchMarkerTimestamp++; }; /** * @param {Array} searchMarker * @param {Item} p * @param {number} index */ const markPosition = (searchMarker, p, index) => { if (searchMarker.length >= maxSearchMarker) { // override oldest marker (we don't want to create more objects) const marker = searchMarker.reduce((a, b) => (a.timestamp < b.timestamp ? a : b)); overwriteMarker(marker, p, index); return marker; } else { // create new marker const pm = new ArraySearchMarker(p, index); searchMarker.push(pm); return pm; } }; /** * Search marker help us to find positions in the associative array faster. * * They speed up the process of finding a position without much bookkeeping. * * A maximum of `maxSearchMarker` objects are created. * * This function always returns a refreshed marker (updated timestamp) * * @param {AbstractType} yarray * @param {number} index */ const findMarker = (yarray, index) => { if (yarray._start === null || index === 0 || yarray._searchMarker === null) { return null; } const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => (abs(index - a.index) < abs(index - b.index) ? a : b)); let p = yarray._start; let pindex = 0; if (marker !== null) { p = marker.p; pindex = marker.index; refreshMarkerTimestamp(marker); // we used it, we might need to use it again } // iterate to right if possible while (p.right !== null && pindex < index) { if (!p.deleted && p.countable) { if (index < pindex + p.length) { break; } pindex += p.length; } p = p.right; } // iterate to left if necessary (might be that pindex > index) while (p.left !== null && pindex > index) { p = p.left; if (!p.deleted && p.countable) { pindex -= p.length; } } // we want to make sure that p can't be merged with left, because that would screw up everything // in that cas just return what we have (it is most likely the best marker anyway) // iterate to left until p can't be merged with left while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) { p = p.left; if (!p.deleted && p.countable) { pindex -= p.length; } } // @todo remove! // assure position // { // let start = yarray._start // let pos = 0 // while (start !== p) { // if (!start.deleted && start.countable) { // pos += start.length // } // start = /** @type {Item} */ (start.right) // } // if (pos !== pindex) { // debugger // throw new Error('Gotcha position fail!') // } // } // if (marker) { // if (window.lengths == null) { // window.lengths = [] // window.getLengths = () => window.lengths.sort((a, b) => a - b) // } // window.lengths.push(marker.index - pindex) // console.log('distance', marker.index - pindex, 'len', p && p.parent.length) // } if (marker !== null && abs(marker.index - pindex) < /** @type {YText|YArray} */ (p.parent).length / maxSearchMarker) { // adjust existing marker overwriteMarker(marker, p, pindex); return marker; } else { // create new marker return markPosition(yarray._searchMarker, p, pindex); } }; /** * Update markers when a change happened. * * This should be called before doing a deletion! * * @param {Array} searchMarker * @param {number} index * @param {number} len If insertion, len is positive. If deletion, len is negative. */ const updateMarkerChanges = (searchMarker, index, len) => { for (let i = searchMarker.length - 1; i >= 0; i--) { const m = searchMarker[i]; if (len > 0) { /** * @type {Item|null} */ let p = m.p; p.marker = false; // Ideally we just want to do a simple position comparison, but this will only work if // search markers don't point to deleted items for formats. // Iterate marker to prev undeleted countable position so we know what to do when updating a position while (p && (p.deleted || !p.countable)) { p = p.left; if (p && !p.deleted && p.countable) { // adjust position. the loop should break now m.index -= p.length; } } if (p === null || p.marker === true) { // remove search marker if updated position is null or if position is already marked searchMarker.splice(i, 1); continue; } m.p = p; p.marker = true; } if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice m.index = max$1(index, m.index + len); } } }; /** * Call event listeners with an event. This will also add an event to all * parents (for `.observeDeep` handlers). * * @template EventType * @param {AbstractType} type * @param {Transaction} transaction * @param {EventType} event */ const callTypeObservers = (type, transaction, event) => { const changedType = type; const changedParentTypes = transaction.changedParentTypes; while (true) { // @ts-ignore setIfUndefined(changedParentTypes, type, () => []).push(event); if (type._item === null) { break; } type = /** @type {AbstractType} */ (type._item.parent); } callEventHandlerListeners(changedType._eH, event, transaction); }; /** * @template EventType * Abstract Yjs Type class */ class AbstractType { constructor() { /** * @type {Item|null} */ this._item = null; /** * @type {Map} */ this._map = new Map(); /** * @type {Item|null} */ this._start = null; /** * @type {Doc|null} */ this.doc = null; this._length = 0; /** * Event handlers * @type {EventHandler} */ this._eH = createEventHandler(); /** * Deep event handlers * @type {EventHandler>,Transaction>} */ this._dEH = createEventHandler(); /** * @type {null | Array} */ this._searchMarker = null; } /** * @return {AbstractType|null} */ get parent() { return this._item ? /** @type {AbstractType} */ (this._item.parent) : null; } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item|null} item */ _integrate(y, item) { this.doc = y; this._item = item; } /** * @return {AbstractType} */ _copy() { throw methodUnimplemented(); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {AbstractType} */ clone() { throw methodUnimplemented(); } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder */ _write(_encoder) {} /** * The first non-deleted item */ get _first() { let n = this._start; while (n !== null && n.deleted) { n = n.right; } return n; } /** * Creates YEvent and calls all type observers. * Must be implemented by each type. * * @param {Transaction} transaction * @param {Set} _parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver(transaction, _parentSubs) { if (!transaction.local && this._searchMarker) { this._searchMarker.length = 0; } } /** * Observe all events that are created on this type. * * @param {function(EventType, Transaction):void} f Observer function */ observe(f) { addEventHandlerListener(this._eH, f); } /** * Observe all events that are created by this type and its children. * * @param {function(Array>,Transaction):void} f Observer function */ observeDeep(f) { addEventHandlerListener(this._dEH, f); } /** * Unregister an observer function. * * @param {function(EventType,Transaction):void} f Observer function */ unobserve(f) { removeEventHandlerListener(this._eH, f); } /** * Unregister an observer function. * * @param {function(Array>,Transaction):void} f Observer function */ unobserveDeep(f) { removeEventHandlerListener(this._dEH, f); } /** * @abstract * @return {any} */ toJSON() {} } /** * @param {AbstractType} type * @param {number} start * @param {number} end * @return {Array} * * @private * @function */ const typeListSlice = (type, start, end) => { type.doc ?? warnPrematureAccess(); if (start < 0) { start = type._length + start; } if (end < 0) { end = type._length + end; } let len = end - start; const cs = []; let n = type._start; while (n !== null && len > 0) { if (n.countable && !n.deleted) { const c = n.content.getContent(); if (c.length <= start) { start -= c.length; } else { for (let i = start; i < c.length && len > 0; i++) { cs.push(c[i]); len--; } start = 0; } } n = n.right; } return cs; }; /** * @param {AbstractType} type * @return {Array} * * @private * @function */ const typeListToArray = (type) => { type.doc ?? warnPrematureAccess(); const cs = []; let n = type._start; while (n !== null) { if (n.countable && !n.deleted) { const c = n.content.getContent(); for (let i = 0; i < c.length; i++) { cs.push(c[i]); } } n = n.right; } return cs; }; /** * @param {AbstractType} type * @param {Snapshot} snapshot * @return {Array} * * @private * @function */ const typeListToArraySnapshot = (type, snapshot) => { const cs = []; let n = type._start; while (n !== null) { if (n.countable && isVisible$1(n, snapshot)) { const c = n.content.getContent(); for (let i = 0; i < c.length; i++) { cs.push(c[i]); } } n = n.right; } return cs; }; /** * Executes a provided function on once on every element of this YArray. * * @param {AbstractType} type * @param {function(any,number,any):void} f A function to execute on every element of this YArray. * * @private * @function */ const typeListForEach = (type, f) => { let index = 0; let n = type._start; type.doc ?? warnPrematureAccess(); while (n !== null) { if (n.countable && !n.deleted) { const c = n.content.getContent(); for (let i = 0; i < c.length; i++) { f(c[i], index++, type); } } n = n.right; } }; /** * @template C,R * @param {AbstractType} type * @param {function(C,number,AbstractType):R} f * @return {Array} * * @private * @function */ const typeListMap = (type, f) => { /** * @type {Array} */ const result = []; typeListForEach(type, (c, i) => { result.push(f(c, i, type)); }); return result; }; /** * @param {AbstractType} type * @return {IterableIterator} * * @private * @function */ const typeListCreateIterator = (type) => { let n = type._start; /** * @type {Array|null} */ let currentContent = null; let currentContentIndex = 0; return { [Symbol.iterator]() { return this; }, next: () => { // find some content if (currentContent === null) { while (n !== null && n.deleted) { n = n.right; } // check if we reached the end, no need to check currentContent, because it does not exist if (n === null) { return { done: true, value: undefined, }; } // we found n, so we can set currentContent currentContent = n.content.getContent(); currentContentIndex = 0; n = n.right; // we used the content of n, now iterate to next } const value = currentContent[currentContentIndex++]; // check if we need to empty currentContent if (currentContent.length <= currentContentIndex) { currentContent = null; } return { done: false, value, }; }, }; }; /** * @param {AbstractType} type * @param {number} index * @return {any} * * @private * @function */ const typeListGet = (type, index) => { type.doc ?? warnPrematureAccess(); const marker = findMarker(type, index); let n = type._start; if (marker !== null) { n = marker.p; index -= marker.index; } for (; n !== null; n = n.right) { if (!n.deleted && n.countable) { if (index < n.length) { return n.content.getContent()[index]; } index -= n.length; } } }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {Item?} referenceItem * @param {Array|Array|boolean|number|null|string|Uint8Array>} content * * @private * @function */ const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { let left = referenceItem; const doc = transaction.doc; const ownClientId = doc.clientID; const store = doc.store; const right = referenceItem === null ? parent._start : referenceItem.right; /** * @type {Array|number|null>} */ let jsonContent = []; const packJsonContent = () => { if (jsonContent.length > 0) { left = new Item$1(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)); left.integrate(transaction, 0); jsonContent = []; } }; content.forEach((c) => { if (c === null) { jsonContent.push(c); } else { switch (c.constructor) { case Number: case Object: case Boolean: case Array: case String: jsonContent.push(c); break; default: packJsonContent(); switch (c.constructor) { case Uint8Array: case ArrayBuffer: left = new Item$1( createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))), ); left.integrate(transaction, 0); break; case Doc: left = new Item$1( createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)), ); left.integrate(transaction, 0); break; default: if (c instanceof AbstractType) { left = new Item$1(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c)); left.integrate(transaction, 0); } else { throw new Error("Unexpected content type in insert operation"); } } } } }); packJsonContent(); }; const lengthExceeded = () => create$7("Length exceeded!"); /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @param {Array|Array|number|null|string|Uint8Array>} content * * @private * @function */ const typeListInsertGenerics = (transaction, parent, index, content) => { if (index > parent._length) { throw lengthExceeded(); } if (index === 0) { if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, index, content.length); } return typeListInsertGenericsAfter(transaction, parent, null, content); } const startIndex = index; const marker = findMarker(parent, index); let n = parent._start; if (marker !== null) { n = marker.p; index -= marker.index; // we need to iterate one to the left so that the algorithm works if (index === 0) { // @todo refactor this as it actually doesn't consider formats n = n.prev; // important! get the left undeleted item so that we can actually decrease index index += n && n.countable && !n.deleted ? n.length : 0; } } for (; n !== null; n = n.right) { if (!n.deleted && n.countable) { if (index <= n.length) { if (index < n.length) { // insert in-between getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); } break; } index -= n.length; } } if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, startIndex, content.length); } return typeListInsertGenericsAfter(transaction, parent, n, content); }; /** * Pushing content is special as we generally want to push after the last item. So we don't have to update * the search marker. * * @param {Transaction} transaction * @param {AbstractType} parent * @param {Array|Array|number|null|string|Uint8Array>} content * * @private * @function */ const typeListPushGenerics = (transaction, parent, content) => { // Use the marker with the highest index and iterate to the right. const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => (currMarker.index > maxMarker.index ? currMarker : maxMarker), { index: 0, p: parent._start }); let n = marker.p; if (n) { while (n.right) { n = n.right; } } return typeListInsertGenericsAfter(transaction, parent, n, content); }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @param {number} length * * @private * @function */ const typeListDelete = (transaction, parent, index, length) => { if (length === 0) { return; } const startIndex = index; const startLength = length; const marker = findMarker(parent, index); let n = parent._start; if (marker !== null) { n = marker.p; index -= marker.index; } // compute the first item to be deleted for (; n !== null && index > 0; n = n.right) { if (!n.deleted && n.countable) { if (index < n.length) { getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)); } index -= n.length; } } // delete all items until done while (length > 0 && n !== null) { if (!n.deleted) { if (length < n.length) { getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length)); } n.delete(transaction); length -= n.length; } n = n.right; } if (length > 0) { throw lengthExceeded(); } if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */); } }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {string} key * * @private * @function */ const typeMapDelete = (transaction, parent, key) => { const c = parent._map.get(key); if (c !== undefined) { c.delete(transaction); } }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {string} key * @param {Object|number|null|Array|string|Uint8Array|AbstractType} value * * @private * @function */ const typeMapSet = (transaction, parent, key, value) => { const left = parent._map.get(key) || null; const doc = transaction.doc; const ownClientId = doc.clientID; let content; if (value == null) { content = new ContentAny([value]); } else { switch (value.constructor) { case Number: case Object: case Boolean: case Array: case String: case Date: case BigInt: content = new ContentAny([value]); break; case Uint8Array: content = new ContentBinary(/** @type {Uint8Array} */ (value)); break; case Doc: content = new ContentDoc(/** @type {Doc} */ (value)); break; default: if (value instanceof AbstractType) { content = new ContentType(value); } else { throw new Error("Unexpected content type"); } } } new Item$1(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0); }; /** * @param {AbstractType} parent * @param {string} key * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined} * * @private * @function */ const typeMapGet = (parent, key) => { parent.doc ?? warnPrematureAccess(); const val = parent._map.get(key); return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined; }; /** * @param {AbstractType} parent * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined>} * * @private * @function */ const typeMapGetAll = (parent) => { /** * @type {Object} */ const res = {}; parent.doc ?? warnPrematureAccess(); parent._map.forEach((value, key) => { if (!value.deleted) { res[key] = value.content.getContent()[value.length - 1]; } }); return res; }; /** * @param {AbstractType} parent * @param {string} key * @return {boolean} * * @private * @function */ const typeMapHas = (parent, key) => { parent.doc ?? warnPrematureAccess(); const val = parent._map.get(key); return val !== undefined && !val.deleted; }; /** * @param {AbstractType} parent * @param {Snapshot} snapshot * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined>} * * @private * @function */ const typeMapGetAllSnapshot = (parent, snapshot) => { /** * @type {Object} */ const res = {}; parent._map.forEach((value, key) => { /** * @type {Item|null} */ let v = value; while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { v = v.left; } if (v !== null && isVisible$1(v, snapshot)) { res[key] = v.content.getContent()[v.length - 1]; } }); return res; }; /** * @param {AbstractType & { _map: Map }} type * @return {IterableIterator>} * * @private * @function */ const createMapIterator = (type) => { type.doc ?? warnPrematureAccess(); return iteratorFilter(type._map.entries(), /** @param {any} entry */ (entry) => !entry[1].deleted); }; /** * @module YArray */ /** * Event that describes the changes on a YArray * @template T * @extends YEvent> */ class YArrayEvent extends YEvent {} /** * A shared Array implementation. * @template T * @extends AbstractType> * @implements {Iterable} */ class YArray extends AbstractType { constructor() { super(); /** * @type {Array?} * @private */ this._prelimContent = []; /** * @type {Array} */ this._searchMarker = []; } /** * Construct a new YArray containing the specified items. * @template {Object|Array|number|null|string|Uint8Array} T * @param {Array} items * @return {YArray} */ static from(items) { /** * @type {YArray} */ const a = new YArray(); a.push(items); return a; } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate(y, item) { super._integrate(y, item); this.insert(0, /** @type {Array} */ (this._prelimContent)); this._prelimContent = null; } /** * @return {YArray} */ _copy() { return new YArray(); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {YArray} */ clone() { /** * @type {YArray} */ const arr = new YArray(); arr.insert( 0, this.toArray().map((el) => (el instanceof AbstractType ? /** @type {typeof el} */ (el.clone()) : el)), ); return arr; } get length() { this.doc ?? warnPrematureAccess(); return this._length; } /** * Creates YArrayEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver(transaction, parentSubs) { super._callObserver(transaction, parentSubs); callTypeObservers(this, transaction, new YArrayEvent(this, transaction)); } /** * Inserts new content at an index. * * Important: This function expects an array of content. Not just a content * object. The reason for this "weirdness" is that inserting several elements * is very efficient when it is done as a single operation. * * @example * // Insert character 'a' at position 0 * yarray.insert(0, ['a']) * // Insert numbers 1, 2 at position 1 * yarray.insert(1, [1, 2]) * * @param {number} index The index to insert content at. * @param {Array} content The array of content */ insert(index, content) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content)); }); } else { /** @type {Array} */ (this._prelimContent).splice(index, 0, ...content); } } /** * Appends content to this YArray. * * @param {Array} content Array of content to append. * * @todo Use the following implementation in all types. */ push(content) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeListPushGenerics(transaction, this, /** @type {any} */ (content)); }); } else { /** @type {Array} */ (this._prelimContent).push(...content); } } /** * Prepends content to this YArray. * * @param {Array} content Array of content to prepend. */ unshift(content) { this.insert(0, content); } /** * Deletes elements starting from an index. * * @param {number} index Index at which to start deleting elements * @param {number} length The number of elements to remove. Defaults to 1. */ delete(index, length = 1) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeListDelete(transaction, this, index, length); }); } else { /** @type {Array} */ (this._prelimContent).splice(index, length); } } /** * Returns the i-th element from a YArray. * * @param {number} index The index of the element to return from the YArray * @return {T} */ get(index) { return typeListGet(this, index); } /** * Transforms this YArray to a JavaScript Array. * * @return {Array} */ toArray() { return typeListToArray(this); } /** * Returns a portion of this YArray into a JavaScript Array selected * from start to end (end not included). * * @param {number} [start] * @param {number} [end] * @return {Array} */ slice(start = 0, end = this.length) { return typeListSlice(this, start, end); } /** * Transforms this Shared Type to a JSON object. * * @return {Array} */ toJSON() { return this.map((c) => (c instanceof AbstractType ? c.toJSON() : c)); } /** * Returns an Array with the result of calling a provided function on every * element of this YArray. * * @template M * @param {function(T,number,YArray):M} f Function that produces an element of the new Array * @return {Array} A new array with each element being the result of the * callback function */ map(f) { return typeListMap(this, /** @type {any} */ (f)); } /** * Executes a provided function once on every element of this YArray. * * @param {function(T,number,YArray):void} f A function to execute on every element of this YArray. */ forEach(f) { typeListForEach(this, f); } /** * @return {IterableIterator} */ [Symbol.iterator]() { return typeListCreateIterator(this); } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write(encoder) { encoder.writeTypeRef(YArrayRefID); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * * @private * @function */ const readYArray = (_decoder) => new YArray(); /** * @module YMap */ /** * @template T * @extends YEvent> * Event that describes the changes on a YMap. */ class YMapEvent extends YEvent { /** * @param {YMap} ymap The YArray that changed. * @param {Transaction} transaction * @param {Set} subs The keys that changed. */ constructor(ymap, transaction, subs) { super(ymap, transaction); this.keysChanged = subs; } } /** * @template MapType * A shared Map implementation. * * @extends AbstractType> * @implements {Iterable<[string, MapType]>} */ class YMap extends AbstractType { /** * * @param {Iterable=} entries - an optional iterable to initialize the YMap */ constructor(entries) { super(); /** * @type {Map?} * @private */ this._prelimContent = null; if (entries === undefined) { this._prelimContent = new Map(); } else { this._prelimContent = new Map(entries); } } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate(y, item) { super._integrate(y, item); /** @type {Map} */ (this._prelimContent).forEach((value, key) => { this.set(key, value); }); this._prelimContent = null; } /** * @return {YMap} */ _copy() { return new YMap(); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {YMap} */ clone() { /** * @type {YMap} */ const map = new YMap(); this.forEach((value, key) => { map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value); }); return map; } /** * Creates YMapEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver(transaction, parentSubs) { callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs)); } /** * Transforms this Shared Type to a JSON object. * * @return {Object} */ toJSON() { this.doc ?? warnPrematureAccess(); /** * @type {Object} */ const map = {}; this._map.forEach((item, key) => { if (!item.deleted) { const v = item.content.getContent()[item.length - 1]; map[key] = v instanceof AbstractType ? v.toJSON() : v; } }); return map; } /** * Returns the size of the YMap (count of key/value pairs) * * @return {number} */ get size() { return [...createMapIterator(this)].length; } /** * Returns the keys for each element in the YMap Type. * * @return {IterableIterator} */ keys() { return iteratorMap(createMapIterator(this), /** @param {any} v */ (v) => v[0]); } /** * Returns the values for each element in the YMap Type. * * @return {IterableIterator} */ values() { return iteratorMap(createMapIterator(this), /** @param {any} v */ (v) => v[1].content.getContent()[v[1].length - 1]); } /** * Returns an Iterator of [key, value] pairs * * @return {IterableIterator<[string, MapType]>} */ entries() { return iteratorMap(createMapIterator(this), /** @param {any} v */ (v) => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]])); } /** * Executes a provided function on once on every key-value pair. * * @param {function(MapType,string,YMap):void} f A function to execute on every element of this YArray. */ forEach(f) { this.doc ?? warnPrematureAccess(); this._map.forEach((item, key) => { if (!item.deleted) { f(item.content.getContent()[item.length - 1], key, this); } }); } /** * Returns an Iterator of [key, value] pairs * * @return {IterableIterator<[string, MapType]>} */ [Symbol.iterator]() { return this.entries(); } /** * Remove a specified element from this YMap. * * @param {string} key The key of the element to remove. */ delete(key) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeMapDelete(transaction, this, key); }); } else { /** @type {Map} */ (this._prelimContent).delete(key); } } /** * Adds or updates an element with a specified key and value. * @template {MapType} VAL * * @param {string} key The key of the element to add to this YMap * @param {VAL} value The value of the element to add * @return {VAL} */ set(key, value) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeMapSet(transaction, this, key, /** @type {any} */ (value)); }); } else { /** @type {Map} */ (this._prelimContent).set(key, value); } return value; } /** * Returns a specified element from this YMap. * * @param {string} key * @return {MapType|undefined} */ get(key) { return /** @type {any} */ (typeMapGet(this, key)); } /** * Returns a boolean indicating whether the specified key exists or not. * * @param {string} key The key to test. * @return {boolean} */ has(key) { return typeMapHas(this, key); } /** * Removes all elements from this YMap. */ clear() { if (this.doc !== null) { transact(this.doc, (transaction) => { this.forEach(function (_value, key, map) { typeMapDelete(transaction, map, key); }); }); } else { /** @type {Map} */ (this._prelimContent).clear(); } } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write(encoder) { encoder.writeTypeRef(YMapRefID); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * * @private * @function */ const readYMap = (_decoder) => new YMap(); /** * @module YText */ /** * @param {any} a * @param {any} b * @return {boolean} */ const equalAttrs$1 = (a, b) => a === b || (typeof a === "object" && typeof b === "object" && a && b && equalFlat(a, b)); class ItemTextListPosition { /** * @param {Item|null} left * @param {Item|null} right * @param {number} index * @param {Map} currentAttributes */ constructor(left, right, index, currentAttributes) { this.left = left; this.right = right; this.index = index; this.currentAttributes = currentAttributes; } /** * Only call this if you know that this.right is defined */ forward() { if (this.right === null) { unexpectedCase(); } switch (this.right.content.constructor) { case ContentFormat: if (!this.right.deleted) { updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content)); } break; default: if (!this.right.deleted) { this.index += this.right.length; } break; } this.left = this.right; this.right = this.right.right; } } /** * @param {Transaction} transaction * @param {ItemTextListPosition} pos * @param {number} count steps to move forward * @return {ItemTextListPosition} * * @private * @function */ const findNextPosition = (transaction, pos, count) => { while (pos.right !== null && count > 0) { switch (pos.right.content.constructor) { case ContentFormat: if (!pos.right.deleted) { updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content)); } break; default: if (!pos.right.deleted) { if (count < pos.right.length) { // split right getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count)); } pos.index += pos.right.length; count -= pos.right.length; } break; } pos.left = pos.right; pos.right = pos.right.right; // pos.forward() - we don't forward because that would halve the performance because we already do the checks above } return pos; }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {number} index * @param {boolean} useSearchMarker * @return {ItemTextListPosition} * * @private * @function */ const findPosition = (transaction, parent, index, useSearchMarker) => { const currentAttributes = new Map(); const marker = useSearchMarker ? findMarker(parent, index) : null; if (marker) { const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes); return findNextPosition(transaction, pos, index - marker.index); } else { const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes); return findNextPosition(transaction, pos, index); } }; /** * Negate applied formats * * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {Map} negatedAttributes * * @private * @function */ const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => { // check if we really need to remove attributes while ( currPos.right !== null && (currPos.right.deleted === true || (currPos.right.content.constructor === ContentFormat && equalAttrs$1(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value))) ) { if (!currPos.right.deleted) { negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key); } currPos.forward(); } const doc = transaction.doc; const ownClientId = doc.clientID; negatedAttributes.forEach((val, key) => { const left = currPos.left; const right = currPos.right; const nextFormat = new Item$1(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); nextFormat.integrate(transaction, 0); currPos.right = nextFormat; currPos.forward(); }); }; /** * @param {Map} currentAttributes * @param {ContentFormat} format * * @private * @function */ const updateCurrentAttributes = (currentAttributes, format) => { const { key, value } = format; if (value === null) { currentAttributes.delete(key); } else { currentAttributes.set(key, value); } }; /** * @param {ItemTextListPosition} currPos * @param {Object} attributes * * @private * @function */ const minimizeAttributeChanges = (currPos, attributes) => { // go right while attributes[right.key] === right.value (or right is deleted) while (true) { if (currPos.right === null) { break; } else if ( currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs$1(attributes[/** @type {ContentFormat} */ (currPos.right.content).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value)) ); else { break; } currPos.forward(); } }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {Object} attributes * @return {Map} * * @private * @function **/ const insertAttributes = (transaction, parent, currPos, attributes) => { const doc = transaction.doc; const ownClientId = doc.clientID; const negatedAttributes = new Map(); // insert format-start items for (const key in attributes) { const val = attributes[key]; const currentVal = currPos.currentAttributes.get(key) ?? null; if (!equalAttrs$1(currentVal, val)) { // save negated attribute (set null if currentVal undefined) negatedAttributes.set(key, currentVal); const { left, right } = currPos; currPos.right = new Item$1(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)); currPos.right.integrate(transaction, 0); currPos.forward(); } } return negatedAttributes; }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {string|object|AbstractType} text * @param {Object} attributes * * @private * @function **/ const insertText = (transaction, parent, currPos, text, attributes) => { currPos.currentAttributes.forEach((_val, key) => { if (attributes[key] === undefined) { attributes[key] = null; } }); const doc = transaction.doc; const ownClientId = doc.clientID; minimizeAttributeChanges(currPos, attributes); const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); // insert content const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text); let { left, right, index } = currPos; if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()); } right = new Item$1(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content); right.integrate(transaction, 0); currPos.right = right; currPos.index = index; currPos.forward(); insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); }; /** * @param {Transaction} transaction * @param {AbstractType} parent * @param {ItemTextListPosition} currPos * @param {number} length * @param {Object} attributes * * @private * @function */ const formatText = (transaction, parent, currPos, length, attributes) => { const doc = transaction.doc; const ownClientId = doc.clientID; minimizeAttributeChanges(currPos, attributes); const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes); // iterate until first non-format or null is found // delete all formats with attributes[format.key] != null // also check the attributes after the first non-format as we do not want to insert redundant negated attributes there // eslint-disable-next-line no-labels iterationLoop: while (currPos.right !== null && (length > 0 || (negatedAttributes.size > 0 && (currPos.right.deleted || currPos.right.content.constructor === ContentFormat)))) { if (!currPos.right.deleted) { switch (currPos.right.content.constructor) { case ContentFormat: { const { key, value } = /** @type {ContentFormat} */ (currPos.right.content); const attr = attributes[key]; if (attr !== undefined) { if (equalAttrs$1(attr, value)) { negatedAttributes.delete(key); } else { if (length === 0) { // no need to further extend negatedAttributes // eslint-disable-next-line no-labels break iterationLoop; } negatedAttributes.set(key, value); } currPos.right.delete(transaction); } else { currPos.currentAttributes.set(key, value); } break; } default: if (length < currPos.right.length) { getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length)); } length -= currPos.right.length; break; } } currPos.forward(); } // Quill just assumes that the editor starts with a newline and that it always // ends with a newline. We only insert that newline when a new newline is // inserted - i.e when length is bigger than type.length if (length > 0) { let newlines = ""; for (; length > 0; length--) { newlines += "\n"; } currPos.right = new Item$1( createID(ownClientId, getState(doc.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines), ); currPos.right.integrate(transaction, 0); currPos.forward(); } insertNegatedAttributes(transaction, parent, currPos, negatedAttributes); }; /** * Call this function after string content has been deleted in order to * clean up formatting Items. * * @param {Transaction} transaction * @param {Item} start * @param {Item|null} curr exclusive end, automatically iterates to the next Content Item * @param {Map} startAttributes * @param {Map} currAttributes * @return {number} The amount of formatting Items deleted. * * @function */ const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => { /** * @type {Item|null} */ let end = start; /** * @type {Map} */ const endFormats = create$9(); while (end && (!end.countable || end.deleted)) { if (!end.deleted && end.content.constructor === ContentFormat) { const cf = /** @type {ContentFormat} */ (end.content); endFormats.set(cf.key, cf); } end = end.right; } let cleanups = 0; let reachedCurr = false; while (start !== end) { if (curr === start) { reachedCurr = true; } if (!start.deleted) { const content = start.content; switch (content.constructor) { case ContentFormat: { const { key, value } = /** @type {ContentFormat} */ (content); const startAttrValue = startAttributes.get(key) ?? null; if (endFormats.get(key) !== content || startAttrValue === value) { // Either this format is overwritten or it is not necessary because the attribute already existed. start.delete(transaction); cleanups++; if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) { if (startAttrValue === null) { currAttributes.delete(key); } else { currAttributes.set(key, startAttrValue); } } } if (!reachedCurr && !start.deleted) { updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content)); } break; } } } start = /** @type {Item} */ (start.right); } return cleanups; }; /** * @param {Transaction} transaction * @param {Item | null} item */ const cleanupContextlessFormattingGap = (transaction, item) => { // iterate until item.right is null or content while (item && item.right && (item.right.deleted || !item.right.countable)) { item = item.right; } const attrs = new Set(); // iterate back until a content item is found while (item && (item.deleted || !item.countable)) { if (!item.deleted && item.content.constructor === ContentFormat) { const key = /** @type {ContentFormat} */ (item.content).key; if (attrs.has(key)) { item.delete(transaction); } else { attrs.add(key); } } item = item.left; } }; /** * This function is experimental and subject to change / be removed. * * Ideally, we don't need this function at all. Formatting attributes should be cleaned up * automatically after each change. This function iterates twice over the complete YText type * and removes unnecessary formatting attributes. This is also helpful for testing. * * This function won't be exported anymore as soon as there is confidence that the YText type works as intended. * * @param {YText} type * @return {number} How many formatting attributes have been cleaned up. */ const cleanupYTextFormatting = (type) => { let res = 0; transact(/** @type {Doc} */ (type.doc), (transaction) => { let start = /** @type {Item} */ (type._start); let end = type._start; let startAttributes = create$9(); const currentAttributes = copy(startAttributes); while (end) { if (end.deleted === false) { switch (end.content.constructor) { case ContentFormat: updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content)); break; default: res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes); startAttributes = copy(currentAttributes); start = end; break; } } end = end.right; } }); return res; }; /** * This will be called by the transaction once the event handlers are called to potentially cleanup * formatting attributes. * * @param {Transaction} transaction */ const cleanupYTextAfterTransaction = (transaction) => { /** * @type {Set} */ const needFullCleanup = new Set(); // check if another formatting item was inserted const doc = transaction.doc; for (const [client, afterClock] of transaction.afterState.entries()) { const clock = transaction.beforeState.get(client) || 0; if (afterClock === clock) { continue; } iterateStructs(transaction, /** @type {Array} */ (doc.store.clients.get(client)), clock, afterClock, (item) => { if (!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC) { needFullCleanup.add(/** @type {any} */ (item).parent); } }); } // cleanup in a new transaction transact(doc, (t) => { iterateDeletedStructs(transaction, transaction.deleteSet, (item) => { if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) { return; } const parent = /** @type {YText} */ (item.parent); if (item.content.constructor === ContentFormat) { needFullCleanup.add(parent); } else { // If no formatting attribute was inserted or deleted, we can make due with contextless // formatting cleanups. // Contextless: it is not necessary to compute currentAttributes for the affected position. cleanupContextlessFormattingGap(t, item); } }); // If a formatting item was inserted, we simply clean the whole type. // We need to compute currentAttributes for the current position anyway. for (const yText of needFullCleanup) { cleanupYTextFormatting(yText); } }); }; /** * @param {Transaction} transaction * @param {ItemTextListPosition} currPos * @param {number} length * @return {ItemTextListPosition} * * @private * @function */ const deleteText = (transaction, currPos, length) => { const startLength = length; const startAttrs = copy(currPos.currentAttributes); const start = currPos.right; while (length > 0 && currPos.right !== null) { if (currPos.right.deleted === false) { switch (currPos.right.content.constructor) { case ContentType: case ContentEmbed: case ContentString: if (length < currPos.right.length) { getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length)); } length -= currPos.right.length; currPos.right.delete(transaction); break; } } currPos.forward(); } if (start) { cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes); } const parent = /** @type {AbstractType} */ (/** @type {Item} */ (currPos.left || currPos.right).parent); if (parent._searchMarker) { updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length); } return currPos; }; /** * The Quill Delta format represents changes on a text document with * formatting information. For more information visit {@link https://quilljs.com/docs/delta/|Quill Delta} * * @example * { * ops: [ * { insert: 'Gandalf', attributes: { bold: true } }, * { insert: ' the ' }, * { insert: 'Grey', attributes: { color: '#cccccc' } } * ] * } * */ /** * Attributes that can be assigned to a selection of text. * * @example * { * bold: true, * font-size: '40px' * } * * @typedef {Object} TextAttributes */ /** * @extends YEvent * Event that describes the changes on a YText type. */ class YTextEvent extends YEvent { /** * @param {YText} ytext * @param {Transaction} transaction * @param {Set} subs The keys that changed */ constructor(ytext, transaction, subs) { super(ytext, transaction); /** * Whether the children changed. * @type {Boolean} * @private */ this.childListChanged = false; /** * Set of all changed attributes. * @type {Set} */ this.keysChanged = new Set(); subs.forEach((sub) => { if (sub === null) { this.childListChanged = true; } else { this.keysChanged.add(sub); } }); } /** * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string, delete?:number, retain?:number}>}} */ get changes() { if (this._changes === null) { /** * @type {{added:Set,deleted:Set,keys:Map,delta:Array<{insert?:Array|string|AbstractType|object, delete?:number, retain?:number}>}} */ const changes = { keys: this.keys, delta: this.delta, added: new Set(), deleted: new Set(), }; this._changes = changes; } return /** @type {any} */ (this._changes); } /** * Compute the changes in the delta format. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} * * @public */ get delta() { if (this._delta === null) { const y = /** @type {Doc} */ (this.target.doc); /** * @type {Array<{insert?:string|object|AbstractType, delete?:number, retain?:number, attributes?: Object}>} */ const delta = []; transact(y, (transaction) => { const currentAttributes = new Map(); // saves all current attributes for insert const oldAttributes = new Map(); let item = this.target._start; /** * @type {string?} */ let action = null; /** * @type {Object} */ const attributes = {}; // counts added or removed new attributes for retain /** * @type {string|object} */ let insert = ""; let retain = 0; let deleteLen = 0; const addOp = () => { if (action !== null) { /** * @type {any} */ let op = null; switch (action) { case "delete": if (deleteLen > 0) { op = { delete: deleteLen }; } deleteLen = 0; break; case "insert": if (typeof insert === "object" || insert.length > 0) { op = { insert }; if (currentAttributes.size > 0) { op.attributes = {}; currentAttributes.forEach((value, key) => { if (value !== null) { op.attributes[key] = value; } }); } } insert = ""; break; case "retain": if (retain > 0) { op = { retain }; if (!isEmpty(attributes)) { op.attributes = assign({}, attributes); } } retain = 0; break; } if (op) delta.push(op); action = null; } }; while (item !== null) { switch (item.content.constructor) { case ContentType: case ContentEmbed: if (this.adds(item)) { if (!this.deletes(item)) { addOp(); action = "insert"; insert = item.content.getContent()[0]; addOp(); } } else if (this.deletes(item)) { if (action !== "delete") { addOp(); action = "delete"; } deleteLen += 1; } else if (!item.deleted) { if (action !== "retain") { addOp(); action = "retain"; } retain += 1; } break; case ContentString: if (this.adds(item)) { if (!this.deletes(item)) { if (action !== "insert") { addOp(); action = "insert"; } insert += /** @type {ContentString} */ (item.content).str; } } else if (this.deletes(item)) { if (action !== "delete") { addOp(); action = "delete"; } deleteLen += item.length; } else if (!item.deleted) { if (action !== "retain") { addOp(); action = "retain"; } retain += item.length; } break; case ContentFormat: { const { key, value } = /** @type {ContentFormat} */ (item.content); if (this.adds(item)) { if (!this.deletes(item)) { const curVal = currentAttributes.get(key) ?? null; if (!equalAttrs$1(curVal, value)) { if (action === "retain") { addOp(); } if (equalAttrs$1(value, oldAttributes.get(key) ?? null)) { delete attributes[key]; } else { attributes[key] = value; } } else if (value !== null) { item.delete(transaction); } } } else if (this.deletes(item)) { oldAttributes.set(key, value); const curVal = currentAttributes.get(key) ?? null; if (!equalAttrs$1(curVal, value)) { if (action === "retain") { addOp(); } attributes[key] = curVal; } } else if (!item.deleted) { oldAttributes.set(key, value); const attr = attributes[key]; if (attr !== undefined) { if (!equalAttrs$1(attr, value)) { if (action === "retain") { addOp(); } if (value === null) { delete attributes[key]; } else { attributes[key] = value; } } else if (attr !== null) { // this will be cleaned up automatically by the contextless cleanup function item.delete(transaction); } } } if (!item.deleted) { if (action === "insert") { addOp(); } updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content)); } break; } } item = item.right; } addOp(); while (delta.length > 0) { const lastOp = delta[delta.length - 1]; if (lastOp.retain !== undefined && lastOp.attributes === undefined) { // retain delta's if they don't assign attributes delta.pop(); } else { break; } } }); this._delta = delta; } return /** @type {any} */ (this._delta); } } /** * Type that represents text with formatting information. * * This type replaces y-richtext as this implementation is able to handle * block formats (format information on a paragraph), embeds (complex elements * like pictures and videos), and text formats (**bold**, *italic*). * * @extends AbstractType */ class YText extends AbstractType { /** * @param {String} [string] The initial value of the YText. */ constructor(string) { super(); /** * Array of pending operations on this type * @type {Array?} */ this._pending = string !== undefined ? [() => this.insert(0, string)] : []; /** * @type {Array|null} */ this._searchMarker = []; /** * Whether this YText contains formatting attributes. * This flag is updated when a formatting item is integrated (see ContentFormat.integrate) */ this._hasFormatting = false; } /** * Number of characters of this text type. * * @type {number} */ get length() { this.doc ?? warnPrematureAccess(); return this._length; } /** * @param {Doc} y * @param {Item} item */ _integrate(y, item) { super._integrate(y, item); try { /** @type {Array} */ (this._pending).forEach((f) => f()); } catch (e) { console.error(e); } this._pending = null; } _copy() { return new YText(); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {YText} */ clone() { const text = new YText(); text.applyDelta(this.toDelta()); return text; } /** * Creates YTextEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver(transaction, parentSubs) { super._callObserver(transaction, parentSubs); const event = new YTextEvent(this, transaction, parentSubs); callTypeObservers(this, transaction, event); // If a remote change happened, we try to cleanup potential formatting duplicates. if (!transaction.local && this._hasFormatting) { transaction._needFormattingCleanup = true; } } /** * Returns the unformatted string representation of this YText type. * * @public */ toString() { this.doc ?? warnPrematureAccess(); let str = ""; /** * @type {Item|null} */ let n = this._start; while (n !== null) { if (!n.deleted && n.countable && n.content.constructor === ContentString) { str += /** @type {ContentString} */ (n.content).str; } n = n.right; } return str; } /** * Returns the unformatted string representation of this YText type. * * @return {string} * @public */ toJSON() { return this.toString(); } /** * Apply a {@link Delta} on this shared YText type. * * @param {Array} delta The changes to apply on this element. * @param {object} opts * @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true. * * * @public */ applyDelta(delta, { sanitize = true } = {}) { if (this.doc !== null) { transact(this.doc, (transaction) => { const currPos = new ItemTextListPosition(null, this._start, 0, new Map()); for (let i = 0; i < delta.length; i++) { const op = delta[i]; if (op.insert !== undefined) { // Quill assumes that the content starts with an empty paragraph. // Yjs/Y.Text assumes that it starts empty. We always hide that // there is a newline at the end of the content. // If we omit this step, clients will see a different number of // paragraphs, but nothing bad will happen. const ins = !sanitize && typeof op.insert === "string" && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === "\n" ? op.insert.slice(0, -1) : op.insert; if (typeof ins !== "string" || ins.length > 0) { insertText(transaction, this, currPos, ins, op.attributes || {}); } } else if (op.retain !== undefined) { formatText(transaction, this, currPos, op.retain, op.attributes || {}); } else if (op.delete !== undefined) { deleteText(transaction, currPos, op.delete); } } }); } else { /** @type {Array} */ (this._pending).push(() => this.applyDelta(delta)); } } /** * Returns the Delta representation of this YText type. * * @param {Snapshot} [snapshot] * @param {Snapshot} [prevSnapshot] * @param {function('removed' | 'added', ID):any} [computeYChange] * @return {any} The Delta representation of this type. * * @public */ toDelta(snapshot, prevSnapshot, computeYChange) { this.doc ?? warnPrematureAccess(); /** * @type{Array} */ const ops = []; const currentAttributes = new Map(); const doc = /** @type {Doc} */ (this.doc); let str = ""; let n = this._start; function packStr() { if (str.length > 0) { // pack str with attributes to ops /** * @type {Object} */ const attributes = {}; let addAttributes = false; currentAttributes.forEach((value, key) => { addAttributes = true; attributes[key] = value; }); /** * @type {Object} */ const op = { insert: str }; if (addAttributes) { op.attributes = attributes; } ops.push(op); str = ""; } } const computeDelta = () => { while (n !== null) { if (isVisible$1(n, snapshot) || (prevSnapshot !== undefined && isVisible$1(n, prevSnapshot))) { switch (n.content.constructor) { case ContentString: { const cur = currentAttributes.get("ychange"); if (snapshot !== undefined && !isVisible$1(n, snapshot)) { if (cur === undefined || cur.user !== n.id.client || cur.type !== "removed") { packStr(); currentAttributes.set("ychange", computeYChange ? computeYChange("removed", n.id) : { type: "removed" }); } } else if (prevSnapshot !== undefined && !isVisible$1(n, prevSnapshot)) { if (cur === undefined || cur.user !== n.id.client || cur.type !== "added") { packStr(); currentAttributes.set("ychange", computeYChange ? computeYChange("added", n.id) : { type: "added" }); } } else if (cur !== undefined) { packStr(); currentAttributes.delete("ychange"); } str += /** @type {ContentString} */ (n.content).str; break; } case ContentType: case ContentEmbed: { packStr(); /** * @type {Object} */ const op = { insert: n.content.getContent()[0], }; if (currentAttributes.size > 0) { const attrs = /** @type {Object} */ ({}); op.attributes = attrs; currentAttributes.forEach((value, key) => { attrs[key] = value; }); } ops.push(op); break; } case ContentFormat: if (isVisible$1(n, snapshot)) { packStr(); updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content)); } break; } } n = n.right; } packStr(); }; if (snapshot || prevSnapshot) { // snapshots are merged again after the transaction, so we need to keep the // transaction alive until we are done transact( doc, (transaction) => { if (snapshot) { splitSnapshotAffectedStructs(transaction, snapshot); } if (prevSnapshot) { splitSnapshotAffectedStructs(transaction, prevSnapshot); } computeDelta(); }, "cleanup", ); } else { computeDelta(); } return ops; } /** * Insert text at a given index. * * @param {number} index The index at which to start inserting. * @param {String} text The text to insert at the specified position. * @param {TextAttributes} [attributes] Optionally define some formatting * information to apply on the inserted * Text. * @public */ insert(index, text, attributes) { if (text.length <= 0) { return; } const y = this.doc; if (y !== null) { transact(y, (transaction) => { const pos = findPosition(transaction, this, index, !attributes); if (!attributes) { attributes = {}; // @ts-ignore pos.currentAttributes.forEach((v, k) => { attributes[k] = v; }); } insertText(transaction, this, pos, text, attributes); }); } else { /** @type {Array} */ (this._pending).push(() => this.insert(index, text, attributes)); } } /** * Inserts an embed at a index. * * @param {number} index The index to insert the embed at. * @param {Object | AbstractType} embed The Object that represents the embed. * @param {TextAttributes} [attributes] Attribute information to apply on the * embed * * @public */ insertEmbed(index, embed, attributes) { const y = this.doc; if (y !== null) { transact(y, (transaction) => { const pos = findPosition(transaction, this, index, !attributes); insertText(transaction, this, pos, embed, attributes || {}); }); } else { /** @type {Array} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes || {})); } } /** * Deletes text starting from an index. * * @param {number} index Index at which to start deleting. * @param {number} length The number of characters to remove. Defaults to 1. * * @public */ delete(index, length) { if (length === 0) { return; } const y = this.doc; if (y !== null) { transact(y, (transaction) => { deleteText(transaction, findPosition(transaction, this, index, true), length); }); } else { /** @type {Array} */ (this._pending).push(() => this.delete(index, length)); } } /** * Assigns properties to a range of text. * * @param {number} index The position where to start formatting. * @param {number} length The amount of characters to assign properties to. * @param {TextAttributes} attributes Attribute information to apply on the * text. * * @public */ format(index, length, attributes) { if (length === 0) { return; } const y = this.doc; if (y !== null) { transact(y, (transaction) => { const pos = findPosition(transaction, this, index, false); if (pos.right === null) { return; } formatText(transaction, this, pos, length, attributes); }); } else { /** @type {Array} */ (this._pending).push(() => this.format(index, length, attributes)); } } /** * Removes an attribute. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @param {String} attributeName The attribute name that is to be removed. * * @public */ removeAttribute(attributeName) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeMapDelete(transaction, this, attributeName); }); } else { /** @type {Array} */ (this._pending).push(() => this.removeAttribute(attributeName)); } } /** * Sets or updates an attribute. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @param {String} attributeName The attribute name that is to be set. * @param {any} attributeValue The attribute value that is to be set. * * @public */ setAttribute(attributeName, attributeValue) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeMapSet(transaction, this, attributeName, attributeValue); }); } else { /** @type {Array} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue)); } } /** * Returns an attribute value that belongs to the attribute name. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @param {String} attributeName The attribute name that identifies the * queried value. * @return {any} The queried attribute value. * * @public */ getAttribute(attributeName) { return /** @type {any} */ (typeMapGet(this, attributeName)); } /** * Returns all attribute name/value pairs in a JSON Object. * * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. * * @return {Object} A JSON Object that describes the attributes. * * @public */ getAttributes() { return typeMapGetAll(this); } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write(encoder) { encoder.writeTypeRef(YTextRefID); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * @return {YText} * * @private * @function */ const readYText = (_decoder) => new YText(); /** * @module YXml */ /** * Define the elements to which a set of CSS queries apply. * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors} * * @example * query = '.classSelector' * query = 'nodeSelector' * query = '#idSelector' * * @typedef {string} CSS_Selector */ /** * Dom filter function. * * @callback domFilter * @param {string} nodeName The nodeName of the element * @param {Map} attributes The map of attributes. * @return {boolean} Whether to include the Dom node in the YXmlElement. */ /** * Represents a subset of the nodes of a YXmlElement / YXmlFragment and a * position within them. * * Can be created with {@link YXmlFragment#createTreeWalker} * * @public * @implements {Iterable} */ class YXmlTreeWalker { /** * @param {YXmlFragment | YXmlElement} root * @param {function(AbstractType):boolean} [f] */ constructor(root, f = () => true) { this._filter = f; this._root = root; /** * @type {Item} */ this._currentNode = /** @type {Item} */ (root._start); this._firstCall = true; root.doc ?? warnPrematureAccess(); } [Symbol.iterator]() { return this; } /** * Get the next node. * * @return {IteratorResult} The next node. * * @public */ next() { /** * @type {Item|null} */ let n = this._currentNode; let type = n && n.content && /** @type {any} */ (n.content).type; if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item do { type = /** @type {any} */ (n.content).type; if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) { // walk down in the tree n = type._start; } else { // walk right or up in the tree while (n !== null) { /** * @type {Item | null} */ const nxt = n.next; if (nxt !== null) { n = nxt; break; } else if (n.parent === this._root) { n = null; } else { n = /** @type {AbstractType} */ (n.parent)._item; } } } } while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type))); } this._firstCall = false; if (n === null) { // @ts-ignore return { value: undefined, done: true }; } this._currentNode = n; return { value: /** @type {any} */ (n.content).type, done: false }; } } /** * Represents a list of {@link YXmlElement}.and {@link YXmlText} types. * A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a * nodeName and it does not have attributes. Though it can be bound to a DOM * element - in this case the attributes and the nodeName are not shared. * * @public * @extends AbstractType */ class YXmlFragment extends AbstractType { constructor() { super(); /** * @type {Array|null} */ this._prelimContent = []; } /** * @type {YXmlElement|YXmlText|null} */ get firstChild() { const first = this._first; return first ? first.content.getContent()[0] : null; } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate(y, item) { super._integrate(y, item); this.insert(0, /** @type {Array} */ (this._prelimContent)); this._prelimContent = null; } _copy() { return new YXmlFragment(); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {YXmlFragment} */ clone() { const el = new YXmlFragment(); // @ts-ignore el.insert( 0, this.toArray().map((item) => (item instanceof AbstractType ? item.clone() : item)), ); return el; } get length() { this.doc ?? warnPrematureAccess(); return this._prelimContent === null ? this._length : this._prelimContent.length; } /** * Create a subtree of childNodes. * * @example * const walker = elem.createTreeWalker(dom => dom.nodeName === 'div') * for (let node in walker) { * // `node` is a div node * nop(node) * } * * @param {function(AbstractType):boolean} filter Function that is called on each child element and * returns a Boolean indicating whether the child * is to be included in the subtree. * @return {YXmlTreeWalker} A subtree and a position within it. * * @public */ createTreeWalker(filter) { return new YXmlTreeWalker(this, filter); } /** * Returns the first YXmlElement that matches the query. * Similar to DOM's {@link querySelector}. * * Query support: * - tagname * TODO: * - id * - attribute * * @param {CSS_Selector} query The query on the children. * @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null. * * @public */ querySelector(query) { query = query.toUpperCase(); // @ts-ignore const iterator = new YXmlTreeWalker(this, (element) => element.nodeName && element.nodeName.toUpperCase() === query); const next = iterator.next(); if (next.done) { return null; } else { return next.value; } } /** * Returns all YXmlElements that match the query. * Similar to Dom's {@link querySelectorAll}. * * @todo Does not yet support all queries. Currently only query by tagName. * * @param {CSS_Selector} query The query on the children * @return {Array} The elements that match this query. * * @public */ querySelectorAll(query) { query = query.toUpperCase(); // @ts-ignore return from(new YXmlTreeWalker(this, (element) => element.nodeName && element.nodeName.toUpperCase() === query)); } /** * Creates YXmlEvent and calls observers. * * @param {Transaction} transaction * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. */ _callObserver(transaction, parentSubs) { callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction)); } /** * Get the string representation of all the children of this YXmlFragment. * * @return {string} The string representation of all children. */ toString() { return typeListMap(this, (xml) => xml.toString()).join(""); } /** * @return {string} */ toJSON() { return this.toString(); } /** * Creates a Dom Element that mirrors this YXmlElement. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object} [hooks={}] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM(_document = document, hooks = {}, binding) { const fragment = _document.createDocumentFragment(); if (binding !== undefined) { binding._createAssociation(fragment, this); } typeListForEach(this, (xmlType) => { fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null); }); return fragment; } /** * Inserts new content at an index. * * @example * // Insert character 'a' at position 0 * xml.insert(0, [new Y.XmlText('text')]) * * @param {number} index The index to insert content at * @param {Array} content The array of content */ insert(index, content) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeListInsertGenerics(transaction, this, index, content); }); } else { // @ts-ignore _prelimContent is defined because this is not yet integrated this._prelimContent.splice(index, 0, ...content); } } /** * Inserts new content at an index. * * @example * // Insert character 'a' at position 0 * xml.insert(0, [new Y.XmlText('text')]) * * @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at * @param {Array} content The array of content */ insertAfter(ref, content) { if (this.doc !== null) { transact(this.doc, (transaction) => { const refItem = ref && ref instanceof AbstractType ? ref._item : ref; typeListInsertGenericsAfter(transaction, this, refItem, content); }); } else { const pc = /** @type {Array} */ (this._prelimContent); const index = ref === null ? 0 : pc.findIndex((el) => el === ref) + 1; if (index === 0 && ref !== null) { throw create$7("Reference item not found"); } pc.splice(index, 0, ...content); } } /** * Deletes elements starting from an index. * * @param {number} index Index at which to start deleting elements * @param {number} [length=1] The number of elements to remove. Defaults to 1. */ delete(index, length = 1) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeListDelete(transaction, this, index, length); }); } else { // @ts-ignore _prelimContent is defined because this is not yet integrated this._prelimContent.splice(index, length); } } /** * Transforms this YArray to a JavaScript Array. * * @return {Array} */ toArray() { return typeListToArray(this); } /** * Appends content to this YArray. * * @param {Array} content Array of content to append. */ push(content) { this.insert(this.length, content); } /** * Prepends content to this YArray. * * @param {Array} content Array of content to prepend. */ unshift(content) { this.insert(0, content); } /** * Returns the i-th element from a YArray. * * @param {number} index The index of the element to return from the YArray * @return {YXmlElement|YXmlText} */ get(index) { return typeListGet(this, index); } /** * Returns a portion of this YXmlFragment into a JavaScript Array selected * from start to end (end not included). * * @param {number} [start] * @param {number} [end] * @return {Array} */ slice(start = 0, end = this.length) { return typeListSlice(this, start, end); } /** * Executes a provided function on once on every child element. * * @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray. */ forEach(f) { typeListForEach(this, f); } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. */ _write(encoder) { encoder.writeTypeRef(YXmlFragmentRefID); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder * @return {YXmlFragment} * * @private * @function */ const readYXmlFragment = (_decoder) => new YXmlFragment(); /** * @typedef {Object|number|null|Array|string|Uint8Array|AbstractType} ValueTypes */ /** * An YXmlElement imitates the behavior of a * https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element * * * An YXmlElement has attributes (key value pairs) * * An YXmlElement has childElements that must inherit from YXmlElement * * @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }] */ class YXmlElement extends YXmlFragment { constructor(nodeName = "UNDEFINED") { super(); this.nodeName = nodeName; /** * @type {Map|null} */ this._prelimAttrs = new Map(); } /** * @type {YXmlElement|YXmlText|null} */ get nextSibling() { const n = this._item ? this._item.next : null; return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null; } /** * @type {YXmlElement|YXmlText|null} */ get prevSibling() { const n = this._item ? this._item.prev : null; return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null; } /** * Integrate this type into the Yjs instance. * * * Save this struct in the os * * This type is sent to other client * * Observer functions are fired * * @param {Doc} y The Yjs instance * @param {Item} item */ _integrate(y, item) { super._integrate(y, item); /** @type {Map} */ (this._prelimAttrs).forEach((value, key) => { this.setAttribute(key, value); }); this._prelimAttrs = null; } /** * Creates an Item with the same effect as this Item (without position effect) * * @return {YXmlElement} */ _copy() { return new YXmlElement(this.nodeName); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {YXmlElement} */ clone() { /** * @type {YXmlElement} */ const el = new YXmlElement(this.nodeName); const attrs = this.getAttributes(); forEach(attrs, (value, key) => { el.setAttribute(key, /** @type {any} */ (value)); }); // @ts-ignore el.insert( 0, this.toArray().map((v) => (v instanceof AbstractType ? v.clone() : v)), ); return el; } /** * Returns the XML serialization of this YXmlElement. * The attributes are ordered by attribute-name, so you can easily use this * method to compare YXmlElements * * @return {string} The string representation of this type. * * @public */ toString() { const attrs = this.getAttributes(); const stringBuilder = []; const keys = []; for (const key in attrs) { keys.push(key); } keys.sort(); const keysLen = keys.length; for (let i = 0; i < keysLen; i++) { const key = keys[i]; stringBuilder.push(key + '="' + attrs[key] + '"'); } const nodeName = this.nodeName.toLocaleLowerCase(); const attrsString = stringBuilder.length > 0 ? " " + stringBuilder.join(" ") : ""; return `<${nodeName}${attrsString}>${super.toString()}`; } /** * Removes an attribute from this YXmlElement. * * @param {string} attributeName The attribute name that is to be removed. * * @public */ removeAttribute(attributeName) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeMapDelete(transaction, this, attributeName); }); } else { /** @type {Map} */ (this._prelimAttrs).delete(attributeName); } } /** * Sets or updates an attribute. * * @template {keyof KV & string} KEY * * @param {KEY} attributeName The attribute name that is to be set. * @param {KV[KEY]} attributeValue The attribute value that is to be set. * * @public */ setAttribute(attributeName, attributeValue) { if (this.doc !== null) { transact(this.doc, (transaction) => { typeMapSet(transaction, this, attributeName, attributeValue); }); } else { /** @type {Map} */ (this._prelimAttrs).set(attributeName, attributeValue); } } /** * Returns an attribute value that belongs to the attribute name. * * @template {keyof KV & string} KEY * * @param {KEY} attributeName The attribute name that identifies the * queried value. * @return {KV[KEY]|undefined} The queried attribute value. * * @public */ getAttribute(attributeName) { return /** @type {any} */ (typeMapGet(this, attributeName)); } /** * Returns whether an attribute exists * * @param {string} attributeName The attribute name to check for existence. * @return {boolean} whether the attribute exists. * * @public */ hasAttribute(attributeName) { return /** @type {any} */ (typeMapHas(this, attributeName)); } /** * Returns all attribute name/value pairs in a JSON Object. * * @param {Snapshot} [snapshot] * @return {{ [Key in Extract]?: KV[Key]}} A JSON Object that describes the attributes. * * @public */ getAttributes(snapshot) { return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this)); } /** * Creates a Dom Element that mirrors this YXmlElement. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object} [hooks={}] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. * @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM(_document = document, hooks = {}, binding) { const dom = _document.createElement(this.nodeName); const attrs = this.getAttributes(); for (const key in attrs) { const value = attrs[key]; if (typeof value === "string") { dom.setAttribute(key, value); } } typeListForEach(this, (yxml) => { dom.appendChild(yxml.toDOM(_document, hooks, binding)); }); if (binding !== undefined) { binding._createAssociation(dom, this); } return dom; } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. */ _write(encoder) { encoder.writeTypeRef(YXmlElementRefID); encoder.writeKey(this.nodeName); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {YXmlElement} * * @function */ const readYXmlElement = (decoder) => new YXmlElement(decoder.readKey()); /** * @extends YEvent * An Event that describes changes on a YXml Element or Yxml Fragment */ class YXmlEvent extends YEvent { /** * @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created. * @param {Set} subs The set of changed attributes. `null` is included if the * child list changed. * @param {Transaction} transaction The transaction instance with which the * change was created. */ constructor(target, subs, transaction) { super(target, transaction); /** * Whether the children changed. * @type {Boolean} * @private */ this.childListChanged = false; /** * Set of all changed attributes. * @type {Set} */ this.attributesChanged = new Set(); subs.forEach((sub) => { if (sub === null) { this.childListChanged = true; } else { this.attributesChanged.add(sub); } }); } } /** * You can manage binding to a custom type with YXmlHook. * * @extends {YMap} */ class YXmlHook extends YMap { /** * @param {string} hookName nodeName of the Dom Node. */ constructor(hookName) { super(); /** * @type {string} */ this.hookName = hookName; } /** * Creates an Item with the same effect as this Item (without position effect) */ _copy() { return new YXmlHook(this.hookName); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {YXmlHook} */ clone() { const el = new YXmlHook(this.hookName); this.forEach((value, key) => { el.set(key, value); }); return el; } /** * Creates a Dom Element that mirrors this YXmlElement. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object.} [hooks] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM(_document = document, hooks = {}, binding) { const hook = hooks[this.hookName]; let dom; if (hook !== undefined) { dom = hook.createDom(this); } else { dom = document.createElement(this.hookName); } dom.setAttribute("data-yjs-hook", this.hookName); if (binding !== undefined) { binding._createAssociation(dom, this); } return dom; } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. */ _write(encoder) { encoder.writeTypeRef(YXmlHookRefID); encoder.writeKey(this.hookName); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {YXmlHook} * * @private * @function */ const readYXmlHook = (decoder) => new YXmlHook(decoder.readKey()); /** * Represents text in a Dom Element. In the future this type will also handle * simple formatting information like bold and italic. */ class YXmlText extends YText { /** * @type {YXmlElement|YXmlText|null} */ get nextSibling() { const n = this._item ? this._item.next : null; return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null; } /** * @type {YXmlElement|YXmlText|null} */ get prevSibling() { const n = this._item ? this._item.prev : null; return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null; } _copy() { return new YXmlText(); } /** * Makes a copy of this data type that can be included somewhere else. * * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. * * @return {YXmlText} */ clone() { const text = new YXmlText(); text.applyDelta(this.toDelta()); return text; } /** * Creates a Dom Element that mirrors this YXmlText. * * @param {Document} [_document=document] The document object (you must define * this when calling this method in * nodejs) * @param {Object} [hooks] Optional property to customize how hooks * are presented in the DOM * @param {any} [binding] You should not set this property. This is * used if DomBinding wants to create a * association to the created DOM type. * @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element} * * @public */ toDOM(_document = document, hooks, binding) { const dom = _document.createTextNode(this.toString()); if (binding !== undefined) { binding._createAssociation(dom, this); } return dom; } toString() { // @ts-ignore return this.toDelta() .map((delta) => { const nestedNodes = []; for (const nodeName in delta.attributes) { const attrs = []; for (const key in delta.attributes[nodeName]) { attrs.push({ key, value: delta.attributes[nodeName][key] }); } // sort attributes to get a unique order attrs.sort((a, b) => (a.key < b.key ? -1 : 1)); nestedNodes.push({ nodeName, attrs }); } // sort node order to get a unique order nestedNodes.sort((a, b) => (a.nodeName < b.nodeName ? -1 : 1)); // now convert to dom string let str = ""; for (let i = 0; i < nestedNodes.length; i++) { const node = nestedNodes[i]; str += `<${node.nodeName}`; for (let j = 0; j < node.attrs.length; j++) { const attr = node.attrs[j]; str += ` ${attr.key}="${attr.value}"`; } str += ">"; } str += delta.insert; for (let i = nestedNodes.length - 1; i >= 0; i--) { str += ``; } return str; }) .join(""); } /** * @return {string} */ toJSON() { return this.toString(); } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder */ _write(encoder) { encoder.writeTypeRef(YXmlTextRefID); } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {YXmlText} * * @private * @function */ const readYXmlText = (decoder) => new YXmlText(); class AbstractStruct { /** * @param {ID} id * @param {number} length */ constructor(id, length) { this.id = id; this.length = length; } /** * @type {boolean} */ get deleted() { throw methodUnimplemented(); } /** * Merge this struct with the item to the right. * This method is already assuming that `this.id.clock + this.length === this.id.clock`. * Also this method does *not* remove right from StructStore! * @param {AbstractStruct} right * @return {boolean} whether this merged with right */ mergeWith(right) { return false; } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. * @param {number} offset * @param {number} encodingRef */ write(encoder, offset, encodingRef) { throw methodUnimplemented(); } /** * @param {Transaction} transaction * @param {number} offset */ integrate(transaction, offset) { throw methodUnimplemented(); } } const structGCRefNumber = 0; /** * @private */ class GC extends AbstractStruct { get deleted() { return true; } delete() {} /** * @param {GC} right * @return {boolean} */ mergeWith(right) { if (this.constructor !== right.constructor) { return false; } this.length += right.length; return true; } /** * @param {Transaction} transaction * @param {number} offset */ integrate(transaction, offset) { if (offset > 0) { this.id.clock += offset; this.length -= offset; } addStruct(transaction.doc.store, this); } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeInfo(structGCRefNumber); encoder.writeLen(this.length - offset); } /** * @param {Transaction} transaction * @param {StructStore} store * @return {null | number} */ getMissing(transaction, store) { return null; } } class ContentBinary { /** * @param {Uint8Array} content */ constructor(content) { this.content = content; } /** * @return {number} */ getLength() { return 1; } /** * @return {Array} */ getContent() { return [this.content]; } /** * @return {boolean} */ isCountable() { return true; } /** * @return {ContentBinary} */ copy() { return new ContentBinary(this.content); } /** * @param {number} offset * @return {ContentBinary} */ splice(offset) { throw methodUnimplemented(); } /** * @param {ContentBinary} right * @return {boolean} */ mergeWith(right) { return false; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) {} /** * @param {Transaction} transaction */ delete(transaction) {} /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeBuf(this.content); } /** * @return {number} */ getRef() { return 3; } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder * @return {ContentBinary} */ const readContentBinary = (decoder) => new ContentBinary(decoder.readBuf()); class ContentDeleted { /** * @param {number} len */ constructor(len) { this.len = len; } /** * @return {number} */ getLength() { return this.len; } /** * @return {Array} */ getContent() { return []; } /** * @return {boolean} */ isCountable() { return false; } /** * @return {ContentDeleted} */ copy() { return new ContentDeleted(this.len); } /** * @param {number} offset * @return {ContentDeleted} */ splice(offset) { const right = new ContentDeleted(this.len - offset); this.len = offset; return right; } /** * @param {ContentDeleted} right * @return {boolean} */ mergeWith(right) { this.len += right.len; return true; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) { addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len); item.markDeleted(); } /** * @param {Transaction} transaction */ delete(transaction) {} /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeLen(this.len - offset); } /** * @return {number} */ getRef() { return 1; } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder * @return {ContentDeleted} */ const readContentDeleted = (decoder) => new ContentDeleted(decoder.readLen()); /** * @param {string} guid * @param {Object} opts */ const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false }); /** * @private */ class ContentDoc { /** * @param {Doc} doc */ constructor(doc) { if (doc._item) { console.error("This document was already integrated as a sub-document. You should create a second instance instead with the same guid."); } /** * @type {Doc} */ this.doc = doc; /** * @type {any} */ const opts = {}; this.opts = opts; if (!doc.gc) { opts.gc = false; } if (doc.autoLoad) { opts.autoLoad = true; } if (doc.meta !== null) { opts.meta = doc.meta; } } /** * @return {number} */ getLength() { return 1; } /** * @return {Array} */ getContent() { return [this.doc]; } /** * @return {boolean} */ isCountable() { return true; } /** * @return {ContentDoc} */ copy() { return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts)); } /** * @param {number} offset * @return {ContentDoc} */ splice(offset) { throw methodUnimplemented(); } /** * @param {ContentDoc} right * @return {boolean} */ mergeWith(right) { return false; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) { // this needs to be reflected in doc.destroy as well this.doc._item = item; transaction.subdocsAdded.add(this.doc); if (this.doc.shouldLoad) { transaction.subdocsLoaded.add(this.doc); } } /** * @param {Transaction} transaction */ delete(transaction) { if (transaction.subdocsAdded.has(this.doc)) { transaction.subdocsAdded.delete(this.doc); } else { transaction.subdocsRemoved.add(this.doc); } } /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeString(this.doc.guid); encoder.writeAny(this.opts); } /** * @return {number} */ getRef() { return 9; } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentDoc} */ const readContentDoc = (decoder) => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny())); /** * @private */ class ContentEmbed { /** * @param {Object} embed */ constructor(embed) { this.embed = embed; } /** * @return {number} */ getLength() { return 1; } /** * @return {Array} */ getContent() { return [this.embed]; } /** * @return {boolean} */ isCountable() { return true; } /** * @return {ContentEmbed} */ copy() { return new ContentEmbed(this.embed); } /** * @param {number} offset * @return {ContentEmbed} */ splice(offset) { throw methodUnimplemented(); } /** * @param {ContentEmbed} right * @return {boolean} */ mergeWith(right) { return false; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) {} /** * @param {Transaction} transaction */ delete(transaction) {} /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeJSON(this.embed); } /** * @return {number} */ getRef() { return 5; } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentEmbed} */ const readContentEmbed = (decoder) => new ContentEmbed(decoder.readJSON()); /** * @private */ class ContentFormat { /** * @param {string} key * @param {Object} value */ constructor(key, value) { this.key = key; this.value = value; } /** * @return {number} */ getLength() { return 1; } /** * @return {Array} */ getContent() { return []; } /** * @return {boolean} */ isCountable() { return false; } /** * @return {ContentFormat} */ copy() { return new ContentFormat(this.key, this.value); } /** * @param {number} _offset * @return {ContentFormat} */ splice(_offset) { throw methodUnimplemented(); } /** * @param {ContentFormat} _right * @return {boolean} */ mergeWith(_right) { return false; } /** * @param {Transaction} _transaction * @param {Item} item */ integrate(_transaction, item) { // @todo searchmarker are currently unsupported for rich text documents const p = /** @type {YText} */ (item.parent); p._searchMarker = null; p._hasFormatting = true; } /** * @param {Transaction} transaction */ delete(transaction) {} /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeKey(this.key); encoder.writeJSON(this.value); } /** * @return {number} */ getRef() { return 6; } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentFormat} */ const readContentFormat = (decoder) => new ContentFormat(decoder.readKey(), decoder.readJSON()); /** * @private */ class ContentJSON { /** * @param {Array} arr */ constructor(arr) { /** * @type {Array} */ this.arr = arr; } /** * @return {number} */ getLength() { return this.arr.length; } /** * @return {Array} */ getContent() { return this.arr; } /** * @return {boolean} */ isCountable() { return true; } /** * @return {ContentJSON} */ copy() { return new ContentJSON(this.arr); } /** * @param {number} offset * @return {ContentJSON} */ splice(offset) { const right = new ContentJSON(this.arr.slice(offset)); this.arr = this.arr.slice(0, offset); return right; } /** * @param {ContentJSON} right * @return {boolean} */ mergeWith(right) { this.arr = this.arr.concat(right.arr); return true; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) {} /** * @param {Transaction} transaction */ delete(transaction) {} /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { const len = this.arr.length; encoder.writeLen(len - offset); for (let i = offset; i < len; i++) { const c = this.arr[i]; encoder.writeString(c === undefined ? "undefined" : JSON.stringify(c)); } } /** * @return {number} */ getRef() { return 2; } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentJSON} */ const readContentJSON = (decoder) => { const len = decoder.readLen(); const cs = []; for (let i = 0; i < len; i++) { const c = decoder.readString(); if (c === "undefined") { cs.push(undefined); } else { cs.push(JSON.parse(c)); } } return new ContentJSON(cs); }; const isDevMode = getVariable("node_env") === "development"; class ContentAny { /** * @param {Array} arr */ constructor(arr) { /** * @type {Array} */ this.arr = arr; isDevMode && deepFreeze(arr); } /** * @return {number} */ getLength() { return this.arr.length; } /** * @return {Array} */ getContent() { return this.arr; } /** * @return {boolean} */ isCountable() { return true; } /** * @return {ContentAny} */ copy() { return new ContentAny(this.arr); } /** * @param {number} offset * @return {ContentAny} */ splice(offset) { const right = new ContentAny(this.arr.slice(offset)); this.arr = this.arr.slice(0, offset); return right; } /** * @param {ContentAny} right * @return {boolean} */ mergeWith(right) { this.arr = this.arr.concat(right.arr); return true; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) {} /** * @param {Transaction} transaction */ delete(transaction) {} /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { const len = this.arr.length; encoder.writeLen(len - offset); for (let i = offset; i < len; i++) { const c = this.arr[i]; encoder.writeAny(c); } } /** * @return {number} */ getRef() { return 8; } } /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentAny} */ const readContentAny = (decoder) => { const len = decoder.readLen(); const cs = []; for (let i = 0; i < len; i++) { cs.push(decoder.readAny()); } return new ContentAny(cs); }; /** * @private */ class ContentString { /** * @param {string} str */ constructor(str) { /** * @type {string} */ this.str = str; } /** * @return {number} */ getLength() { return this.str.length; } /** * @return {Array} */ getContent() { return this.str.split(""); } /** * @return {boolean} */ isCountable() { return true; } /** * @return {ContentString} */ copy() { return new ContentString(this.str); } /** * @param {number} offset * @return {ContentString} */ splice(offset) { const right = new ContentString(this.str.slice(offset)); this.str = this.str.slice(0, offset); // Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248 const firstCharCode = this.str.charCodeAt(offset - 1); if (firstCharCode >= 0xd800 && firstCharCode <= 0xdbff) { // Last character of the left split is the start of a surrogate utf16/ucs2 pair. // We don't support splitting of surrogate pairs because this may lead to invalid documents. // Replace the invalid character with a unicode replacement character (� / U+FFFD) this.str = this.str.slice(0, offset - 1) + "�"; // replace right as well right.str = "�" + right.str.slice(1); } return right; } /** * @param {ContentString} right * @return {boolean} */ mergeWith(right) { this.str += right.str; return true; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) {} /** * @param {Transaction} transaction */ delete(transaction) {} /** * @param {StructStore} store */ gc(store) {} /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeString(offset === 0 ? this.str : this.str.slice(offset)); } /** * @return {number} */ getRef() { return 4; } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentString} */ const readContentString = (decoder) => new ContentString(decoder.readString()); /** * @type {Array>} * @private */ const typeRefs = [readYArray, readYMap, readYText, readYXmlElement, readYXmlFragment, readYXmlHook, readYXmlText]; const YArrayRefID = 0; const YMapRefID = 1; const YTextRefID = 2; const YXmlElementRefID = 3; const YXmlFragmentRefID = 4; const YXmlHookRefID = 5; const YXmlTextRefID = 6; /** * @private */ class ContentType { /** * @param {AbstractType} type */ constructor(type) { /** * @type {AbstractType} */ this.type = type; } /** * @return {number} */ getLength() { return 1; } /** * @return {Array} */ getContent() { return [this.type]; } /** * @return {boolean} */ isCountable() { return true; } /** * @return {ContentType} */ copy() { return new ContentType(this.type._copy()); } /** * @param {number} offset * @return {ContentType} */ splice(offset) { throw methodUnimplemented(); } /** * @param {ContentType} right * @return {boolean} */ mergeWith(right) { return false; } /** * @param {Transaction} transaction * @param {Item} item */ integrate(transaction, item) { this.type._integrate(transaction.doc, item); } /** * @param {Transaction} transaction */ delete(transaction) { let item = this.type._start; while (item !== null) { if (!item.deleted) { item.delete(transaction); } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) { // This will be gc'd later and we want to merge it if possible // We try to merge all deleted items after each transaction, // but we have no knowledge about that this needs to be merged // since it is not in transaction.ds. Hence we add it to transaction._mergeStructs transaction._mergeStructs.push(item); } item = item.right; } this.type._map.forEach((item) => { if (!item.deleted) { item.delete(transaction); } else if (item.id.clock < (transaction.beforeState.get(item.id.client) || 0)) { // same as above transaction._mergeStructs.push(item); } }); transaction.changed.delete(this.type); } /** * @param {StructStore} store */ gc(store) { let item = this.type._start; while (item !== null) { item.gc(store, true); item = item.right; } this.type._start = null; this.type._map.forEach( /** @param {Item | null} item */ (item) => { while (item !== null) { item.gc(store, true); item = item.left; } }, ); this.type._map = new Map(); } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { this.type._write(encoder); } /** * @return {number} */ getRef() { return 7; } } /** * @private * * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentType} */ const readContentType = (decoder) => new ContentType(typeRefs[decoder.readTypeRef()](decoder)); /** * @todo This should return several items * * @param {StructStore} store * @param {ID} id * @return {{item:Item, diff:number}} */ const followRedone = (store, id) => { /** * @type {ID|null} */ let nextID = id; let diff = 0; let item; do { if (diff > 0) { nextID = createID(nextID.client, nextID.clock + diff); } item = getItem(store, nextID); diff = nextID.clock - item.id.clock; nextID = item.redone; } while (nextID !== null && item instanceof Item$1); return { item, diff, }; }; /** * Make sure that neither item nor any of its parents is ever deleted. * * This property does not persist when storing it into a database or when * sending it to other peers * * @param {Item|null} item * @param {boolean} keep */ const keepItem = (item, keep) => { while (item !== null && item.keep !== keep) { item.keep = keep; item = /** @type {AbstractType} */ (item.parent)._item; } }; /** * Split leftItem into two items * @param {Transaction} transaction * @param {Item} leftItem * @param {number} diff * @return {Item} * * @function * @private */ const splitItem = (transaction, leftItem, diff) => { // create rightItem const { client, clock } = leftItem.id; const rightItem = new Item$1( createID(client, clock + diff), leftItem, createID(client, clock + diff - 1), leftItem.right, leftItem.rightOrigin, leftItem.parent, leftItem.parentSub, leftItem.content.splice(diff), ); if (leftItem.deleted) { rightItem.markDeleted(); } if (leftItem.keep) { rightItem.keep = true; } if (leftItem.redone !== null) { rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff); } // update left (do not set leftItem.rightOrigin as it will lead to problems when syncing) leftItem.right = rightItem; // update right if (rightItem.right !== null) { rightItem.right.left = rightItem; } // right is more specific. transaction._mergeStructs.push(rightItem); // update parent._map if (rightItem.parentSub !== null && rightItem.right === null) { /** @type {AbstractType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem); } leftItem.length = diff; return rightItem; }; /** * @param {Array} stack * @param {ID} id */ const isDeletedByUndoStack = (stack, id) => some(stack, /** @param {StackItem} s */ (s) => isDeleted(s.deletions, id)); /** * Redoes the effect of this operation. * * @param {Transaction} transaction The Yjs instance. * @param {Item} item * @param {Set} redoitems * @param {DeleteSet} itemsToDelete * @param {boolean} ignoreRemoteMapChanges * @param {import('../utils/UndoManager.js').UndoManager} um * * @return {Item|null} * * @private */ const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) => { const doc = transaction.doc; const store = doc.store; const ownClientID = doc.clientID; const redone = item.redone; if (redone !== null) { return getItemCleanStart(transaction, redone); } let parentItem = /** @type {AbstractType} */ (item.parent)._item; /** * @type {Item|null} */ let left = null; /** * @type {Item|null} */ let right; // make sure that parent is redone if (parentItem !== null && parentItem.deleted === true) { // try to undo parent if it will be undone anyway if (parentItem.redone === null && (!redoitems.has(parentItem) || redoItem(transaction, parentItem, redoitems, itemsToDelete, ignoreRemoteMapChanges, um) === null)) { return null; } while (parentItem.redone !== null) { parentItem = getItemCleanStart(transaction, parentItem.redone); } } const parentType = parentItem === null ? /** @type {AbstractType} */ (item.parent) : /** @type {ContentType} */ (parentItem.content).type; if (item.parentSub === null) { // Is an array item. Insert at the old position left = item.left; right = item; // find next cloned_redo items while (left !== null) { /** * @type {Item|null} */ let leftTrace = left; // trace redone until parent matches while (leftTrace !== null && /** @type {AbstractType} */ (leftTrace.parent)._item !== parentItem) { leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone); } if (leftTrace !== null && /** @type {AbstractType} */ (leftTrace.parent)._item === parentItem) { left = leftTrace; break; } left = left.left; } while (right !== null) { /** * @type {Item|null} */ let rightTrace = right; // trace redone until parent matches while (rightTrace !== null && /** @type {AbstractType} */ (rightTrace.parent)._item !== parentItem) { rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone); } if (rightTrace !== null && /** @type {AbstractType} */ (rightTrace.parent)._item === parentItem) { right = rightTrace; break; } right = right.right; } } else { right = null; if (item.right && !ignoreRemoteMapChanges) { left = item; // Iterate right while right is in itemsToDelete // If it is intended to delete right while item is redone, we can expect that item should replace right. while ( left !== null && left.right !== null && (left.right.redone || isDeleted(itemsToDelete, left.right.id) || isDeletedByUndoStack(um.undoStack, left.right.id) || isDeletedByUndoStack(um.redoStack, left.right.id)) ) { left = left.right; // follow redone while (left.redone) left = getItemCleanStart(transaction, left.redone); } if (left && left.right !== null) { // It is not possible to redo this item because it conflicts with a // change from another client return null; } } else { left = parentType._map.get(item.parentSub) || null; } // drop cross-parent left so origin doesn't mislead the remote (#757) if (left !== null && /** @type {AbstractType} */ (left.parent)._item !== parentItem) { left = parentType._map.get(item.parentSub) || null; } } const nextClock = getState(store, ownClientID); const nextId = createID(ownClientID, nextClock); const redoneItem = new Item$1(nextId, left, left && left.lastId, right, right && right.id, parentType, item.parentSub, item.content.copy()); item.redone = nextId; keepItem(redoneItem, true); redoneItem.integrate(transaction, 0); return redoneItem; }; /** * Abstract class that represents any content. */ let Item$1 = class Item extends AbstractStruct { /** * @param {ID} id * @param {Item | null} left * @param {ID | null} origin * @param {Item | null} right * @param {ID | null} rightOrigin * @param {AbstractType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. * @param {string | null} parentSub * @param {AbstractContent} content */ constructor(id, left, origin, right, rightOrigin, parent, parentSub, content) { super(id, content.getLength()); /** * The item that was originally to the left of this item. * @type {ID | null} */ this.origin = origin; /** * The item that is currently to the left of this item. * @type {Item | null} */ this.left = left; /** * The item that is currently to the right of this item. * @type {Item | null} */ this.right = right; /** * The item that was originally to the right of this item. * @type {ID | null} */ this.rightOrigin = rightOrigin; /** * @type {AbstractType|ID|null} */ this.parent = parent; /** * If the parent refers to this item with some kind of key (e.g. YMap, the * key is specified here. The key is then used to refer to the list in which * to insert this item. If `parentSub = null` type._start is the list in * which to insert to. Otherwise it is `parent._map`. * @type {String | null} */ this.parentSub = parentSub; /** * If this type's effect is redone this type refers to the type that undid * this operation. * @type {ID | null} */ this.redone = null; /** * @type {AbstractContent} */ this.content = content; /** * bit1: keep * bit2: countable * bit3: deleted * bit4: mark - mark node as fast-search-marker * @type {number} byte */ this.info = this.content.isCountable() ? BIT2 : 0; } /** * This is used to mark the item as an indexed fast-search marker * * @type {boolean} */ set marker(isMarked) { if ((this.info & BIT4) > 0 !== isMarked) { this.info ^= BIT4; } } get marker() { return (this.info & BIT4) > 0; } /** * If true, do not garbage collect this Item. */ get keep() { return (this.info & BIT1) > 0; } set keep(doKeep) { if (this.keep !== doKeep) { this.info ^= BIT1; } } get countable() { return (this.info & BIT2) > 0; } /** * Whether this item was deleted or not. * @type {Boolean} */ get deleted() { return (this.info & BIT3) > 0; } set deleted(doDelete) { if (this.deleted !== doDelete) { this.info ^= BIT3; } } markDeleted() { this.info |= BIT3; } /** * Return the creator clientID of the missing op or define missing items and return null. * * @param {Transaction} transaction * @param {StructStore} store * @return {null | number} */ getMissing(transaction, store) { if (this.origin && this.origin.client !== this.id.client && this.origin.clock >= getState(store, this.origin.client)) { return this.origin.client; } if (this.rightOrigin && this.rightOrigin.client !== this.id.client && this.rightOrigin.clock >= getState(store, this.rightOrigin.client)) { return this.rightOrigin.client; } if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) { return this.parent.client; } // We have all missing ids, now find the items if (this.origin) { this.left = getItemCleanEnd(transaction, store, this.origin); this.origin = this.left.lastId; } if (this.rightOrigin) { this.right = getItemCleanStart(transaction, this.rightOrigin); this.rightOrigin = this.right.id; } if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) { this.parent = null; } else if (!this.parent) { // only set parent if this shouldn't be garbage collected if (this.left && this.left.constructor === Item) { this.parent = this.left.parent; this.parentSub = this.left.parentSub; } else if (this.right && this.right.constructor === Item) { this.parent = this.right.parent; this.parentSub = this.right.parentSub; } } else if (this.parent.constructor === ID) { const parentItem = getItem(store, this.parent); if (parentItem.constructor === GC) { this.parent = null; } else { this.parent = /** @type {ContentType} */ (parentItem.content).type; } } return null; } /** * @param {Transaction} transaction * @param {number} offset */ integrate(transaction, offset) { if (offset > 0) { this.id.clock += offset; this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1)); this.origin = this.left.lastId; this.content = this.content.splice(offset); this.length -= offset; } if (this.parent) { if ((!this.left && (!this.right || this.right.left !== null)) || (this.left && this.left.right !== this.right)) { /** * @type {Item|null} */ let left = this.left; /** * @type {Item|null} */ let o; // set o to the first conflicting item if (left !== null) { o = left.right; } else if (this.parentSub !== null) { o = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null; while (o !== null && o.left !== null) { o = o.left; } } else { o = /** @type {AbstractType} */ (this.parent)._start; } // TODO: use something like DeleteSet here (a tree implementation would be best) // @todo use global set definitions /** * @type {Set} */ const conflictingItems = new Set(); /** * @type {Set} */ const itemsBeforeOrigin = new Set(); // Let c in conflictingItems, b in itemsBeforeOrigin // ***{origin}bbbb{this}{c,b}{c,b}{o}*** // Note that conflictingItems is a subset of itemsBeforeOrigin while (o !== null && o !== this.right) { itemsBeforeOrigin.add(o); conflictingItems.add(o); if (compareIDs(this.origin, o.origin)) { // case 1 if (o.id.client < this.id.client) { left = o; conflictingItems.clear(); } else if (compareIDs(this.rightOrigin, o.rightOrigin)) { // this and o are conflicting and point to the same integration points. The id decides which item comes first. // Since this is to the left of o, we can break here break; } // else, o might be integrated before an item that this conflicts with. If so, we will find it in the next iterations } else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { // use getItem instead of getItemCleanEnd because we don't want / need to split items. // case 2 if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) { left = o; conflictingItems.clear(); } } else { break; } o = o.right; } this.left = left; } // reconnect left/right + update parent map/start if necessary if (this.left !== null) { const right = this.left.right; this.right = right; this.left.right = this; } else { let r; if (this.parentSub !== null) { r = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null; while (r !== null && r.left !== null) { r = r.left; } } else { r = /** @type {AbstractType} */ (this.parent)._start; /** @type {AbstractType} */ (this.parent)._start = this; } this.right = r; } if (this.right !== null) { this.right.left = this; } else if (this.parentSub !== null) { // set as current parent value if right === null and this is parentSub /** @type {AbstractType} */ (this.parent)._map.set(this.parentSub, this); if (this.left !== null) { // this is the current attribute value of parent. delete right this.left.delete(transaction); } } // adjust length of parent if (this.parentSub === null && this.countable && !this.deleted) { /** @type {AbstractType} */ (this.parent)._length += this.length; } addStruct(transaction.doc.store, this); this.content.integrate(transaction, this); // add parent to transaction.changed addChangedTypeToTransaction(transaction, /** @type {AbstractType} */ (this.parent), this.parentSub); if (/** @type {AbstractType} */ (this.parent._item !== null && /** @type {AbstractType} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) { // delete if parent is deleted or if this is not the current attribute value of parent this.delete(transaction); } } else { // parent is not defined. Integrate GC struct instead new GC(this.id, this.length).integrate(transaction, 0); } } /** * Returns the next non-deleted item */ get next() { let n = this.right; while (n !== null && n.deleted) { n = n.right; } return n; } /** * Returns the previous non-deleted item */ get prev() { let n = this.left; while (n !== null && n.deleted) { n = n.left; } return n; } /** * Computes the last content address of this Item. */ get lastId() { // allocating ids is pretty costly because of the amount of ids created, so we try to reuse whenever possible return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1); } /** * Try to merge two items * * @param {Item} right * @return {boolean} */ mergeWith(right) { if ( this.constructor === right.constructor && compareIDs(right.origin, this.lastId) && this.right === right && compareIDs(this.rightOrigin, right.rightOrigin) && this.id.client === right.id.client && this.id.clock + this.length === right.id.clock && this.deleted === right.deleted && this.redone === null && right.redone === null && this.content.constructor === right.content.constructor && this.content.mergeWith(right.content) ) { const searchMarker = /** @type {AbstractType} */ (this.parent)._searchMarker; if (searchMarker) { searchMarker.forEach((marker) => { if (marker.p === right) { // right is going to be "forgotten" so we need to update the marker marker.p = this; // adjust marker index if (!this.deleted && this.countable) { marker.index -= this.length; } } }); } if (right.keep) { this.keep = true; } this.right = right.right; if (this.right !== null) { this.right.left = this; } this.length += right.length; return true; } return false; } /** * Mark this Item as deleted. * * @param {Transaction} transaction */ delete(transaction) { if (!this.deleted) { const parent = /** @type {AbstractType} */ (this.parent); // adjust the length of parent if (this.countable && this.parentSub === null) { parent._length -= this.length; } this.markDeleted(); addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length); addChangedTypeToTransaction(transaction, parent, this.parentSub); this.content.delete(transaction); } } /** * @param {StructStore} store * @param {boolean} parentGCd */ gc(store, parentGCd) { if (!this.deleted) { throw unexpectedCase(); } this.content.gc(store); if (parentGCd) { replaceStruct(store, this, new GC(this.id, this.length)); } else { this.content = new ContentDeleted(this.length); } } /** * Transform the properties of this type to binary and write it to an * BinaryEncoder. * * This is called when this Item is sent to a remote peer. * * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. * @param {number} offset */ write(encoder, offset) { const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin; const rightOrigin = this.rightOrigin; const parentSub = this.parentSub; const info = (this.content.getRef() & BITS5) | (origin === null ? 0 : BIT8) | // origin is defined (rightOrigin === null ? 0 : BIT7) | // right origin is defined (parentSub === null ? 0 : BIT6); // parentSub is non-null encoder.writeInfo(info); if (origin !== null) { encoder.writeLeftID(origin); } if (rightOrigin !== null) { encoder.writeRightID(rightOrigin); } if (origin === null && rightOrigin === null) { const parent = /** @type {AbstractType} */ (this.parent); if (parent._item !== undefined) { const parentItem = parent._item; if (parentItem === null) { // parent type on y._map // find the correct key const ykey = findRootTypeKey(parent); encoder.writeParentInfo(true); // write parentYKey encoder.writeString(ykey); } else { encoder.writeParentInfo(false); // write parent id encoder.writeLeftID(parentItem.id); } } else if (parent.constructor === String) { // this edge case was added by differential updates encoder.writeParentInfo(true); // write parentYKey encoder.writeString(parent); } else if (parent.constructor === ID) { encoder.writeParentInfo(false); // write parent id encoder.writeLeftID(parent); } else { unexpectedCase(); } if (parentSub !== null) { encoder.writeString(parentSub); } } this.content.write(encoder, offset); } }; /** * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @param {number} info */ const readItemContent = (decoder, info) => contentRefs[info & BITS5](decoder); /** * A lookup map for reading Item content. * * @type {Array} */ const contentRefs = [ () => { unexpectedCase(); }, // GC is not ItemContent readContentDeleted, // 1 readContentJSON, // 2 readContentBinary, // 3 readContentString, // 4 readContentEmbed, // 5 readContentFormat, // 6 readContentType, // 7 readContentAny, // 8 readContentDoc, // 9 () => { unexpectedCase(); }, // 10 - Skip is not ItemContent ]; const structSkipRefNumber = 10; /** * @private */ class Skip extends AbstractStruct { get deleted() { return true; } delete() {} /** * @param {Skip} right * @return {boolean} */ mergeWith(right) { if (this.constructor !== right.constructor) { return false; } this.length += right.length; return true; } /** * @param {Transaction} transaction * @param {number} offset */ integrate(transaction, offset) { // skip structs cannot be integrated unexpectedCase(); } /** * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder * @param {number} offset */ write(encoder, offset) { encoder.writeInfo(structSkipRefNumber); // write as VarUint because Skips can't make use of predictable length-encoding writeVarUint(encoder.restEncoder, this.length - offset); } /** * @param {Transaction} transaction * @param {StructStore} store * @return {null | number} */ getMissing(transaction, store) { return null; } } /** eslint-env browser */ const glo = /** @type {any} */ ( typeof globalThis !== "undefined" ? globalThis : typeof window !== "undefined" ? window // @ts-ignore : typeof global !== "undefined" ? global : {} ); const importIdentifier = "__ $YJS$ __"; if (glo[importIdentifier] === true) { /** * Dear reader of this message. Please take this seriously. * * If you see this message, make sure that you only import one version of Yjs. In many cases, * your package manager installs two versions of Yjs that are used by different packages within your project. * Another reason for this message is that some parts of your project use the commonjs version of Yjs * and others use the EcmaScript version of Yjs. * * This often leads to issues that are hard to debug. We often need to perform constructor checks, * e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to * do the constructor checks anymore - which might break the CRDT algorithm. * * https://github.com/yjs/yjs/issues/438 */ console.error("Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438"); } glo[importIdentifier] = true; /** * Mutual exclude for JavaScript. * * @module mutex */ /** * @callback mutex * @param {function():void} cb Only executed when this mutex is not in the current stack * @param {function():void} [elseCb] Executed when this mutex is in the current stack */ /** * Creates a mutual exclude function with the following property: * * ```js * const mutex = createMutex() * mutex(() => { * // This function is immediately executed * mutex(() => { * // This function is not executed, as the mutex is already active. * }) * }) * ``` * * @return {mutex} A mutual exclude function * @public */ const createMutex = () => { let token = true; return (f, g) => { if (token) { token = false; try { f(); } finally { token = true; } } else if (g !== undefined) { g(); } }; }; /** * Efficient diffs. * * @module diff */ /** * A SimpleDiff describes a change on a String. * * ```js * console.log(a) // the old value * console.log(b) // the updated value * // Apply changes of diff (pseudocode) * a.remove(diff.index, diff.remove) // Remove `diff.remove` characters * a.insert(diff.index, diff.insert) // Insert `diff.insert` * a === b // values match * ``` * * @template {string|Array} T * @typedef {Object} SimpleDiff * @property {Number} index The index where changes were applied * @property {Number} remove The number of characters to delete starting * at `index`. * @property {T} insert The new text to insert at `index` after applying */ const highSurrogateRegex = /[\uD800-\uDBFF]/; const lowSurrogateRegex = /[\uDC00-\uDFFF]/; /** * Create a diff between two strings. This diff implementation is highly * efficient, but not very sophisticated. * * @function * * @param {string} a The old version of the string * @param {string} b The updated version of the string * @return {SimpleDiff} The diff description. */ const simpleDiffString = (a, b) => { let left = 0; // number of same characters counting from left let right = 0; // number of same characters counting from right while (left < a.length && left < b.length && a[left] === b[left]) { left++; } // If the last same character is a high surrogate, we need to rollback to the previous character if (left > 0 && highSurrogateRegex.test(a[left - 1])) left--; while (right + left < a.length && right + left < b.length && a[a.length - right - 1] === b[b.length - right - 1]) { right++; } // If the last same character is a low surrogate, we need to rollback to the previous character if (right > 0 && lowSurrogateRegex.test(a[a.length - right])) right--; return { index: left, remove: a.length - left - right, insert: b.slice(left, b.length - right), }; }; /** * @todo Remove in favor of simpleDiffString * @deprecated */ const simpleDiff = simpleDiffString; /** * The unique prosemirror plugin key for syncPlugin * * @public */ const ySyncPluginKey = new PluginKey("y-sync"); /** * The unique prosemirror plugin key for undoPlugin * * @public * @type {PluginKey} */ const yUndoPluginKey = new PluginKey("y-undo"); /** * The unique prosemirror plugin key for cursorPlugin * * @public */ const yCursorPluginKey = new PluginKey("yjs-cursor"); /** * @module sha256 * Spec: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf * Resources: * - https://web.archive.org/web/20150315061807/http://csrc.nist.gov/groups/STM/cavp/documents/shs/sha256-384-512.pdf */ /** * @param {number} w - a 32bit uint * @param {number} shift */ const rotr = (w, shift) => (w >>> shift) | (w << (32 - shift)); /** * Helper for SHA-224 & SHA-256. See 4.1.2. * @param {number} x */ const sum0to256 = (x) => rotr(x, 2) ^ rotr(x, 13) ^ rotr(x, 22); /** * Helper for SHA-224 & SHA-256. See 4.1.2. * @param {number} x */ const sum1to256 = (x) => rotr(x, 6) ^ rotr(x, 11) ^ rotr(x, 25); /** * Helper for SHA-224 & SHA-256. See 4.1.2. * @param {number} x */ const sigma0to256 = (x) => rotr(x, 7) ^ rotr(x, 18) ^ (x >>> 3); /** * Helper for SHA-224 & SHA-256. See 4.1.2. * @param {number} x */ const sigma1to256 = (x) => rotr(x, 17) ^ rotr(x, 19) ^ (x >>> 10); // @todo don't init these variables globally /** * See 4.2.2: Constant for sha256 & sha224 * These words represent the first thirty-two bits of the fractional parts of * the cube roots of the first sixty-four prime numbers. In hex, these constant words are (from left to * right) */ const K$1 = new Uint32Array([ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, ]); /** * See 5.3.3. Initial hash value. * * These words were obtained by taking the first thirty-two bits of the fractional parts of the * square roots of the first eight prime numbers. * * @todo shouldn't be a global variable */ const HINIT = new Uint32Array([0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19]); // time to beat: (large value < 4.35s) class Hasher { constructor() { const buf = new ArrayBuffer(64 + 64 * 4); // Init working variables using a single arraybuffer this._H = new Uint32Array(buf, 0, 8); this._H.set(HINIT); // "Message schedule" - a working variable this._W = new Uint32Array(buf, 64, 64); } _updateHash() { const H = this._H; const W = this._W; for (let t = 16; t < 64; t++) { W[t] = sigma1to256(W[t - 2]) + W[t - 7] + sigma0to256(W[t - 15]) + W[t - 16]; } let a = H[0]; let b = H[1]; let c = H[2]; let d = H[3]; let e = H[4]; let f = H[5]; let g = H[6]; let h = H[7]; for (let tt = 0, T1, T2; tt < 64; tt++) { T1 = (h + sum1to256(e) + ((e & f) ^ (~e & g)) + K$1[tt] + W[tt]) >>> 0; T2 = (sum0to256(a) + ((a & b) ^ (a & c) ^ (b & c))) >>> 0; h = g; g = f; f = e; e = (d + T1) >>> 0; d = c; c = b; b = a; a = (T1 + T2) >>> 0; } H[0] += a; H[1] += b; H[2] += c; H[3] += d; H[4] += e; H[5] += f; H[6] += g; H[7] += h; } /** * Returns a 32-byte hash. * * @param {Uint8Array} data */ digest(data) { let i = 0; for (; i + 56 <= data.length; ) { // write data in big endianess let j = 0; for (; j < 16 && i + 3 < data.length; j++) { this._W[j] = (data[i++] << 24) | (data[i++] << 16) | (data[i++] << 8) | data[i++]; } if (i % 64 !== 0) { // there is still room to write partial content and the ending bit. this._W.fill(0, j, 16); while (i < data.length) { this._W[j] |= data[i] << ((3 - (i % 4)) * 8); i++; } this._W[j] |= BIT8 << ((3 - (i % 4)) * 8); } this._updateHash(); } // same check as earlier - the ending bit has been written const isPaddedWith1 = i % 64 !== 0; this._W.fill(0, 0, 16); let j = 0; for (; i < data.length; j++) { for (let ci = 3; ci >= 0 && i < data.length; ci--) { this._W[j] |= data[i++] << (ci * 8); } } // Write padding of the message. See 5.1.2. if (!isPaddedWith1) { this._W[j - (i % 4 === 0 ? 0 : 1)] |= BIT8 << ((3 - (i % 4)) * 8); } // write length of message (size in bits) as 64 bit uint // @todo test that this works correctly this._W[14] = data.byteLength / BIT30; // same as data.byteLength >>> 30 - but works on floats this._W[15] = data.byteLength * 8; this._updateHash(); // correct H endianness to use big endiannes and return a Uint8Array const dv = new Uint8Array(32); for (let i = 0; i < this._H.length; i++) { for (let ci = 0; ci < 4; ci++) { dv[i * 4 + ci] = this._H[i] >>> ((3 - ci) * 8); } } return dv; } } /** * Returns a 32-byte hash. * * @param {Uint8Array} data */ const digest = (data) => new Hasher().digest(data); /** * Custom function to transform sha256 hash to N byte * * @param {Uint8Array} digest */ const _convolute = (digest) => { const N = 6; for (let i = N; i < digest.length; i++) { digest[i % N] = digest[i % N] ^ digest[i]; } return digest.slice(0, N); }; /** * @param {any} json */ const hashOfJSON = (json) => toBase64(_convolute(digest(encodeAny(json)))); /** * @module bindings/prosemirror */ /** * @param {Y.Item} item * @param {Y.Snapshot} [snapshot] */ const isVisible = (item, snapshot) => snapshot === undefined ? !item.deleted : snapshot.sv.has(item.id.client) && /** @type {number} */ (snapshot.sv.get(item.id.client)) > item.id.clock && !isDeleted(snapshot.ds, item.id); /** * Either a node if type is YXmlElement or an Array of text nodes if YXmlText * @typedef {Map, PModel.Node | Array>} ProsemirrorMapping */ /** * @typedef {Object} ColorDef * @property {string} ColorDef.light * @property {string} ColorDef.dark */ /** * @typedef {Object} YSyncOpts * @property {Array} [YSyncOpts.colors] * @property {Map} [YSyncOpts.colorMapping] * @property {Y.PermanentUserData|null} [YSyncOpts.permanentUserData] * @property {ProsemirrorMapping} [YSyncOpts.mapping] * @property {function} [YSyncOpts.onFirstRender] Fired when the content from Yjs is initially rendered to ProseMirror */ /** * @type {Array} */ const defaultColors = [{ light: "#ecd44433", dark: "#ecd444" }]; /** * @param {Map} colorMapping * @param {Array} colors * @param {string} user * @return {ColorDef} */ const getUserColor = (colorMapping, colors, user) => { // @todo do not hit the same color twice if possible if (!colorMapping.has(user)) { if (colorMapping.size < colors.length) { const usedColors = create$8(); colorMapping.forEach((color) => usedColors.add(color)); colors = colors.filter((color) => !usedColors.has(color)); } colorMapping.set(user, oneOf$1(colors)); } return /** @type {ColorDef} */ (colorMapping.get(user)); }; /** * This plugin listens to changes in prosemirror view and keeps yXmlState and view in sync. * * This plugin also keeps references to the type and the shared document so other plugins can access it. * @param {Y.XmlFragment} yXmlFragment * @param {YSyncOpts} opts * @return {any} Returns a prosemirror plugin that binds to this type */ const ySyncPlugin = (yXmlFragment, { colors = defaultColors, colorMapping = new Map(), permanentUserData = null, onFirstRender = () => {}, mapping } = {}) => { let initialContentChanged = false; const binding = new ProsemirrorBinding(yXmlFragment, mapping); const plugin = new Plugin({ props: { editable: (state) => { const syncState = ySyncPluginKey.getState(state); return syncState.snapshot == null && syncState.prevSnapshot == null; }, }, key: ySyncPluginKey, state: { /** * @returns {any} */ init: (_initargs, _state) => { return { type: yXmlFragment, doc: yXmlFragment.doc, binding, snapshot: null, prevSnapshot: null, isChangeOrigin: false, isUndoRedoOperation: false, addToHistory: true, colors, colorMapping, permanentUserData, }; }, apply: (tr, pluginState) => { const change = tr.getMeta(ySyncPluginKey); if (change !== undefined) { pluginState = Object.assign({}, pluginState); for (const key in change) { pluginState[key] = change[key]; } } pluginState.addToHistory = tr.getMeta("addToHistory") !== false; // always set isChangeOrigin. If undefined, this is not change origin. pluginState.isChangeOrigin = change !== undefined && !!change.isChangeOrigin; pluginState.isUndoRedoOperation = change !== undefined && !!change.isChangeOrigin && !!change.isUndoRedoOperation; if (binding.prosemirrorView !== null) { if (change !== undefined && (change.snapshot != null || change.prevSnapshot != null)) { // snapshot changed, rerender next timeout(0, () => { if (binding.prosemirrorView == null) { return; } if (change.restore == null) { binding._renderSnapshot(change.snapshot, change.prevSnapshot, pluginState); } else { binding._renderSnapshot(change.snapshot, change.snapshot, pluginState); // reset to current prosemirror state delete pluginState.restore; delete pluginState.snapshot; delete pluginState.prevSnapshot; binding.mux(() => { binding._prosemirrorChanged(binding.prosemirrorView.state.doc); }); } }); } } return pluginState; }, }, view: (view) => { binding.initView(view); if (mapping == null) { // force rerender to update the bindings mapping binding._forceRerender(); } onFirstRender(); return { update: () => { const pluginState = plugin.getState(view.state); if (pluginState.snapshot == null && pluginState.prevSnapshot == null) { if ( // If the content doesn't change initially, we don't render anything to Yjs // If the content was cleared by a user action, we want to catch the change and // represent it in Yjs initialContentChanged || view.state.doc.content.findDiffStart(view.state.doc.type.createAndFill().content) !== null ) { initialContentChanged = true; if (pluginState.addToHistory === false && !pluginState.isChangeOrigin) { const yUndoPluginState = yUndoPluginKey.getState(view.state); /** * @type {Y.UndoManager} */ const um = yUndoPluginState && yUndoPluginState.undoManager; if (um) { um.stopCapturing(); } } binding.mux(() => { /** @type {Y.Doc} */ (pluginState.doc).transact((tr) => { tr.meta.set("addToHistory", pluginState.addToHistory); binding._prosemirrorChanged(view.state.doc); }, ySyncPluginKey); }); } } }, destroy: () => { binding.destroy(); }, }; }, }); return plugin; }; /** * @param {import('prosemirror-state').Transaction} tr * @param {ReturnType} relSel * @param {ProsemirrorBinding} binding */ const restoreRelativeSelection = (tr, relSel, binding) => { if (relSel !== null && relSel.anchor !== null && relSel.head !== null) { if (relSel.type === "all") { tr.setSelection(new AllSelection(tr.doc)); } else if (relSel.type === "node") { const anchor = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.anchor, binding.mapping); tr.setSelection(NodeSelection.create(tr.doc, anchor)); } else { const anchor = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.anchor, binding.mapping); const head = relativePositionToAbsolutePosition(binding.doc, binding.type, relSel.head, binding.mapping); if (anchor !== null && head !== null) { const sel = TextSelection.between(tr.doc.resolve(anchor), tr.doc.resolve(head)); tr.setSelection(sel); } } } }; /** * @param {ProsemirrorBinding} pmbinding * @param {import('prosemirror-state').EditorState} state */ const getRelativeSelection = (pmbinding, state) => ({ type: /** @type {any} */ (state.selection).jsonID, anchor: absolutePositionToRelativePosition(state.selection.anchor, pmbinding.type, pmbinding.mapping), head: absolutePositionToRelativePosition(state.selection.head, pmbinding.type, pmbinding.mapping), }); /** * Binding for prosemirror. * * @protected */ class ProsemirrorBinding { /** * @param {Y.XmlFragment} yXmlFragment The bind source * @param {ProsemirrorMapping} mapping */ constructor(yXmlFragment, mapping = new Map()) { this.type = yXmlFragment; /** * this will be set once the view is created * @type {any} */ this.prosemirrorView = null; this.mux = createMutex(); this.mapping = mapping; /** * Is overlapping mark - i.e. mark does not exclude itself. * * @type {Map} */ this.isOMark = new Map(); this._observeFunction = this._typeChanged.bind(this); /** * @type {Y.Doc} */ // @ts-ignore this.doc = yXmlFragment.doc; /** * current selection as relative positions in the Yjs model */ this.beforeTransactionSelection = null; this.beforeAllTransactions = () => { if (this.beforeTransactionSelection === null && this.prosemirrorView != null) { this.beforeTransactionSelection = getRelativeSelection(this, this.prosemirrorView.state); } }; this.afterAllTransactions = () => { this.beforeTransactionSelection = null; }; this._domSelectionInView = null; } /** * Create a transaction for changing the prosemirror state. * * @returns */ get _tr() { return this.prosemirrorView.state.tr.setMeta("addToHistory", false); } _isLocalCursorInView() { if (!this.prosemirrorView.hasFocus()) return false; if (isBrowser && this._domSelectionInView === null) { // Calculate the domSelectionInView and clear by next tick after all events are finished timeout(0, () => { this._domSelectionInView = null; }); this._domSelectionInView = this._isDomSelectionInView(); } return this._domSelectionInView; } _isDomSelectionInView() { const selection = this.prosemirrorView._root.getSelection(); if (selection == null || selection.anchorNode == null) return false; const range = this.prosemirrorView._root.createRange(); range.setStart(selection.anchorNode, selection.anchorOffset); range.setEnd(selection.focusNode, selection.focusOffset); // This is a workaround for an edgecase where getBoundingClientRect will // return zero values if the selection is collapsed at the start of a newline // see reference here: https://stackoverflow.com/a/59780954 const rects = range.getClientRects(); if (rects.length === 0) { // probably buggy newline behavior, explicitly select the node contents if (range.startContainer && range.collapsed) { range.selectNodeContents(range.startContainer); } } const bounding = range.getBoundingClientRect(); const documentElement = doc.documentElement; return ( bounding.bottom >= 0 && bounding.right >= 0 && bounding.left <= (window.innerWidth || documentElement.clientWidth || 0) && bounding.top <= (window.innerHeight || documentElement.clientHeight || 0) ); } /** * @param {Y.Snapshot} snapshot * @param {Y.Snapshot} prevSnapshot */ renderSnapshot(snapshot, prevSnapshot) { if (!prevSnapshot) { prevSnapshot = createSnapshot(createDeleteSet(), new Map()); } this.prosemirrorView.dispatch(this._tr.setMeta(ySyncPluginKey, { snapshot, prevSnapshot })); } unrenderSnapshot() { this.mapping.clear(); this.mux(() => { const fragmentContent = this.type .toArray() .map((t) => createNodeFromYElement(/** @type {Y.XmlElement} */ (t), this.prosemirrorView.state.schema, this)) .filter((n) => n !== null); // @ts-ignore const tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new Slice(Fragment.from(fragmentContent), 0, 0)); tr.setMeta(ySyncPluginKey, { snapshot: null, prevSnapshot: null }); this.prosemirrorView.dispatch(tr); }); } _forceRerender() { this.mapping.clear(); this.mux(() => { // If this is a forced rerender, this might neither happen as a pm change nor within a Yjs // transaction. Then the "before selection" doesn't exist. In this case, we need to create a // relative position before replacing content. Fixes #126 const sel = this.beforeTransactionSelection !== null ? null : this.prosemirrorView.state.selection; const fragmentContent = this.type .toArray() .map((t) => createNodeFromYElement(/** @type {Y.XmlElement} */ (t), this.prosemirrorView.state.schema, this)) .filter((n) => n !== null); // @ts-ignore const tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new Slice(Fragment.from(fragmentContent), 0, 0)); if (sel) { /** * If the Prosemirror document we just created from this.type is * smaller than the previous document, the selection might be * out of bound, which would make Prosemirror throw an error. */ const clampedAnchor = min$1(max$1(sel.anchor, 0), tr.doc.content.size); const clampedHead = min$1(max$1(sel.head, 0), tr.doc.content.size); tr.setSelection(TextSelection.create(tr.doc, clampedAnchor, clampedHead)); } this.prosemirrorView.dispatch(tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, binding: this })); }); } /** * @param {Y.Snapshot|Uint8Array} snapshot * @param {Y.Snapshot|Uint8Array} prevSnapshot * @param {Object} pluginState */ _renderSnapshot(snapshot$1, prevSnapshot, pluginState) { /** * The document that contains the full history of this document. * @type {Y.Doc} */ let historyDoc = this.doc; let historyType = this.type; if (!snapshot$1) { snapshot$1 = snapshot(this.doc); } if (snapshot$1 instanceof Uint8Array || prevSnapshot instanceof Uint8Array) { if (!(snapshot$1 instanceof Uint8Array) || !(prevSnapshot instanceof Uint8Array)) { // expected both snapshots to be v2 updates unexpectedCase(); } historyDoc = new Doc({ gc: false }); applyUpdateV2(historyDoc, prevSnapshot); prevSnapshot = snapshot(historyDoc); applyUpdateV2(historyDoc, snapshot$1); snapshot$1 = snapshot(historyDoc); if (historyType._item === null) { /** * If is a root type, we need to find the root key in the initial document * and use it to get the history type. */ const rootKey = Array.from(this.doc.share.keys()).find((key) => this.doc.share.get(key) === this.type); historyType = historyDoc.getXmlFragment(rootKey); } else { /** * If it is a sub type, we use the item id to find the history type. */ const historyStructs = historyDoc.store.clients.get(historyType._item.id.client) ?? []; const itemIndex = findIndexSS(historyStructs, historyType._item.id.clock); const item = /** @type {Y.Item} */ (historyStructs[itemIndex]); const content = /** @type {Y.ContentType} */ (item.content); historyType = /** @type {Y.XmlFragment} */ (content.type); } } // clear mapping because we are going to rerender this.mapping.clear(); this.mux(() => { historyDoc.transact((transaction) => { // before rendering, we are going to sanitize ops and split deleted ops // if they were deleted by seperate users. /** * @type {Y.PermanentUserData} */ const pud = pluginState.permanentUserData; if (pud) { pud.dss.forEach((ds) => { iterateDeletedStructs(transaction, ds, (_item) => {}); }); } /** * @param {'removed'|'added'} type * @param {Y.ID} id */ const computeYChange = (type, id) => { const user = type === "added" ? pud.getUserByClientId(id.client) : pud.getUserByDeletedId(id); return { user, type, color: getUserColor(pluginState.colorMapping, pluginState.colors, user), }; }; // Create document fragment and render const fragmentContent = typeListToArraySnapshot(historyType, new Snapshot(prevSnapshot.ds, snapshot$1.sv)) .map((t) => { if (!t._item.deleted || isVisible(t._item, snapshot$1) || isVisible(t._item, prevSnapshot)) { return createNodeFromYElement(t, this.prosemirrorView.state.schema, { mapping: new Map(), isOMark: new Map() }, snapshot$1, prevSnapshot, computeYChange); } else { // No need to render elements that are not visible by either snapshot. // If a client adds and deletes content in the same snapshot the element is not visible by either snapshot. return null; } }) .filter((n) => n !== null); // @ts-ignore const tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new Slice(Fragment.from(fragmentContent), 0, 0)); this.prosemirrorView.dispatch(tr.setMeta(ySyncPluginKey, { isChangeOrigin: true })); }, ySyncPluginKey); }); } /** * @param {Array>} events * @param {Y.Transaction} transaction */ _typeChanged(events, transaction) { if (this.prosemirrorView == null) return; const syncState = ySyncPluginKey.getState(this.prosemirrorView.state); if (events.length === 0 || syncState.snapshot != null || syncState.prevSnapshot != null) { // drop out if snapshot is active this.renderSnapshot(syncState.snapshot, syncState.prevSnapshot); return; } this.mux(() => { /** * @param {any} _ * @param {Y.AbstractType} type */ const delType = (_, type) => this.mapping.delete(type); iterateDeletedStructs(transaction, transaction.deleteSet, (struct) => { if (struct.constructor === Item$1) { const type = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (struct).content).type; type && this.mapping.delete(type); } }); transaction.changed.forEach(delType); transaction.changedParentTypes.forEach(delType); const fragmentContent = this.type .toArray() .map((t) => createNodeIfNotExists(/** @type {Y.XmlElement | Y.XmlHook} */ (t), this.prosemirrorView.state.schema, this)) .filter((n) => n !== null); // @ts-ignore let tr = this._tr.replace(0, this.prosemirrorView.state.doc.content.size, new Slice(Fragment.from(fragmentContent), 0, 0)); restoreRelativeSelection(tr, this.beforeTransactionSelection, this); tr = tr.setMeta(ySyncPluginKey, { isChangeOrigin: true, isUndoRedoOperation: transaction.origin instanceof UndoManager }); if (this.beforeTransactionSelection !== null && this._isLocalCursorInView()) { tr.scrollIntoView(); } this.prosemirrorView.dispatch(tr); }); } /** * @param {import('prosemirror-model').Node} doc */ _prosemirrorChanged(doc) { this.doc.transact(() => { updateYFragment(this.doc, this.type, doc, this); this.beforeTransactionSelection = getRelativeSelection(this, this.prosemirrorView.state); }, ySyncPluginKey); } /** * View is ready to listen to changes. Register observers. * @param {any} prosemirrorView */ initView(prosemirrorView) { if (this.prosemirrorView != null) this.destroy(); this.prosemirrorView = prosemirrorView; this.doc.on("beforeAllTransactions", this.beforeAllTransactions); this.doc.on("afterAllTransactions", this.afterAllTransactions); this.type.observeDeep(this._observeFunction); } destroy() { if (this.prosemirrorView == null) return; this.prosemirrorView = null; this.type.unobserveDeep(this._observeFunction); this.doc.off("beforeAllTransactions", this.beforeAllTransactions); this.doc.off("afterAllTransactions", this.afterAllTransactions); } } /** * @private * @param {Y.XmlElement | Y.XmlHook} el * @param {PModel.Schema} schema * @param {BindingMetadata} meta * @param {Y.Snapshot} [snapshot] * @param {Y.Snapshot} [prevSnapshot] * @param {function('removed' | 'added', Y.ID):any} [computeYChange] * @return {PModel.Node | null} */ const createNodeIfNotExists = (el, schema, meta, snapshot, prevSnapshot, computeYChange) => { const node = /** @type {PModel.Node} */ (meta.mapping.get(el)); if (node === undefined) { if (el instanceof YXmlElement) { return createNodeFromYElement(el, schema, meta, snapshot, prevSnapshot, computeYChange); } else { throw methodUnimplemented(); // we are currently not handling hooks } } return node; }; /** * @private * @param {Y.XmlElement} el * @param {any} schema * @param {BindingMetadata} meta * @param {Y.Snapshot} [snapshot] * @param {Y.Snapshot} [prevSnapshot] * @param {function('removed' | 'added', Y.ID):any} [computeYChange] * @return {PModel.Node | null} Returns node if node could be created. Otherwise it deletes the yjs type and returns null */ const createNodeFromYElement = (el, schema, meta, snapshot, prevSnapshot, computeYChange) => { const children = []; /** * @param {Y.XmlElement | Y.XmlText} type */ const createChildren = (type) => { if (type instanceof YXmlElement) { const n = createNodeIfNotExists(type, schema, meta, snapshot, prevSnapshot, computeYChange); if (n !== null) { children.push(n); } } else { // If the next ytext exists and was created by us, move the content to the current ytext. // This is a fix for #160 -- duplication of characters when two Y.Text exist next to each // other. const nextytext = /** @type {Y.ContentType} */ (type._item.right?.content)?.type; if (nextytext instanceof YText && !nextytext._item.deleted && nextytext._item.id.client === nextytext.doc.clientID) { type.applyDelta([{ retain: type.length }, ...nextytext.toDelta()]); nextytext.doc.transact((tr) => { nextytext._item.delete(tr); }); } // now create the prosemirror text nodes const ns = createTextNodesFromYText(type, schema, meta, snapshot, prevSnapshot, computeYChange); if (ns !== null) { ns.forEach((textchild) => { if (textchild !== null) { children.push(textchild); } }); } } }; if (snapshot === undefined || prevSnapshot === undefined) { el.toArray().forEach(createChildren); } else { typeListToArraySnapshot(el, new Snapshot(prevSnapshot.ds, snapshot.sv)).forEach(createChildren); } try { const attrs = el.getAttributes(snapshot); if (snapshot !== undefined) { if (!isVisible(/** @type {Y.Item} */ (el._item), snapshot)) { attrs.ychange = computeYChange ? computeYChange("removed", /** @type {Y.Item} */ (el._item).id) : { type: "removed" }; } else if (!isVisible(/** @type {Y.Item} */ (el._item), prevSnapshot)) { attrs.ychange = computeYChange ? computeYChange("added", /** @type {Y.Item} */ (el._item).id) : { type: "added" }; } } const node = schema.node(el.nodeName, attrs, children); meta.mapping.set(el, node); return node; } catch (e) { // an error occured while creating the node. This is probably a result of a concurrent action. /** @type {Y.Doc} */ (el.doc).transact((transaction) => { /** @type {Y.Item} */ (el._item).delete(transaction); }, ySyncPluginKey); meta.mapping.delete(el); return null; } }; /** * @private * @param {Y.XmlText} text * @param {import('prosemirror-model').Schema} schema * @param {BindingMetadata} _meta * @param {Y.Snapshot} [snapshot] * @param {Y.Snapshot} [prevSnapshot] * @param {function('removed' | 'added', Y.ID):any} [computeYChange] * @return {Array|null} */ const createTextNodesFromYText = (text, schema, _meta, snapshot, prevSnapshot, computeYChange) => { const nodes = []; const deltas = text.toDelta(snapshot, prevSnapshot, computeYChange); try { for (let i = 0; i < deltas.length; i++) { const delta = deltas[i]; nodes.push(schema.text(delta.insert, attributesToMarks(delta.attributes, schema))); } } catch (e) { // an error occured while creating the node. This is probably a result of a concurrent action. /** @type {Y.Doc} */ (text.doc).transact((transaction) => { /** @type {Y.Item} */ (text._item).delete(transaction); }, ySyncPluginKey); return null; } // @ts-ignore return nodes; }; /** * @private * @param {Array} nodes prosemirror node * @param {BindingMetadata} meta * @return {Y.XmlText} */ const createTypeFromTextNodes = (nodes, meta) => { const type = new YXmlText(); const delta = nodes.map((node) => ({ // @ts-ignore insert: node.text, attributes: marksToAttributes(node.marks, meta), })); type.applyDelta(delta); meta.mapping.set(type, nodes); return type; }; /** * @private * @param {any} node prosemirror node * @param {BindingMetadata} meta * @return {Y.XmlElement} */ const createTypeFromElementNode = (node, meta) => { const type = new YXmlElement(node.type.name); for (const key in node.attrs) { const val = node.attrs[key]; if (val !== null && key !== "ychange") { type.setAttribute(key, val); } } type.insert( 0, normalizePNodeContent(node).map((n) => createTypeFromTextOrElementNode(n, meta)), ); meta.mapping.set(type, node); return type; }; /** * @private * @param {PModel.Node|Array} node prosemirror text node * @param {BindingMetadata} meta * @return {Y.XmlElement|Y.XmlText} */ const createTypeFromTextOrElementNode = (node, meta) => (node instanceof Array ? createTypeFromTextNodes(node, meta) : createTypeFromElementNode(node, meta)); /** * @param {any} val */ const isObject = (val) => typeof val === "object" && val !== null; /** * @param {any} pattrs * @param {any} yattrs */ const equalAttrs = (pattrs, yattrs) => { const keys = Object.keys(pattrs).filter((key) => pattrs[key] !== null); let eq = keys.length === (yattrs == null ? 0 : Object.keys(yattrs).filter((key) => yattrs[key] !== null).length); for (let i = 0; i < keys.length && eq; i++) { const key = keys[i]; const l = pattrs[key]; const r = yattrs[key]; eq = key === "ychange" || l === r || (isObject(l) && isObject(r) && equalAttrs(l, r)); } return eq; }; /** * @typedef {Array|PModel.Node>} NormalizedPNodeContent */ /** * @param {any} pnode * @return {NormalizedPNodeContent} */ const normalizePNodeContent = (pnode) => { const c = pnode.content.content; const res = []; for (let i = 0; i < c.length; i++) { const n = c[i]; if (n.isText) { const textNodes = []; for (let tnode = c[i]; i < c.length && tnode.isText; tnode = c[++i]) { textNodes.push(tnode); } i--; res.push(textNodes); } else { res.push(n); } } return res; }; /** * @param {Y.XmlText} ytext * @param {Array} ptexts */ const equalYTextPText = (ytext, ptexts) => { const delta = ytext.toDelta(); return ( delta.length === ptexts.length && delta.every( /** @type {(d:any,i:number) => boolean} */ (d, i) => d.insert === /** @type {any} */ (ptexts[i]).text && keys$1(d.attributes || {}).length === ptexts[i].marks.length && every(d.attributes, (attr, yattrname) => { const markname = yattr2markname(yattrname); const pmarks = ptexts[i].marks; return equalAttrs(attr, pmarks.find(/** @param {any} mark */ (mark) => mark.type.name === markname)?.attrs); }), ) ); }; /** * @param {Y.XmlElement|Y.XmlText|Y.XmlHook} ytype * @param {any|Array} pnode */ const equalYTypePNode = (ytype, pnode) => { if (ytype instanceof YXmlElement && !(pnode instanceof Array) && matchNodeName(ytype, pnode)) { const normalizedContent = normalizePNodeContent(pnode); return ytype._length === normalizedContent.length && equalAttrs(ytype.getAttributes(), pnode.attrs) && ytype.toArray().every((ychild, i) => equalYTypePNode(ychild, normalizedContent[i])); } return ytype instanceof YXmlText && pnode instanceof Array && equalYTextPText(ytype, pnode); }; /** * @param {PModel.Node | Array | undefined} mapped * @param {PModel.Node | Array} pcontent */ const mappedIdentity = (mapped, pcontent) => mapped === pcontent || (mapped instanceof Array && pcontent instanceof Array && mapped.length === pcontent.length && mapped.every((a, i) => pcontent[i] === a)); /** * @param {Y.XmlElement} ytype * @param {PModel.Node} pnode * @param {BindingMetadata} meta * @return {{ foundMappedChild: boolean, equalityFactor: number }} */ const computeChildEqualityFactor = (ytype, pnode, meta) => { const yChildren = ytype.toArray(); const pChildren = normalizePNodeContent(pnode); const pChildCnt = pChildren.length; const yChildCnt = yChildren.length; const minCnt = min$1(yChildCnt, pChildCnt); let left = 0; let right = 0; let foundMappedChild = false; for (; left < minCnt; left++) { const leftY = yChildren[left]; const leftP = pChildren[left]; if (mappedIdentity(meta.mapping.get(leftY), leftP)) { foundMappedChild = true; // definite (good) match! } else if (!equalYTypePNode(leftY, leftP)) { break; } } for (; left + right < minCnt; right++) { const rightY = yChildren[yChildCnt - right - 1]; const rightP = pChildren[pChildCnt - right - 1]; if (mappedIdentity(meta.mapping.get(rightY), rightP)) { foundMappedChild = true; } else if (!equalYTypePNode(rightY, rightP)) { break; } } return { equalityFactor: left + right, foundMappedChild, }; }; /** * @param {Y.Text} ytext */ const ytextTrans = (ytext) => { let str = ""; /** * @type {Y.Item|null} */ let n = ytext._start; const nAttrs = {}; while (n !== null) { if (!n.deleted) { if (n.countable && n.content instanceof ContentString) { str += n.content.str; } else if (n.content instanceof ContentFormat) { nAttrs[n.content.key] = null; } } n = n.right; } return { str, nAttrs, }; }; /** * @todo test this more * * @param {Y.Text} ytext * @param {Array} ptexts * @param {BindingMetadata} meta */ const updateYText = (ytext, ptexts, meta) => { meta.mapping.set(ytext, ptexts); const { nAttrs, str } = ytextTrans(ytext); const content = ptexts.map((p) => ({ insert: /** @type {any} */ (p).text, attributes: Object.assign({}, nAttrs, marksToAttributes(p.marks, meta)), })); const { insert, remove, index } = simpleDiff(str, content.map((c) => c.insert).join("")); ytext.delete(index, remove); ytext.insert(index, insert); ytext.applyDelta(content.map((c) => ({ retain: c.insert.length, attributes: c.attributes }))); }; const hashedMarkNameRegex = /(.*)(--[a-zA-Z0-9+/=]{8})$/; /** * @param {string} attrName */ const yattr2markname = (attrName) => hashedMarkNameRegex.exec(attrName)?.[1] ?? attrName; /** * @todo move this to markstoattributes * * @param {Object} attrs * @param {import('prosemirror-model').Schema} schema */ const attributesToMarks = (attrs, schema) => { /** * @type {Array} */ const marks = []; for (const markName in attrs) { // remove hashes if necessary marks.push(schema.mark(yattr2markname(markName), attrs[markName])); } return marks; }; /** * @param {Array} marks * @param {BindingMetadata} meta */ const marksToAttributes = (marks, meta) => { const pattrs = {}; marks.forEach((mark) => { if (mark.type.name !== "ychange") { const isOverlapping = setIfUndefined(meta.isOMark, mark.type, () => !mark.type.excludes(mark.type)); pattrs[isOverlapping ? `${mark.type.name}--${hashOfJSON(mark.toJSON())}` : mark.type.name] = mark.attrs; } }); return pattrs; }; /** * Update a yDom node by syncing the current content of the prosemirror node. * * This is a y-prosemirror internal feature that you can use at your own risk. * * @private * @unstable * * @param {{transact: Function}} y * @param {Y.XmlFragment} yDomFragment * @param {any} pNode * @param {BindingMetadata} meta */ const updateYFragment = (y, yDomFragment, pNode, meta) => { if (yDomFragment instanceof YXmlElement && yDomFragment.nodeName !== pNode.type.name) { throw new Error("node name mismatch!"); } meta.mapping.set(yDomFragment, pNode); // update attributes if (yDomFragment instanceof YXmlElement) { const yDomAttrs = yDomFragment.getAttributes(); const pAttrs = pNode.attrs; for (const key in pAttrs) { if (pAttrs[key] !== null) { if (yDomAttrs[key] !== pAttrs[key] && key !== "ychange") { yDomFragment.setAttribute(key, pAttrs[key]); } } else { yDomFragment.removeAttribute(key); } } // remove all keys that are no longer in pAttrs for (const key in yDomAttrs) { if (pAttrs[key] === undefined) { yDomFragment.removeAttribute(key); } } } // update children const pChildren = normalizePNodeContent(pNode); const pChildCnt = pChildren.length; const yChildren = yDomFragment.toArray(); const yChildCnt = yChildren.length; const minCnt = min$1(pChildCnt, yChildCnt); let left = 0; let right = 0; // find number of matching elements from left for (; left < minCnt; left++) { const leftY = yChildren[left]; const leftP = pChildren[left]; if (!mappedIdentity(meta.mapping.get(leftY), leftP)) { if (equalYTypePNode(leftY, leftP)) { // update mapping meta.mapping.set(leftY, leftP); } else { break; } } } // find number of matching elements from right for (; right + left < minCnt; right++) { const rightY = yChildren[yChildCnt - right - 1]; const rightP = pChildren[pChildCnt - right - 1]; if (!mappedIdentity(meta.mapping.get(rightY), rightP)) { if (equalYTypePNode(rightY, rightP)) { // update mapping meta.mapping.set(rightY, rightP); } else { break; } } } y.transact(() => { // try to compare and update while (yChildCnt - left - right > 0 && pChildCnt - left - right > 0) { const leftY = yChildren[left]; const leftP = pChildren[left]; const rightY = yChildren[yChildCnt - right - 1]; const rightP = pChildren[pChildCnt - right - 1]; if (leftY instanceof YXmlText && leftP instanceof Array) { if (!equalYTextPText(leftY, leftP)) { updateYText(leftY, leftP, meta); } left += 1; } else { let updateLeft = leftY instanceof YXmlElement && matchNodeName(leftY, leftP); let updateRight = rightY instanceof YXmlElement && matchNodeName(rightY, rightP); if (updateLeft && updateRight) { // decide which which element to update const equalityLeft = computeChildEqualityFactor(/** @type {Y.XmlElement} */ (leftY), /** @type {PModel.Node} */ (leftP), meta); const equalityRight = computeChildEqualityFactor(/** @type {Y.XmlElement} */ (rightY), /** @type {PModel.Node} */ (rightP), meta); if (equalityLeft.foundMappedChild && !equalityRight.foundMappedChild) { updateRight = false; } else if (!equalityLeft.foundMappedChild && equalityRight.foundMappedChild) { updateLeft = false; } else if (equalityLeft.equalityFactor < equalityRight.equalityFactor) { updateLeft = false; } else { updateRight = false; } } if (updateLeft) { updateYFragment(y, /** @type {Y.XmlFragment} */ (leftY), /** @type {PModel.Node} */ (leftP), meta); left += 1; } else if (updateRight) { updateYFragment(y, /** @type {Y.XmlFragment} */ (rightY), /** @type {PModel.Node} */ (rightP), meta); right += 1; } else { meta.mapping.delete(yDomFragment.get(left)); yDomFragment.delete(left, 1); yDomFragment.insert(left, [createTypeFromTextOrElementNode(leftP, meta)]); left += 1; } } } const yDelLen = yChildCnt - left - right; if (yChildCnt === 1 && pChildCnt === 0 && yChildren[0] instanceof YXmlText) { meta.mapping.delete(yChildren[0]); // Edge case handling https://github.com/yjs/y-prosemirror/issues/108 // Only delete the content of the Y.Text to retain remote changes on the same Y.Text object yChildren[0].delete(0, yChildren[0].length); } else if (yDelLen > 0) { yDomFragment.slice(left, left + yDelLen).forEach((type) => meta.mapping.delete(type)); yDomFragment.delete(left, yDelLen); } if (left + right < pChildCnt) { const ins = []; for (let i = left; i < pChildCnt - right; i++) { ins.push(createTypeFromTextOrElementNode(pChildren[i], meta)); } yDomFragment.insert(left, ins); } }, ySyncPluginKey); }; /** * @function * @param {Y.XmlElement} yElement * @param {any} pNode Prosemirror Node */ const matchNodeName = (yElement, pNode) => !(pNode instanceof Array) && yElement.nodeName === pNode.type.name; /** * Either a node if type is YXmlElement or an Array of text nodes if YXmlText * @typedef {Map>} ProsemirrorMapping */ /** * Is null if no timeout is in progress. * Is defined if a timeout is in progress. * Maps from view * @type {Map>|null} */ let viewsToUpdate = null; const updateMetas = () => { const ups = /** @type {Map>} */ (viewsToUpdate); viewsToUpdate = null; ups.forEach((metas, view) => { const tr = view.state.tr; const syncState = ySyncPluginKey.getState(view.state); if (syncState && syncState.binding && !syncState.binding.isDestroyed) { metas.forEach((val, key) => { tr.setMeta(key, val); }); view.dispatch(tr); } }); }; const setMeta = (view, key, value) => { if (!viewsToUpdate) { viewsToUpdate = new Map(); timeout(0, updateMetas); } setIfUndefined(viewsToUpdate, view, create$9).set(key, value); }; /** * Transforms a Prosemirror based absolute position to a Yjs Cursor (relative position in the Yjs model). * * @param {number} pos * @param {Y.XmlFragment} type * @param {ProsemirrorMapping} mapping * @return {any} relative position */ const absolutePositionToRelativePosition = (pos, type, mapping) => { if (pos === 0) { // if the type is later populated, we want to retain the 0 position (hence assoc=-1) return createRelativePositionFromTypeIndex(type, 0, type.length === 0 ? -1 : 0); } /** * @type {any} */ let n = type._first === null ? null : /** @type {Y.ContentType} */ (type._first.content).type; while (n !== null && type !== n) { if (n instanceof YXmlText) { if (n._length >= pos) { return createRelativePositionFromTypeIndex(n, pos, type.length === 0 ? -1 : 0); } else { pos -= n._length; } if (n._item !== null && n._item.next !== null) { n = /** @type {Y.ContentType} */ (n._item.next.content).type; } else { do { n = n._item === null ? null : n._item.parent; pos--; } while (n !== type && n !== null && n._item !== null && n._item.next === null); if (n !== null && n !== type) { // @ts-gnore we know that n.next !== null because of above loop conditition n = n._item === null ? null : /** @type {Y.ContentType} */ (/** @type Y.Item */ (n._item.next).content).type; } } } else { const pNodeSize = /** @type {any} */ (mapping.get(n) || { nodeSize: 0 }).nodeSize; if (n._first !== null && pos < pNodeSize) { n = /** @type {Y.ContentType} */ (n._first.content).type; pos--; } else { if (pos === 1 && n._length === 0 && pNodeSize > 1) { // edge case, should end in this paragraph return new RelativePosition(n._item === null ? null : n._item.id, n._item === null ? findRootTypeKey(n) : null, null); } pos -= pNodeSize; if (n._item !== null && n._item.next !== null) { n = /** @type {Y.ContentType} */ (n._item.next.content).type; } else { if (pos === 0) { // set to end of n.parent n = n._item === null ? n : n._item.parent; return new RelativePosition(n._item === null ? null : n._item.id, n._item === null ? findRootTypeKey(n) : null, null); } do { n = /** @type {Y.Item} */ (n._item).parent; pos--; } while (n !== type && /** @type {Y.Item} */ (n._item).next === null); // if n is null at this point, we have an unexpected case if (n !== type) { // We know that n._item.next is defined because of above loop condition n = /** @type {Y.ContentType} */ (/** @type {Y.Item} */ (/** @type {Y.Item} */ (n._item).next).content).type; } } } } if (n === null) { throw unexpectedCase(); } if (pos === 0 && n.constructor !== YXmlText && n !== type) { // TODO: set to <= 0 return createRelativePosition(n._item.parent, n._item); } } return createRelativePositionFromTypeIndex(type, type._length, type.length === 0 ? -1 : 0); }; const createRelativePosition = (type, item) => { let typeid = null; let tname = null; if (type._item === null) { tname = findRootTypeKey(type); } else { typeid = createID(type._item.id.client, type._item.id.clock); } return new RelativePosition(typeid, tname, item.id); }; /** * @param {Y.Doc} y * @param {Y.XmlFragment} documentType Top level type that is bound to pView * @param {any} relPos Encoded Yjs based relative position * @param {ProsemirrorMapping} mapping * @return {null|number} */ const relativePositionToAbsolutePosition = (y, documentType, relPos, mapping) => { const decodedPos = createAbsolutePositionFromRelativePosition(relPos, y); if (decodedPos === null || (decodedPos.type !== documentType && !isParentOf(documentType, decodedPos.type._item))) { return null; } let type = decodedPos.type; let pos = 0; if (type.constructor === YXmlText) { pos = decodedPos.index; } else if (type._item === null || !type._item.deleted) { let n = type._first; let i = 0; while (i < type._length && i < decodedPos.index && n !== null) { if (!n.deleted) { const t = /** @type {Y.ContentType} */ (n.content).type; i++; if (t instanceof YXmlText) { pos += t._length; } else { pos += /** @type {any} */ (mapping.get(t)).nodeSize; } } n = /** @type {Y.Item} */ (n.right); } pos += 1; // increase because we go out of n } while (type !== documentType && type._item !== null) { // @ts-ignore const parent = type._item.parent; // @ts-ignore if (parent._item === null || !parent._item.deleted) { pos += 1; // the start tag let n = /** @type {Y.AbstractType} */ (parent)._first; // now iterate until we found type while (n !== null) { const contentType = /** @type {Y.ContentType} */ (n.content).type; if (contentType === type) { break; } if (!n.deleted) { if (contentType instanceof YXmlText) { pos += contentType._length; } else { pos += /** @type {any} */ (mapping.get(contentType)).nodeSize; } } n = n.right; } } type = /** @type {Y.AbstractType} */ (parent); } return pos - 1; // we don't count the most outer tag, because it is a fragment }; /** * Default awareness state filter * * @param {number} currentClientId current client id * @param {number} userClientId user client id * @param {any} _user user data * @return {boolean} */ const defaultAwarenessStateFilter = (currentClientId, userClientId, _user) => currentClientId !== userClientId; /** * Default generator for a cursor element * * @param {any} user user data * @return {HTMLElement} */ const defaultCursorBuilder = (user) => { const cursor = document.createElement("span"); cursor.classList.add("ProseMirror-yjs-cursor"); cursor.setAttribute("style", `border-color: ${user.color}`); const userDiv = document.createElement("div"); userDiv.setAttribute("style", `background-color: ${user.color}`); userDiv.insertBefore(document.createTextNode(user.name), null); const nonbreakingSpace1 = document.createTextNode("\u2060"); const nonbreakingSpace2 = document.createTextNode("\u2060"); cursor.insertBefore(nonbreakingSpace1, null); cursor.insertBefore(userDiv, null); cursor.insertBefore(nonbreakingSpace2, null); return cursor; }; /** * Default generator for the selection attributes * * @param {any} user user data * @return {import('prosemirror-view').DecorationAttrs} */ const defaultSelectionBuilder = (user) => { return { style: `background-color: ${user.color}70`, class: "ProseMirror-yjs-selection", }; }; const rxValidColor = /^#[0-9a-fA-F]{6}$/; /** * @param {any} state * @param {Awareness} awareness * @param {function(number, number, any):boolean} awarenessFilter * @param {(user: { name: string, color: string }, clientId: number) => Element} createCursor * @param {(user: { name: string, color: string }, clientId: number) => import('prosemirror-view').DecorationAttrs} createSelection * @return {any} DecorationSet */ const createDecorations = (state, awareness, awarenessFilter, createCursor, createSelection) => { const ystate = ySyncPluginKey.getState(state); const y = ystate.doc; const decorations = []; if (ystate.snapshot != null || ystate.prevSnapshot != null || ystate.binding.mapping.size === 0) { // do not render cursors while snapshot is active return DecorationSet.create(state.doc, []); } awareness.getStates().forEach((aw, clientId) => { if (!awarenessFilter(y.clientID, clientId, aw)) { return; } if (aw.cursor != null) { const user = aw.user || {}; if (user.color == null) { user.color = "#ffa500"; } else if (!rxValidColor.test(user.color)) { // We only support 6-digit RGB colors in y-prosemirror console.warn("A user uses an unsupported color format", user); } if (user.name == null) { user.name = `User: ${clientId}`; } let anchor = relativePositionToAbsolutePosition(y, ystate.type, createRelativePositionFromJSON(aw.cursor.anchor), ystate.binding.mapping); let head = relativePositionToAbsolutePosition(y, ystate.type, createRelativePositionFromJSON(aw.cursor.head), ystate.binding.mapping); if (anchor !== null && head !== null) { const maxsize = max$1(state.doc.content.size - 1, 0); anchor = min$1(anchor, maxsize); head = min$1(head, maxsize); decorations.push( Decoration.widget(head, () => createCursor(user, clientId), { key: clientId + "", side: 10, }), ); const from = min$1(anchor, head); const to = max$1(anchor, head); decorations.push( Decoration.inline(from, to, createSelection(user, clientId), { inclusiveEnd: true, inclusiveStart: false, }), ); } } }); return DecorationSet.create(state.doc, decorations); }; /** * A prosemirror plugin that listens to awareness information on Yjs. * This requires that a `prosemirrorPlugin` is also bound to the prosemirror. * * @public * @param {Awareness} awareness * @param {object} opts * @param {function(any, any, any):boolean} [opts.awarenessStateFilter] * @param {(user: any, clientId: number) => HTMLElement} [opts.cursorBuilder] * @param {(user: any, clientId: number) => import('prosemirror-view').DecorationAttrs} [opts.selectionBuilder] * @param {function(any):any} [opts.getSelection] * @param {string} [cursorStateField] By default all editor bindings use the awareness 'cursor' field to propagate cursor information. * @return {any} */ const yCursorPlugin = ( awareness, { awarenessStateFilter = defaultAwarenessStateFilter, cursorBuilder = defaultCursorBuilder, selectionBuilder = defaultSelectionBuilder, getSelection = (state) => state.selection } = {}, cursorStateField = "cursor", ) => new Plugin({ key: yCursorPluginKey, state: { init(_, state) { return createDecorations(state, awareness, awarenessStateFilter, cursorBuilder, selectionBuilder); }, apply(tr, prevState, _oldState, newState) { const ystate = ySyncPluginKey.getState(newState); const yCursorState = tr.getMeta(yCursorPluginKey); if ((ystate && ystate.isChangeOrigin) || (yCursorState && yCursorState.awarenessUpdated)) { return createDecorations(newState, awareness, awarenessStateFilter, cursorBuilder, selectionBuilder); } return prevState.map(tr.mapping, tr.doc); }, }, props: { decorations: (state) => { return yCursorPluginKey.getState(state); }, }, view: (view) => { const awarenessListener = () => { // @ts-ignore if (view.docView) { setMeta(view, yCursorPluginKey, { awarenessUpdated: true }); } }; const updateCursorInfo = () => { const ystate = ySyncPluginKey.getState(view.state); // @note We make implicit checks when checking for the cursor property const current = awareness.getLocalState() || {}; if (view.hasFocus()) { const selection = getSelection(view.state); /** * @type {Y.RelativePosition} */ const anchor = absolutePositionToRelativePosition(selection.anchor, ystate.type, ystate.binding.mapping); /** * @type {Y.RelativePosition} */ const head = absolutePositionToRelativePosition(selection.head, ystate.type, ystate.binding.mapping); if ( current.cursor == null || !compareRelativePositions(createRelativePositionFromJSON(current.cursor.anchor), anchor) || !compareRelativePositions(createRelativePositionFromJSON(current.cursor.head), head) ) { awareness.setLocalStateField(cursorStateField, { anchor, head, }); } } else if ( current.cursor != null && relativePositionToAbsolutePosition(ystate.doc, ystate.type, createRelativePositionFromJSON(current.cursor.anchor), ystate.binding.mapping) !== null ) { // delete cursor information if current cursor information is owned by this editor binding awareness.setLocalStateField(cursorStateField, null); } }; awareness.on("change", awarenessListener); view.dom.addEventListener("focusin", updateCursorInfo); view.dom.addEventListener("focusout", updateCursorInfo); return { update: updateCursorInfo, destroy: () => { view.dom.removeEventListener("focusin", updateCursorInfo); view.dom.removeEventListener("focusout", updateCursorInfo); awareness.off("change", awarenessListener); awareness.setLocalStateField(cursorStateField, null); }, }; }, }); /** * @typedef {Object} UndoPluginState * @property {import('yjs').UndoManager} undoManager * @property {ReturnType | null} prevSel * @property {boolean} hasUndoOps * @property {boolean} hasRedoOps */ /** * Undo the last user action * * @param {import('prosemirror-state').EditorState} state * @return {boolean} whether a change was undone */ const undo = (state) => yUndoPluginKey.getState(state)?.undoManager?.undo() != null; /** * Redo the last user action * * @param {import('prosemirror-state').EditorState} state * @return {boolean} whether a change was undone */ const redo = (state) => yUndoPluginKey.getState(state)?.undoManager?.redo() != null; /** * Undo the last user action if there are undo operations available * @type {import('prosemirror-state').Command} */ const undoCommand = (state, dispatch) => (dispatch == null ? yUndoPluginKey.getState(state)?.undoManager?.canUndo() : undo(state)); /** * Redo the last user action if there are redo operations available * @type {import('prosemirror-state').Command} */ const redoCommand = (state, dispatch) => (dispatch == null ? yUndoPluginKey.getState(state)?.undoManager?.canRedo() : redo(state)); const defaultProtectedNodes = new Set(["paragraph"]); /** * @param {import('yjs').Item} item * @param {Set} protectedNodes * @returns {boolean} */ const defaultDeleteFilter = (item, protectedNodes) => !(item instanceof Item$1) || !(item.content instanceof ContentType) || !(item.content.type instanceof YText || (item.content.type instanceof YXmlElement && protectedNodes.has(item.content.type.nodeName))) || item.content.type._length === 0; /** * @param {object} [options] * @param {Set} [options.protectedNodes] * @param {any[]} [options.trackedOrigins] * @param {import('yjs').UndoManager | null} [options.undoManager] */ const yUndoPlugin = ({ protectedNodes = defaultProtectedNodes, trackedOrigins = [], undoManager = null } = {}) => new Plugin({ key: yUndoPluginKey, state: { init: (initargs, state) => { // TODO: check if plugin order matches and fix const ystate = ySyncPluginKey.getState(state); const _undoManager = undoManager || new UndoManager(ystate.type, { trackedOrigins: new Set([ySyncPluginKey].concat(trackedOrigins)), deleteFilter: (item) => defaultDeleteFilter(item, protectedNodes), captureTransaction: (tr) => tr.meta.get("addToHistory") !== false, }); return { undoManager: _undoManager, prevSel: null, hasUndoOps: _undoManager.undoStack.length > 0, hasRedoOps: _undoManager.redoStack.length > 0, }; }, apply: (tr, val, oldState, state) => { const binding = ySyncPluginKey.getState(state).binding; const undoManager = val.undoManager; const hasUndoOps = undoManager.undoStack.length > 0; const hasRedoOps = undoManager.redoStack.length > 0; if (binding) { return { undoManager, prevSel: getRelativeSelection(binding, oldState), hasUndoOps, hasRedoOps, }; } else { if (hasUndoOps !== val.hasUndoOps || hasRedoOps !== val.hasRedoOps) { return Object.assign({}, val, { hasUndoOps: undoManager.undoStack.length > 0, hasRedoOps: undoManager.redoStack.length > 0, }); } else { // nothing changed return val; } } }, }, view: (view) => { const ystate = ySyncPluginKey.getState(view.state); const undoManager = yUndoPluginKey.getState(view.state).undoManager; undoManager.on("stack-item-added", ({ stackItem }) => { const binding = ystate.binding; if (binding) { stackItem.meta.set(binding, yUndoPluginKey.getState(view.state).prevSel); } }); undoManager.on("stack-item-popped", ({ stackItem }) => { const binding = ystate.binding; if (binding) { binding.beforeTransactionSelection = stackItem.meta.get(binding) || binding.beforeTransactionSelection; } }); return { destroy: () => { undoManager.destroy(); }, }; }, }); const __storeToDerived = /* @__PURE__ */ new WeakMap(); const __derivedToStore = /* @__PURE__ */ new WeakMap(); const __depsThatHaveWrittenThisTick = { current: [], }; let __isFlushing = false; const __pendingUpdates = /* @__PURE__ */ new Set(); const __initialBatchValues = /* @__PURE__ */ new Map(); function __flush_internals(relatedVals) { const sorted = Array.from(relatedVals).sort((a, b) => { if (a instanceof Derived && a.options.deps.includes(b)) return 1; if (b instanceof Derived && b.options.deps.includes(a)) return -1; return 0; }); for (const derived of sorted) { if (__depsThatHaveWrittenThisTick.current.includes(derived)) { continue; } __depsThatHaveWrittenThisTick.current.push(derived); derived.recompute(); const stores = __derivedToStore.get(derived); if (stores) { for (const store of stores) { const relatedLinkedDerivedVals = __storeToDerived.get(store); if (!relatedLinkedDerivedVals) continue; __flush_internals(relatedLinkedDerivedVals); } } } } function __notifyListeners(store) { const value = { prevVal: store.prevState, currentVal: store.state, }; for (const listener of store.listeners) { listener(value); } } function __notifyDerivedListeners(derived) { const value = { prevVal: derived.prevState, currentVal: derived.state, }; for (const listener of derived.listeners) { listener(value); } } function __flush(store) { __pendingUpdates.add(store); if (__isFlushing) return; try { __isFlushing = true; while (__pendingUpdates.size > 0) { const stores = Array.from(__pendingUpdates); __pendingUpdates.clear(); for (const store2 of stores) { const prevState = __initialBatchValues.get(store2) ?? store2.prevState; store2.prevState = prevState; __notifyListeners(store2); } for (const store2 of stores) { const derivedVals = __storeToDerived.get(store2); if (!derivedVals) continue; __depsThatHaveWrittenThisTick.current.push(store2); __flush_internals(derivedVals); } for (const store2 of stores) { const derivedVals = __storeToDerived.get(store2); if (!derivedVals) continue; for (const derived of derivedVals) { __notifyDerivedListeners(derived); } } } } finally { __isFlushing = false; __depsThatHaveWrittenThisTick.current = []; __initialBatchValues.clear(); } } function isUpdaterFunction(updater) { return typeof updater === "function"; } class Store { constructor(initialState, options) { this.listeners = /* @__PURE__ */ new Set(); this.subscribe = (listener) => { var _a, _b; this.listeners.add(listener); const unsub = (_b = (_a = this.options) == null ? void 0 : _a.onSubscribe) == null ? void 0 : _b.call(_a, listener, this); return () => { this.listeners.delete(listener); unsub == null ? void 0 : unsub(); }; }; this.prevState = initialState; this.state = initialState; this.options = options; } setState(updater) { var _a, _b, _c; this.prevState = this.state; if ((_a = this.options) == null ? void 0 : _a.updateFn) { this.state = this.options.updateFn(this.prevState)(updater); } else { if (isUpdaterFunction(updater)) { this.state = updater(this.prevState); } else { this.state = updater; } } (_c = (_b = this.options) == null ? void 0 : _b.onUpdate) == null ? void 0 : _c.call(_b); __flush(this); } } class Derived { constructor(options) { this.listeners = /* @__PURE__ */ new Set(); this._subscriptions = []; this.lastSeenDepValues = []; this.getDepVals = () => { const l = this.options.deps.length; const prevDepVals = new Array(l); const currDepVals = new Array(l); for (let i = 0; i < l; i++) { const dep = this.options.deps[i]; prevDepVals[i] = dep.prevState; currDepVals[i] = dep.state; } this.lastSeenDepValues = currDepVals; return { prevDepVals, currDepVals, prevVal: this.prevState ?? void 0, }; }; this.recompute = () => { var _a, _b; this.prevState = this.state; const depVals = this.getDepVals(); this.state = this.options.fn(depVals); (_b = (_a = this.options).onUpdate) == null ? void 0 : _b.call(_a); }; this.checkIfRecalculationNeededDeeply = () => { for (const dep of this.options.deps) { if (dep instanceof Derived) { dep.checkIfRecalculationNeededDeeply(); } } let shouldRecompute = false; const lastSeenDepValues = this.lastSeenDepValues; const { currDepVals } = this.getDepVals(); for (let i = 0; i < currDepVals.length; i++) { if (currDepVals[i] !== lastSeenDepValues[i]) { shouldRecompute = true; break; } } if (shouldRecompute) { this.recompute(); } }; this.mount = () => { this.registerOnGraph(); this.checkIfRecalculationNeededDeeply(); return () => { this.unregisterFromGraph(); for (const cleanup of this._subscriptions) { cleanup(); } }; }; this.subscribe = (listener) => { var _a, _b; this.listeners.add(listener); const unsub = (_b = (_a = this.options).onSubscribe) == null ? void 0 : _b.call(_a, listener, this); return () => { this.listeners.delete(listener); unsub == null ? void 0 : unsub(); }; }; this.options = options; this.state = options.fn({ prevDepVals: void 0, prevVal: void 0, currDepVals: this.getDepVals().currDepVals, }); } registerOnGraph(deps = this.options.deps) { for (const dep of deps) { if (dep instanceof Derived) { dep.registerOnGraph(); this.registerOnGraph(dep.options.deps); } else if (dep instanceof Store) { let relatedLinkedDerivedVals = __storeToDerived.get(dep); if (!relatedLinkedDerivedVals) { relatedLinkedDerivedVals = /* @__PURE__ */ new Set(); __storeToDerived.set(dep, relatedLinkedDerivedVals); } relatedLinkedDerivedVals.add(this); let relatedStores = __derivedToStore.get(this); if (!relatedStores) { relatedStores = /* @__PURE__ */ new Set(); __derivedToStore.set(this, relatedStores); } relatedStores.add(dep); } } } unregisterFromGraph(deps = this.options.deps) { for (const dep of deps) { if (dep instanceof Derived) { this.unregisterFromGraph(dep.options.deps); } else if (dep instanceof Store) { const relatedLinkedDerivedVals = __storeToDerived.get(dep); if (relatedLinkedDerivedVals) { relatedLinkedDerivedVals.delete(this); } const relatedStores = __derivedToStore.get(this); if (relatedStores) { relatedStores.delete(dep); } } } } } const r$1 = Symbol("originalFactory"); function a$1(n) { if (typeof n == "object" && "key" in n) return function t() { return ((n[r$1] = t), n); }; if (typeof n != "function") throw new Error("factory must be a function"); return function t(e) { return (i) => { const o = n({ editor: i.editor, options: e }); return ((o[r$1] = t), o); }; }; } function f$2(n, t) { return new Store(n, t); } //#region src/cache.ts /** * Represents a cache of doc positions to the node and decorations at that position */ var DecorationCache = class DecorationCache { constructor(cache) { this.cache = new Map(cache); } /** * Gets the cache entry at the given doc position, or null if it doesn't exist * @param pos The doc position of the node you want the cache for */ get(pos) { return this.cache.get(pos); } /** * Sets the cache entry at the given position with the give node/decoration * values * @param pos The doc position of the node to set the cache for * @param node The node to place in cache * @param decorations The decorations to place in cache */ set(pos, node, decorations) { if (pos < 0) return; this.cache.set(pos, [node, decorations]); } /** * Removes the value at the oldPos (if it exists) and sets the new position to * the given values * @param oldPos The old node position to overwrite * @param newPos The new node position to set the cache for * @param node The new node to place in cache * @param decorations The new decorations to place in cache */ replace(oldPos, newPos, node, decorations) { this.remove(oldPos); this.set(newPos, node, decorations); } /** * Removes the cache entry at the given position * @param pos The doc position to remove from cache */ remove(pos) { this.cache.delete(pos); } /** * Invalidates the cache by removing all decoration entries on nodes that have * changed, updating the positions of the nodes that haven't and removing all * the entries that have been deleted; NOTE: this does not affect the current * cache, but returns an entirely new one * @param tr A transaction to map the current cache to */ invalidate(tr) { const returnCache = new DecorationCache(this.cache); const mapping = tr.mapping; this.cache.forEach(([node, decorations], pos) => { if (pos < 0) return; const result = mapping.mapResult(pos); const mappedNode = tr.doc.nodeAt(result.pos); if (result.deleted || !mappedNode?.eq(node)) returnCache.remove(pos); else if (pos !== result.pos) { const updatedDecorations = decorations .map((d) => { return d.map(mapping, 0, 0); }) .filter((d) => d != null); returnCache.replace(pos, result.pos, mappedNode, updatedDecorations); } }); return returnCache; } }; //#endregion //#region src/plugin.ts /** * Creates a plugin that highlights the contents of all nodes (via Decorations) * with a type passed in blockTypes */ function createHighlightPlugin({ parser, nodeTypes = ["code_block", "codeBlock"], languageExtractor = (node) => node.attrs.language }) { const key = new PluginKey("prosemirror-highlight"); return new Plugin({ key, state: { init(_, instance) { const cache = new DecorationCache(); const [decorations, promises] = calculateDecoration(instance.doc, parser, nodeTypes, languageExtractor, cache); return { cache, decorations, promises, }; }, apply: (tr, data) => { const cache = data.cache.invalidate(tr); const refresh = !!tr.getMeta("prosemirror-highlight-refresh"); if (!tr.docChanged && !refresh) return { cache, decorations: data.decorations.map(tr.mapping, tr.doc), promises: data.promises, }; const [decorations, promises] = calculateDecoration(tr.doc, parser, nodeTypes, languageExtractor, cache); return { cache, decorations, promises, }; }, }, view: (view) => { const promises = /* @__PURE__ */ new Set(); const refresh = () => { if (promises.size > 0) return; const tr = view.state.tr.setMeta("prosemirror-highlight-refresh", true); view.dispatch(tr); }; const check = () => { const state = key.getState(view.state); for (const promise of state?.promises ?? []) { promises.add(promise); promise .then(() => { promises.delete(promise); refresh(); }) .catch(() => { promises.delete(promise); }); } }; check(); return { update: () => { check(); }, }; }, props: { decorations(state) { return this.getState(state)?.decorations; }, }, }); } function calculateDecoration(doc, parser, nodeTypes, languageExtractor, cache) { const result = []; const promises = []; doc.descendants((node, pos) => { if (!node.type.isTextblock) return true; if (nodeTypes.includes(node.type.name)) { const language = languageExtractor(node); const cached = cache.get(pos); if (cached) { const [_, decorations] = cached; result.push(...decorations); } else { const decorations = parser({ content: node.textContent, language: language || void 0, pos, size: node.nodeSize, }); if (decorations && Array.isArray(decorations)) { cache.set(pos, node, decorations); result.push(...decorations); } else if (decorations instanceof Promise) { cache.remove(pos); promises.push(decorations); } } } return false; }); return [DecorationSet.create(doc, result), promises]; } //#region src/shiki.ts function createParser(highlighter, options) { return function parser({ content, language, pos, size }) { const decorations = []; const { tokens, fg, bg, rootStyle } = highlighter.codeToTokens(content, { lang: language, ...{ theme: highlighter.getLoadedThemes()[0] }, }); const style = rootStyle || (fg && bg ? `--prosemirror-highlight:${fg};--prosemirror-highlight-bg:${bg}` : ""); if (style) { const decoration = Decoration.node(pos, pos + size, { style }); decorations.push(decoration); } let from = pos + 1; for (const line of tokens) { for (const token of line) { const to = from + token.content.length; const decoration = Decoration.inline(from, to, { style: stringifyTokenStyle(token.htmlStyle ?? `color: ${token.color}`), class: "shiki", }); decorations.push(decoration); from = to; } from += 1; } return decorations; }; } /** * Copied from https://github.com/shikijs/shiki/blob/f76a371dbc2752cba341023df00ebfe9b66cb3f6/packages/core/src/utils.ts#L213 * * Copy instead of import it from `shiki` to avoid importing the `shiki` package in this file. */ function stringifyTokenStyle(token) { if (typeof token === "string") return token; return Object.entries(token) .map(([key, value]) => `${key}:${value}`) .join(";"); } var Ve$1 = Object.defineProperty; var We = (e, t, n) => (t in e ? Ve$1(e, t, { enumerable: true, configurable: true, writable: true, value: n }) : (e[t] = n)); var w = (e, t, n) => We(e, typeof t != "symbol" ? t + "" : t, n); const bt = () => typeof navigator < "u" && (/Mac/.test(navigator.platform) || (/AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent))); function L$1(e, t = "Ctrl") { return bt() ? e.replace("Mod", "⌘") : e.replace("Mod", t); } function D$2(...e) { return [ // Converts to & from set to remove duplicates. ...new Set( e .filter((t) => t) .join(" ") .split(" "), ), ].join(" "); } function Ct(e, t, n, o) { const r = document.createElement("div"); ((r.className = D$2("bn-block-content", n.class)), r.setAttribute("data-content-type", e)); for (const [s, i] of Object.entries(n)) s !== "class" && r.setAttribute(s, i); const a = document.createElement(t); a.className = D$2("bn-inline-content", o.class); for (const [s, i] of Object.entries(o)) s !== "class" && a.setAttribute(s, i); return ( r.appendChild(a), { dom: r, contentDOM: a, } ); } const de$2 = (e, t) => { let n = bt$1(e, t.pmSchema); n.type.name === "blockContainer" && (n = n.firstChild); const o = t.pmSchema.nodes[n.type.name].spec.toDOM; if (o === void 0) throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`."); const r = o(n); if (typeof r != "object" || !("dom" in r)) throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap node's `renderHTML` function does not return an object with the `dom` property."); return r; }; function we$1(e, t = "
") { const n = e.querySelectorAll("p"); if (n.length > 1) { const o = n[0]; for (let r = 1; r < n.length; r++) { const a = n[r]; ((o.innerHTML += t + a.innerHTML), a.remove()); } } } function F$1(e) { return "data-" + e.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); } function fo$1(e) { const t = e.split("/"); return ( !t.length || // invalid? t[t.length - 1] === "" ) ? e : t[t.length - 1]; } function mo$1(e) { var n; const t = ["mp4", "webm", "ogg", "mov", "mkv", "flv", "avi", "wmv", "m4v"]; try { const r = ((n = new URL(e).pathname.split(".").pop()) == null ? void 0 : n.toLowerCase()) || ""; return t.includes(r); } catch { return false; } } function kt(e) { const t = {}; return ( Object.entries(e).forEach(([n, o]) => { t[n] = { default: o.default, keepOnSplit: true, // Props are displayed in kebab-case as HTML attributes. If a prop's // value is the same as its default, we don't display an HTML // attribute for it. parseHTML: (r) => { const a = r.getAttribute(F$1(n)); if (a === null) return null; if ((o.default === void 0 && o.type === "boolean") || (o.default !== void 0 && typeof o.default == "boolean")) return ( a === "true" ? true : a === "false" ? false : null ); if ((o.default === void 0 && o.type === "number") || (o.default !== void 0 && typeof o.default == "number")) { const s = parseFloat(a); return !Number.isNaN(s) && Number.isFinite(s) ? s : null; } return a; }, renderHTML: (r) => r[n] !== o.default ? { [F$1(n)]: r[n], } : {}, }; }), t ); } function yt(e, t, n, o) { const r = e(); if (r === void 0) throw new Error("Cannot find node position"); const s = n.state.doc.resolve(r).node().attrs.id; if (!s) throw new Error("Block doesn't have id"); const i = t.getBlock(s); if (i.type !== o) throw new Error("Block type does not match"); return i; } function z(e, t, n, o, r = false, a) { const s = document.createElement("div"); if (a !== void 0) for (const [i, c] of Object.entries(a)) i !== "class" && s.setAttribute(i, c); ((s.className = D$2("bn-block-content", (a == null ? void 0 : a.class) || "")), s.setAttribute("data-content-type", t)); for (const [i, c] of Object.entries(n)) { const u = o[i].default; c !== u && s.setAttribute(F$1(i), c); } return ( r && s.setAttribute("data-file-block", ""), s.appendChild(e.dom), e.contentDOM && (e.contentDOM.className = D$2("bn-inline-content", e.contentDOM.className)), { ...e, dom: s, } ); } function vt(e, t, n) { return { config: { type: e.type, content: e.content, propSchema: t, }, implementation: { node: e.node, render: de$2, toExternalHTML: de$2, }, extensions: n, }; } function Et(e, t) { e.stopEvent = (n) => ( n.type === "mousedown" && setTimeout(() => { t.view.dom.blur(); }, 10), true ); } function St(e, t) { const n = [ { tag: "[data-content-type=" + e.type + "]", contentElement: ".bn-inline-content", }, ]; return ( t.parse && n.push({ tag: "*", getAttrs(o) { var a; if (typeof o == "string") return false; const r = (a = t.parse) == null ? void 0 : a.call(t, o); return r === void 0 ? false : r; }, // Because we do the parsing ourselves, we want to preserve whitespace for content we've parsed preserveWhitespace: true, getContent: e.content === "inline" || e.content === "none" ? (o, r) => { var a; if (t.parseContent) { const s = t.parseContent({ el: o, schema: r, }); if (s !== void 0) return s; } if (e.content === "inline") { const i = o.cloneNode(true); return ( we$1( i, (a = t.meta) != null && a.code ? ` ` : "
", ), DOMParser$1.fromSchema(r).parse(i, { topNode: r.nodes.paragraph.create(), preserveWhitespace: true, }).content ); } return Fragment.empty; } : void 0, }), n ); } function ho$1(e, t, n, o) { var a, s, i, c; const r = t.node || Node3.create({ name: e.type, content: e.content === "inline" ? "inline*" : e.content === "none" ? "" : e.content, group: "blockContent", selectable: ((a = t.meta) == null ? void 0 : a.selectable) ?? true, isolating: ((s = t.meta) == null ? void 0 : s.isolating) ?? true, code: ((i = t.meta) == null ? void 0 : i.code) ?? false, defining: ((c = t.meta) == null ? void 0 : c.defining) ?? true, priority: o, addAttributes() { return kt(e.propSchema); }, parseHTML() { return St(e, t); }, renderHTML({ HTMLAttributes: l }) { var d; const u = document.createElement("div"); return z( { dom: u, contentDOM: e.content === "inline" ? u : void 0, }, e.type, {}, e.propSchema, ((d = t.meta) == null ? void 0 : d.fileBlockAccept) !== void 0, l, ); }, addNodeView() { return (l) => { var f, E; const u = this.options.editor, d = yt(l.getPos, u, this.editor, e.type), p = ((f = this.options.domAttributes) == null ? void 0 : f.blockContent) || {}, h = t.render.call({ blockContentDOMAttributes: p, props: l, renderType: "nodeView" }, d, u); return (((E = t.meta) == null ? void 0 : E.selectable) === false && Et(h, this.editor), h); }; }, }); if (r.name !== e.type) throw new Error("Node name does not match block type. This is a bug in BlockNote."); return { config: e, implementation: { ...t, node: r, render(l, u) { var p; const d = ((p = r.options.domAttributes) == null ? void 0 : p.blockContent) || {}; return t.render.call( { blockContentDOMAttributes: d, props: void 0, renderType: "dom", }, l, u, ); }, // TODO: this should not have wrapInBlockStructure and generally be a lot simpler // post-processing in externalHTMLExporter should not be necessary toExternalHTML: (l, u, d) => { var h, f; const p = ((h = r.options.domAttributes) == null ? void 0 : h.blockContent) || {}; return ( ((f = t.toExternalHTML) == null ? void 0 : f.call({ blockContentDOMAttributes: p }, l, u, d)) ?? t.render.call({ blockContentDOMAttributes: p, renderType: "dom", props: void 0 }, l, u) ); }, }, extensions: n, }; } function v$1(e, t, n) { return (o = {}) => { const r = typeof e == "function" ? e(o) : e, a = typeof t == "function" ? t(o) : t, s = n ? typeof n == "function" ? n(o) : n : void 0; return { config: r, implementation: { ...a, // TODO: this should not have wrapInBlockStructure and generally be a lot simpler // post-processing in externalHTMLExporter should not be necessary toExternalHTML(i, c, l) { var d, p; const u = (d = a.toExternalHTML) == null ? void 0 : d.call({ blockContentDOMAttributes: this.blockContentDOMAttributes }, i, c, l); if (u !== void 0) return z(u, i.type, i.props, r.propSchema, ((p = a.meta) == null ? void 0 : p.fileBlockAccept) !== void 0); }, render(i, c) { var d; const l = a.render.call( { blockContentDOMAttributes: this.blockContentDOMAttributes, renderType: this.renderType, props: this.props, }, i, c, ); return z(l, i.type, i.props, r.propSchema, ((d = a.meta) == null ? void 0 : d.fileBlockAccept) !== void 0, this.blockContentDOMAttributes); }, }, extensions: s, }; }; } function Mt$1(e) { return Object.fromEntries(Object.entries(e).map(([t, n]) => [t, n.config])); } function wt(e) { return e === "boolean" ? {} : { stringValue: { default: void 0, keepOnSplit: true, parseHTML: (t) => t.getAttribute("data-value"), renderHTML: (t) => t.stringValue !== void 0 ? { "data-value": t.stringValue, } : {}, }, }; } function W$1(e, t, n, o) { return (e.dom.setAttribute("data-style-type", t), o === "string" && e.dom.setAttribute("data-value", n), e.contentDOM && e.contentDOM.setAttribute("data-editable", ""), e); } function Le$2(e, t) { return { config: e, implementation: t, }; } function I$1(e, t) { return Le$2( { type: e.name, propSchema: t, }, { mark: e, render(n, o) { const r = o.pmSchema.marks[e.name].spec.toDOM; if (r === void 0) throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`."); const a = o.pmSchema.mark(e.name, { stringValue: n, }), s = DOMSerializer.renderSpec(document, r(a, true)); if (typeof s != "object" || !("dom" in s)) throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property."); return s; }, toExternalHTML(n, o) { const r = o.pmSchema.marks[e.name].spec.toDOM; if (r === void 0) throw new Error("This block has no default HTML serialization as its corresponding TipTap node doesn't implement `renderHTML`."); const a = o.pmSchema.mark(e.name, { stringValue: n, }), s = DOMSerializer.renderSpec(document, r(a, true)); if (typeof s != "object" || !("dom" in s)) throw new Error("Cannot use this block's default HTML serialization as its corresponding TipTap mark's `renderHTML` function does not return an object with the `dom` property."); return s; }, }, ); } function Lt$1(e) { return Object.fromEntries(Object.entries(e).map(([t, n]) => [t, n.config])); } function Tt$1(e, t) { const n = [ { tag: `[data-style-type="${e.type}"]`, contentElement: (o) => { const r = o; return r.matches("[data-editable]") ? r : r.querySelector("[data-editable]") || r; }, }, ]; return ( t && n.push({ tag: "*", // By default, styles can overlap each other, so the rules should not // completely consume the element they parse (which can have multiple // styles). consuming: false, getAttrs(o) { if (typeof o == "string") return false; const r = t == null ? void 0 : t(o); return r === void 0 ? false : { stringValue: r }; }, }), n ); } function Te$2(e, t) { const n = Mark.create({ name: e.type, addAttributes() { return wt(e.propSchema); }, parseHTML() { return Tt$1(e, t.parse); }, renderHTML({ mark: o }) { const r = (t.toExternalHTML || t.render)(o.attrs.stringValue); return W$1(r, e.type, o.attrs.stringValue, e.propSchema); }, addMarkView() { return ({ mark: o }) => { const r = t.render(o.attrs.stringValue); return W$1(r, e.type, o.attrs.stringValue, e.propSchema); }; }, }); return Le$2(e, { ...t, mark: n, render: (o) => { const r = t.render(o); return W$1(r, e.type, o, e.propSchema); }, toExternalHTML: (o) => { const r = (t.toExternalHTML || t.render)(o); return W$1(r, e.type, o, e.propSchema); }, }); } function Bt$1(e, t) { let n, o; if ( (t.firstChild.descendants((r, a) => n ? false : !At$1(r) || r.attrs.id !== e ? true : ((n = r), (o = a + 1), false), ), !(n === void 0 || o === void 0)) ) return { node: n, posBeforeNode: o, }; } function At$1(e) { return e.type.isInGroup("bnBlock"); } const yo$1 = (e, t) => ({ tr: n, dispatch: o }) => (o && Q$1(n, e, t), true); function Q$1(e, t, n, o, r) { const a = It$1(e.doc.resolve(t)); let s = null; a.blockNoteType === "table" && (s = Pt$1(e)); const i = ht(e); const c = i.nodes[a.blockNoteType], l = i.nodes[n.type || a.blockNoteType], u = l.isInGroup("bnBlock") ? l : i.nodes.blockContainer; if (a.isBlockContainer && l.isInGroup("blockContent")) { (ue$2(n, e, a), Nt$1(n, e, c, l, a)); } else if (!a.isBlockContainer && l.isInGroup("bnBlock")) ue$2(n, e, a); else { const d = L$2(a.bnBlock.node, i); e.replaceWith( a.bnBlock.beforePos, a.bnBlock.afterPos, bt$1( { children: d.children, // if no children are passed in, use existing children ...n, }, i, ), ); return; } (e.setNodeMarkup(a.bnBlock.beforePos, u, { ...a.bnBlock.node.attrs, ...n.props, }), s && It(e, a, s)); } function Nt$1(e, t, n, o, r, a, s) { const i = ht(t); let c = "keep"; if (e.content) if (typeof e.content == "string") c = T$1([e.content], i, o.name); else if (Array.isArray(e.content)) c = T$1(e.content, i, o.name); else if (e.content.type === "tableContent") c = kt$1(e.content, i); else throw new O(e.content.type); else n.spec.content === "" || (o.spec.content !== n.spec.content && (c = [])); if (c === "keep") t.setNodeMarkup(r.blockContent.beforePos, o, { ...r.blockContent.node.attrs, ...e.props, }); else t.replaceWith( r.blockContent.beforePos, r.blockContent.afterPos, o.createChecked( { ...r.blockContent.node.attrs, ...e.props, }, c, ), ); } function ue$2(e, t, n) { const o = ht(t); if (e.children !== void 0 && e.children.length > 0) { const r = e.children.map((a) => bt$1(a, o)); if (n.childContainer) t.step(new ReplaceStep(n.childContainer.beforePos + 1, n.childContainer.afterPos - 1, new Slice(Fragment.from(r), 0, 0))); else { if (!n.isBlockContainer) throw new Error("impossible"); t.insert(n.blockContent.afterPos, o.nodes.blockGroup.createChecked({}, r)); } } } function vo$1(e, t, n, o, r) { const a = typeof t == "string" ? t : t.id, s = Bt$1(a, e.doc); if (!s) throw new Error(`Block with ID ${a} not found`); Q$1(e, s.posBeforeNode, n); const i = e.doc.resolve(s.posBeforeNode + 1).node(), c = ht(e); return L$2(i, c); } function Pt$1(e) { const t = "selection" in e ? e.selection : null; if (!(t instanceof TextSelection)) return null; const n = e.doc.resolve(t.head); let o = -1, r = -1; for (let x = n.depth; x >= 0; x--) { const S = n.node(x).type.name; if ((o < 0 && (S === "tableCell" || S === "tableHeader") && (o = x), S === "table")) { r = x; break; } } if (o < 0 || r < 0) return null; const a = n.before(o), s = n.before(r), i = e.doc.nodeAt(s); if (!i || i.type.name !== "table") return null; const c = TableMap.get(i), l = a - (s + 1), u = c.map.indexOf(l); if (u < 0) return null; const d = Math.floor(u / c.width), p = u % c.width, f = a + 1 + 1, E = Math.max(0, t.head - f); return { row: d, col: p, offset: E }; } function It(e, t, n) { var x; if (t.blockNoteType !== "table") return false; let o = -1; if (t.isBlockContainer) o = e.mapping.map(t.blockContent.beforePos); else { const S = e.mapping.map(t.bnBlock.beforePos), N = S + (((x = e.doc.nodeAt(S)) == null ? void 0 : x.nodeSize) || 0); e.doc.nodesBetween(S, N, (g, M) => (g.type.name === "table" ? ((o = M), false) : true)); } const r = o >= 0 ? e.doc.nodeAt(o) : null; if (!r || r.type.name !== "table") return false; const a = TableMap.get(r), s = Math.max(0, Math.min(n.row, a.height - 1)), i = Math.max(0, Math.min(n.col, a.width - 1)), c = s * a.width + i, l = a.map[c]; if (l == null) return false; const d = o + 1 + l + 1, p = e.doc.nodeAt(d), h = d + 1, f = p ? p.content.size : 0, E = h + Math.max(0, Math.min(n.offset, f)); return ("selection" in e && e.setSelection(TextSelection.create(e.doc, E)), true); } const T = { gray: { text: "#9b9a97", background: "#ebeced", }, brown: { text: "#64473a", background: "#e9e5e3", }, red: { text: "#e03e3e", background: "#fbe4e4", }, orange: { text: "#d9730d", background: "#f6e9d9", }, yellow: { text: "#dfab01", background: "#fbf3db", }, green: { text: "#4d6461", background: "#ddedea", }, blue: { text: "#0b6e99", background: "#ddebf1", }, purple: { text: "#6940a5", background: "#eae4f2", }, pink: { text: "#ad1a72", background: "#f4dfeb", }, }, m$1 = { backgroundColor: { default: "default", }, textColor: { default: "default", }, textAlignment: { default: "left", values: ["left", "center", "right", "justify"], }, }, C = (e) => { const t = {}; return ( e.hasAttribute("data-background-color") ? (t.backgroundColor = e.getAttribute("data-background-color")) : e.style.backgroundColor && (t.backgroundColor = e.style.backgroundColor), e.hasAttribute("data-text-color") ? (t.textColor = e.getAttribute("data-text-color")) : e.style.color && (t.textColor = e.style.color), (t.textAlignment = m$1.textAlignment.values.includes(e.style.textAlign) ? e.style.textAlign : void 0), t ); }, A$2 = (e, t) => { (e.backgroundColor && e.backgroundColor !== m$1.backgroundColor.default && (t.style.backgroundColor = e.backgroundColor in T ? T[e.backgroundColor].background : e.backgroundColor), e.textColor && e.textColor !== m$1.textColor.default && (t.style.color = e.textColor in T ? T[e.textColor].text : e.textColor), e.textAlignment && e.textAlignment !== m$1.textAlignment.default && (t.style.textAlign = e.textAlignment)); }, So$1 = (e = "backgroundColor") => ({ default: m$1.backgroundColor.default, parseHTML: (t) => t.hasAttribute("data-background-color") ? t.getAttribute("data-background-color") : t.style.backgroundColor ? t.style.backgroundColor : m$1.backgroundColor.default, renderHTML: (t) => t[e] === m$1.backgroundColor.default ? {} : { "data-background-color": t[e], }, }), xo$1 = (e = "textColor") => ({ default: m$1.textColor.default, parseHTML: (t) => t.hasAttribute("data-text-color") ? t.getAttribute("data-text-color") : t.style.color ? t.style.color : m$1.textColor.default, renderHTML: (t) => t[e] === m$1.textColor.default ? {} : { "data-text-color": t[e], }, }), $$2 = (e, t) => { const n = e.querySelector(t); if (!n) return; const o = e.querySelector("figcaption"), r = (o == null ? void 0 : o.textContent) ?? void 0; return { targetElement: n, caption: r }; }, H$1 = a$1(({ editor: e }) => { const t = f$2(void 0); function n() { t.setState(void 0); } return { key: "filePanel", store: t, mount({ signal: o }) { const r = e.onChange( n, // Don't trigger if the changes are caused by a remote user. false, ), a = e.onSelectionChange( n, // Don't trigger if the changes are caused by a remote user. false, ); o.addEventListener("abort", () => { (r(), a()); }); }, closeMenu: n, showMenu(o) { t.setState(o); }, }; }), Ht$1 = (e, t, n) => { const o = document.createElement("div"); o.className = "bn-add-file-button"; const r = document.createElement("div"); ((r.className = "bn-add-file-button-icon"), n ? r.appendChild(n) : (r.innerHTML = ''), o.appendChild(r)); const a = document.createElement("p"); ((a.className = "bn-add-file-button-text"), (a.innerHTML = e.type in t.dictionary.file_blocks.add_button_text ? t.dictionary.file_blocks.add_button_text[e.type] : t.dictionary.file_blocks.add_button_text.file), o.appendChild(a)); const s = (c) => { (c.preventDefault(), c.stopPropagation()); }, i = () => { var c; t.isEditable && ((c = t.getExtension(H$1)) == null || c.showMenu(e.id)); }; return ( o.addEventListener("mousedown", s, true), o.addEventListener("click", i, true), { dom: o, destroy: () => { (o.removeEventListener("mousedown", s, true), o.removeEventListener("click", i, true)); }, } ); }, Dt$1 = '', Ot$1 = (e) => { const t = document.createElement("div"); t.className = "bn-file-name-with-icon"; const n = document.createElement("div"); ((n.className = "bn-file-icon"), (n.innerHTML = Dt$1), t.appendChild(n)); const o = document.createElement("p"); return ( (o.className = "bn-file-name"), (o.textContent = e.props.name), t.appendChild(o), { dom: t, } ); }, J = (e, t, n, o) => { const r = document.createElement("div"); if (((r.className = "bn-file-block-content-wrapper"), e.props.url === "")) { const s = Ht$1(e, t, o); r.appendChild(s.dom); const i = t.onUploadStart((c) => { if (c === e.id) { r.removeChild(s.dom); const l = document.createElement("div"); ((l.className = "bn-file-loading-preview"), (l.textContent = "Loading..."), r.appendChild(l)); } }); return { dom: r, destroy: () => { (i(), s.destroy()); }, }; } const a = { dom: r }; if (e.props.showPreview === false || !n) { const s = Ot$1(e); (r.appendChild(s.dom), (a.destroy = () => { var i; (i = s.destroy) == null || i.call(s); })); } else r.appendChild(n.dom); if (e.props.caption) { const s = document.createElement("p"); ((s.className = "bn-file-caption"), (s.textContent = e.props.caption), r.appendChild(s)); } return a; }, Y = (e, t) => { const n = document.createElement("figure"), o = document.createElement("figcaption"); return ((o.textContent = t), n.appendChild(e), n.appendChild(o), { dom: n }); }, U = (e, t) => { const n = document.createElement("div"), o = document.createElement("p"); return ( (o.textContent = t), n.appendChild(e), n.appendChild(o), { dom: n, } ); }, pe$1 = (e) => ({ url: e.src || void 0 }), _t$1 = '', Vt$1 = (e) => ({ type: "audio", propSchema: { backgroundColor: m$1.backgroundColor, // File name. name: { default: "", }, // File url. url: { default: "", }, // File caption. caption: { default: "", }, showPreview: { default: true, }, }, content: "none", }), Wt$1 = (e = {}) => (t) => { if (t.tagName === "AUDIO") { if (t.closest("figure")) return; const { backgroundColor: n } = C(t); return { ...pe$1(t), backgroundColor: n, }; } if (t.tagName === "FIGURE") { const n = $$2(t, "audio"); if (!n) return; const { targetElement: o, caption: r } = n, { backgroundColor: a } = C(t); return { ...pe$1(o), backgroundColor: a, caption: r, }; } }, Rt$1 = (e = {}) => (t, n) => { const o = document.createElement("div"); o.innerHTML = e.icon ?? _t$1; const r = document.createElement("audio"); return ( (r.className = "bn-audio"), n.resolveFileUrl ? n.resolveFileUrl(t.props.url).then((a) => { r.src = a; }) : (r.src = t.props.url), (r.controls = true), (r.contentEditable = "false"), (r.draggable = false), J(t, n, { dom: r }, o.firstElementChild) ); }, Ft$1 = (e = {}) => (t, n) => { if (!t.props.url) { const r = document.createElement("p"); return ( (r.textContent = "Add audio"), { dom: r, } ); } let o; return ( t.props.showPreview ? ((o = document.createElement("audio")), (o.src = t.props.url)) : ((o = document.createElement("a")), (o.href = t.props.url), (o.textContent = t.props.name || t.props.url)), t.props.caption ? t.props.showPreview ? Y(o, t.props.caption) : U(o, t.props.caption) : { dom: o, } ); }, $t$1 = v$1(Vt$1, (e) => ({ meta: { fileBlockAccept: ["audio/*"], }, parse: Wt$1(e), render: Rt$1(e), toExternalHTML: Ft$1(e), runsBefore: ["file"], })), fe$1 = Symbol.for("blocknote.shikiParser"), j = Symbol.for("blocknote.shikiHighlighterPromise"); function Ut$1(e) { const t = globalThis; let n, o; return createHighlightPlugin({ parser: (s) => { if (!e.createHighlighter) return []; if (!n) return ( (t[j] = t[j] || e.createHighlighter()), t[j].then((c) => { n = c; }) ); const i = Be$1(e, s.language); return ( !i || i === "text" || i === "none" || i === "plaintext" || i === "txt" ? [] : n.getLoadedLanguages().includes(i) ? (o || ((o = t[fe$1] || createParser(n)), (t[fe$1] = o)), o(s)) : n.loadLanguage(i) ); }, languageExtractor: (s) => s.attrs.language, nodeTypes: ["codeBlock"], }); } const qt$1 = ({ defaultLanguage: e = "text" }) => ({ type: "codeBlock", propSchema: { language: { default: e, }, }, content: "inline", }), jt$1 = v$1( qt$1, (e) => ({ meta: { code: true, defining: true, isolating: false, }, parse: (t) => { var r, a; if (t.tagName !== "PRE" || t.childElementCount !== 1 || ((r = t.firstElementChild) == null ? void 0 : r.tagName) !== "CODE") return; const n = t.firstElementChild; return { language: n.getAttribute("data-language") || ((a = n.className.split(" ").find((s) => s.includes("language-"))) == null ? void 0 : a.replace("language-", "")) }; }, parseContent: ({ el: t, schema: n }) => { const o = DOMParser$1.fromSchema(n), r = t.firstElementChild; return o.parse(r, { preserveWhitespace: "full", topNode: n.nodes.codeBlock.create(), }).content; }, render(t, n) { const o = document.createDocumentFragment(), r = document.createElement("pre"), a = document.createElement("code"); r.appendChild(a); let s; if (e.supportedLanguages) { const i = document.createElement("select"); if ( (Object.entries(e.supportedLanguages ?? {}).forEach(([l, { name: u }]) => { const d = document.createElement("option"); ((d.value = l), (d.text = u), i.appendChild(d)); }), (i.value = t.props.language || e.defaultLanguage || "text"), n.isEditable) ) { const l = (u) => { const d = u.target.value; n.updateBlock(t.id, { props: { language: d } }); }; (i.addEventListener("change", l), (s = () => i.removeEventListener("change", l))); } else i.disabled = true; const c = document.createElement("div"); ((c.contentEditable = "false"), c.appendChild(i), o.appendChild(c)); } return ( o.appendChild(r), { dom: o, contentDOM: a, destroy: () => { s == null || s(); }, } ); }, toExternalHTML(t) { const n = document.createElement("pre"), o = document.createElement("code"); return ( (o.className = `language-${t.props.language}`), (o.dataset.language = t.props.language), n.appendChild(o), { dom: n, contentDOM: o, } ); }, }), (e) => [ a$1({ key: "code-block-highlighter", prosemirrorPlugins: [Ut$1(e)], }), a$1({ key: "code-block-keyboard-shortcuts", keyboardShortcuts: { "Delete": ({ editor: t }) => t.transact((n) => { const { block: o } = t.getTextCursorPosition(); if (o.type !== "codeBlock") return false; const { $from: r } = n.selection; return r.parent.textContent ? false : (t.removeBlocks([o]), true); }), "Tab": ({ editor: t }) => e.indentLineWithTab === false ? false : t.transact((n) => { const { block: o } = t.getTextCursorPosition(); return o.type === "codeBlock" ? (n.insertText(" "), true) : false; }), "Enter": ({ editor: t }) => t.transact((n) => { const { block: o, nextBlock: r } = t.getTextCursorPosition(); if (o.type !== "codeBlock") return false; const { $from: a } = n.selection, s = a.parentOffset === a.parent.nodeSize - 2, i = a.parent.textContent.endsWith(` `); if (s && i) { if ((n.delete(a.pos - 2, a.pos), r)) return (t.setTextCursorPosition(r, "start"), true); const [c] = t.insertBlocks([{ type: "paragraph" }], o, "after"); return (t.setTextCursorPosition(c, "start"), true); } return ( n.insertText(` `), true ); }), "Shift-Enter": ({ editor: t }) => t.transact(() => { const { block: n } = t.getTextCursorPosition(); if (n.type !== "codeBlock") return false; const [o] = t.insertBlocks( // insert a new paragraph [{ type: "paragraph" }], n, "after", ); return (t.setTextCursorPosition(o, "start"), true); }), }, inputRules: [ { find: /^```(.*?)\s$/, replace: ({ match: t }) => { const n = t[1].trim(); return { type: "codeBlock", props: { language: { language: Be$1(e, n) ?? n, }.language, }, content: [], }; }, }, ], }), ], ); function Be$1(e, t) { var n; return (n = Object.entries(e.supportedLanguages ?? {}).find(([o, { aliases: r }]) => (r == null ? void 0 : r.includes(t)) || o === t)) == null ? void 0 : n[0]; } const Gt$1 = () => ({ type: "divider", propSchema: {}, content: "none", }), zt$1 = v$1( Gt$1, { meta: { isolating: false, }, parse(e) { if (e.tagName === "HR") return {}; }, render() { return { dom: document.createElement("hr"), }; }, }, [ a$1({ key: "divider-block-shortcuts", inputRules: [ { find: new RegExp("^---$"), replace() { return { type: "divider", props: {}, content: [] }; }, }, ], }), ], ), me$1 = (e) => ({ url: e.src || void 0 }), Zt$2 = () => ({ type: "file", propSchema: { backgroundColor: m$1.backgroundColor, // File name. name: { default: "", }, // File url. url: { default: "", }, // File caption. caption: { default: "", }, }, content: "none", }), Xt$1 = () => (e) => { if (e.tagName === "EMBED") { if (e.closest("figure")) return; const { backgroundColor: t } = C(e); return { ...me$1(e), backgroundColor: t, }; } if (e.tagName === "FIGURE") { const t = $$2(e, "embed"); if (!t) return; const { targetElement: n, caption: o } = t, { backgroundColor: r } = C(e); return { ...me$1(n), backgroundColor: r, caption: o, }; } }, Kt$1 = v$1(Zt$2, { meta: { fileBlockAccept: ["*/*"], }, parse: Xt$1(), render(e, t) { return J(e, t); }, toExternalHTML(e) { if (!e.props.url) { const n = document.createElement("p"); return ( (n.textContent = "Add file"), { dom: n, } ); } const t = document.createElement("a"); return ( (t.href = e.props.url), (t.textContent = e.props.name || e.props.url), e.props.caption ? U(t, e.props.caption) : { dom: t, } ); }, }); function Ae$2(e, t, n) { var u; const o = DOMParser$1.fromSchema(t), r = e.querySelector(":scope > summary"); let a; if (r) { const d = r.cloneNode(true); (we$1(d), (a = o.parse(d, { topNode: t.nodes.paragraph.create(), preserveWhitespace: true, }).content)); } else a = Fragment.empty; const s = document.createElement("div"); s.setAttribute("data-node-type", "blockGroup"); let i = false; for (const d of Array.from(e.childNodes)) d.tagName !== "SUMMARY" && ((d.nodeType === 3 && !((u = d.textContent) != null && u.trim())) || ((i = true), s.appendChild(d.cloneNode(true)))); const c = t.nodes[n].create({}, a); if (!i) return c.content; const l = o.parse(s, { topNode: t.nodes.blockGroup.create(), }); return l.content.size > 0 ? c.content.addToEnd(l) : c.content; } const Qt$1 = { set: (e, t) => window.localStorage.setItem(`toggle-${e.id}`, t ? "true" : "false"), get: (e) => window.localStorage.getItem(`toggle-${e.id}`) === "true", }, Ne$1 = (e, t, n, o = Qt$1) => { if ("isToggleable" in e.props && !e.props.isToggleable) return { dom: n, }; const r = document.createElement("div"), a = document.createElement("div"); a.className = "bn-toggle-wrapper"; const s = document.createElement("button"); ((s.className = "bn-toggle-button"), (s.type = "button"), (s.innerHTML = '')); // https://fonts.google.com/icons?selected=Material+Symbols+Rounded:chevron_right:FILL@0;wght@700;GRAD@0;opsz@24&icon.query=chevron&icon.style=Rounded&icon.size=24&icon.color=%23e8eaed const i = (f) => f.preventDefault(); s.addEventListener("mousedown", i); const c = () => { var f; a.getAttribute("data-show-children") === "true" ? (a.setAttribute("data-show-children", "false"), o.set(t.getBlock(e), false), r.contains(l) && r.removeChild(l)) : (a.setAttribute("data-show-children", "true"), o.set(t.getBlock(e), true), t.isEditable && ((f = t.getBlock(e)) == null ? void 0 : f.children.length) === 0 && !r.contains(l) && r.appendChild(l)); }; (s.addEventListener("click", c), a.appendChild(s), a.appendChild(n)); const l = document.createElement("button"); ((l.className = "bn-toggle-add-block-button"), (l.type = "button"), (l.textContent = t.dictionary.toggle_blocks.add_block_button)); const u = (f) => f.preventDefault(); l.addEventListener("mousedown", u); const d = () => { t.transact(() => { const f = t.updateBlock(e, { // Single empty block with default type. children: [{}], }); (t.setTextCursorPosition(f.children[0].id, "end"), t.focus()); }); }; (l.addEventListener("click", d), r.appendChild(a)); let p = e.children.length; const h = t.onChange(() => { var E; const f = ((E = t.getBlock(e)) == null ? void 0 : E.children.length) ?? 0; (f > p ? (a.getAttribute("data-show-children") === "false" && (a.setAttribute("data-show-children", "true"), o.set(t.getBlock(e), true)), r.contains(l) && r.removeChild(l)) : f === 0 && f < p && (a.getAttribute("data-show-children") === "true" && (a.setAttribute("data-show-children", "false"), o.set(t.getBlock(e), false)), r.contains(l) && r.removeChild(l)), (p = f)); }); return ( o.get(e) ? (a.setAttribute("data-show-children", "true"), t.isEditable && e.children.length === 0 && r.appendChild(l)) : a.setAttribute("data-show-children", "false"), { dom: r, // Prevents re-renders when the toggle button is clicked. ignoreMutation: (f) => f instanceof MutationRecord && // We want to prevent re-renders when the view changes, so we ignore // all mutations where the `data-show-children` attribute is changed // or the "add block" button is added/removed. ((f.type === "attributes" && f.target === a && f.attributeName === "data-show-children") || (f.type === "childList" && (f.addedNodes[0] === l || f.removedNodes[0] === l))), destroy: () => { (s.removeEventListener("mousedown", i), s.removeEventListener("click", c), l.removeEventListener("mousedown", u), l.removeEventListener("click", d), h == null || h()); }, } ); }, Pe$2 = [1, 2, 3, 4, 5, 6], Jt$1 = (e) => ({ editor: t }) => { const n = t.getTextCursorPosition(); return t.schema.blockSchema[n.block.type].content !== "inline" ? false : (t.updateBlock(n.block, { type: "heading", props: { level: e }, }), true); }, Yt$1 = ({ defaultLevel: e = 1, levels: t = Pe$2, allowToggleHeadings: n = true } = {}) => ({ type: "heading", propSchema: { ...m$1, level: { default: e, values: t }, ...(n ? { isToggleable: { default: false, optional: true } } : {}), }, content: "inline", }), en$1 = v$1( Yt$1, ({ allowToggleHeadings: e = true } = {}) => ({ meta: { isolating: false, }, parse(t) { if (e && t.tagName === "DETAILS") { const o = t.querySelector(":scope > summary"); if (!o) return; const r = o.querySelector("h1, h2, h3, h4, h5, h6"); return r ? { ...C(r), level: parseInt(r.tagName[1]), isToggleable: true, } : void 0; } let n; switch (t.tagName) { case "H1": n = 1; break; case "H2": n = 2; break; case "H3": n = 3; break; case "H4": n = 4; break; case "H5": n = 5; break; case "H6": n = 6; break; default: return; } return { ...C(t), level: n, }; }, ...(e ? { parseContent: ({ el: t, schema: n }) => { if (t.tagName === "DETAILS") return Ae$2(t, n, "heading"); }, } : {}), runsBefore: ["toggleListItem"], render(t, n) { const o = document.createElement(`h${t.props.level}`); return e ? { ...Ne$1(t, n, o), contentDOM: o } : { dom: o, contentDOM: o, }; }, toExternalHTML(t) { const n = document.createElement(`h${t.props.level}`); if ((A$2(t.props, n), e && t.props.isToggleable)) { const o = document.createElement("details"); o.setAttribute("open", ""); const r = document.createElement("summary"); return ( r.appendChild(n), o.appendChild(r), { dom: o, contentDOM: n, childrenDOM: o, } ); } return { dom: n, contentDOM: n, }; }, }), ({ levels: e = Pe$2 } = {}) => [ a$1({ key: "heading-shortcuts", keyboardShortcuts: Object.fromEntries(e.map((t) => [`Mod-Alt-${t}`, Jt$1(t)]) ?? []), inputRules: e.map((t) => ({ find: new RegExp(`^(#{${t}})\\s$`), replace({ match: n }) { return { type: "heading", props: { level: n[1].length, }, }; }, })), }), ], ), Ie$2 = (e, t, n, o, r) => { const { dom: a, destroy: s } = J(e, t, n, r), i = a; ((i.style.position = "relative"), e.props.url && e.props.showPreview && (e.props.previewWidth ? (i.style.width = `${e.props.previewWidth}px`) : (i.style.width = "fit-content"))); const c = document.createElement("div"); ((c.className = "bn-resize-handle"), (c.style.left = "4px")); const l = document.createElement("div"); ((l.className = "bn-resize-handle"), (l.style.right = "4px")); const u = document.createElement("div"); ((u.style.position = "absolute"), (u.style.height = "100%"), (u.style.width = "100%")); let d, p = e.props.previewWidth; const h = (g) => { var te, ne; if (!d) { !t.isEditable && o.contains(c) && o.contains(l) && (o.removeChild(c), o.removeChild(l)); return; } let M; const V = "touches" in g ? g.touches[0].clientX : g.clientX; (e.props.textAlignment === "center" ? d.handleUsed === "left" ? (M = d.initialWidth + (d.initialClientX - V) * 2) : (M = d.initialWidth + (V - d.initialClientX) * 2) : d.handleUsed === "left" ? (M = d.initialWidth + d.initialClientX - V) : (M = d.initialWidth + V - d.initialClientX), (p = Math.min(Math.max(M, 64), ((ne = (te = t.domElement) == null ? void 0 : te.firstElementChild) == null ? void 0 : ne.clientWidth) || Number.MAX_VALUE)), (i.style.width = `${p}px`)); }, f = (g) => { ((!g.target || !i.contains(g.target) || !t.isEditable) && o.contains(c) && o.contains(l) && (o.removeChild(c), o.removeChild(l)), d && ((d = void 0), i.contains(u) && i.removeChild(u), t.updateBlock(e, { props: { previewWidth: p, }, }))); }, E = () => { t.isEditable && (o.appendChild(c), o.appendChild(l)); }, x = (g) => { g.relatedTarget === c || g.relatedTarget === l || d || (t.isEditable && o.contains(c) && o.contains(l) && (o.removeChild(c), o.removeChild(l))); }, S = (g) => { (g.preventDefault(), i.contains(u) || i.appendChild(u)); const M = "touches" in g ? g.touches[0].clientX : g.clientX; d = { handleUsed: "left", initialWidth: i.clientWidth, initialClientX: M, }; }, N = (g) => { (g.preventDefault(), i.contains(u) || i.appendChild(u)); const M = "touches" in g ? g.touches[0].clientX : g.clientX; d = { handleUsed: "right", initialWidth: i.clientWidth, initialClientX: M, }; }; return ( window.addEventListener("mousemove", h), window.addEventListener("touchmove", h), window.addEventListener("mouseup", f), window.addEventListener("touchend", f), i.addEventListener("mouseenter", E), i.addEventListener("mouseleave", x), c.addEventListener("mousedown", S), c.addEventListener("touchstart", S), l.addEventListener("mousedown", N), l.addEventListener("touchstart", N), { dom: i, destroy: () => { (s == null || s(), window.removeEventListener("mousemove", h), window.removeEventListener("touchmove", h), window.removeEventListener("mouseup", f), window.removeEventListener("touchend", f), i.removeEventListener("mouseenter", E), i.removeEventListener("mouseleave", x), c.removeEventListener("mousedown", S), c.removeEventListener("touchstart", S), l.removeEventListener("mousedown", N), l.removeEventListener("touchstart", N)); }, } ); }, he$2 = (e) => { const t = e.src || void 0, n = e.width || void 0, o = e.alt || void 0; return { url: t, previewWidth: n, name: o }; }, tn$1 = '', nn$1 = (e = {}) => ({ type: "image", propSchema: { textAlignment: m$1.textAlignment, backgroundColor: m$1.backgroundColor, // File name. name: { default: "", }, // File url. url: { default: "", }, // File caption. caption: { default: "", }, showPreview: { default: true, }, // File preview width in px. previewWidth: { default: void 0, type: "number", }, }, content: "none", }), on$1 = (e = {}) => (t) => { if (t.tagName === "IMG") { if (t.closest("figure")) return; const { backgroundColor: n } = C(t); return { ...he$2(t), backgroundColor: n, }; } if (t.tagName === "FIGURE") { const n = $$2(t, "img"); if (!n) return; const { targetElement: o, caption: r } = n, { backgroundColor: a } = C(t); return { ...he$2(o), backgroundColor: a, caption: r, }; } }, rn$1 = (e = {}) => (t, n) => { const o = document.createElement("div"); o.innerHTML = e.icon ?? tn$1; const r = document.createElement("div"); r.className = "bn-visual-media-wrapper"; const a = document.createElement("img"); return ( (a.className = "bn-visual-media"), n.resolveFileUrl ? n.resolveFileUrl(t.props.url).then((s) => { a.src = s; }) : (a.src = t.props.url), (a.alt = t.props.name || t.props.caption || "BlockNote image"), (a.contentEditable = "false"), (a.draggable = false), r.appendChild(a), Ie$2(t, n, { dom: r }, r, o.firstElementChild) ); }, an$1 = (e = {}) => (t, n) => { if (!t.props.url) { const r = document.createElement("p"); return ( (r.textContent = "Add image"), { dom: r, } ); } let o; return ( t.props.showPreview ? ((o = document.createElement("img")), (o.src = t.props.url), (o.alt = t.props.name || t.props.caption || "BlockNote image"), t.props.previewWidth && (o.width = t.props.previewWidth)) : ((o = document.createElement("a")), (o.href = t.props.url), (o.textContent = t.props.name || t.props.url)), t.props.caption ? t.props.showPreview ? Y(o, t.props.caption) : U(o, t.props.caption) : { dom: o, } ); }, sn$1 = v$1(nn$1, (e) => ({ meta: { fileBlockAccept: ["image/*"], }, parse: on$1(e), render: rn$1(e), toExternalHTML: an$1(e), runsBefore: ["file"], })), wo$1 = (e, t, n) => ({ state: o, dispatch: r }) => r ? He$1(o.tr, e, t, n) : true, He$1 = (e, t, n, o) => { const r = Y$1(e.doc, t), a = Z(r); if (!a.isBlockContainer) return false; const s = ht(e), i = [ { type: a.bnBlock.node.type, // always keep blockcontainer type attrs: o ? { ...a.bnBlock.node.attrs, id: void 0 } : {}, }, { type: n ? a.blockContent.node.type : s.nodes.paragraph, attrs: o ? { ...a.blockContent.node.attrs } : {}, }, ]; return (e.split(t, 2, i), true); }, q$1 = (e, t) => { const { blockInfo: n, selectionEmpty: o } = e.transact((s) => ({ blockInfo: Ot$2(s), selectionEmpty: s.selection.anchor === s.selection.head, })); if (!n.isBlockContainer) return false; const { bnBlock: r, blockContent: a } = n; return ( a.node.type.name !== t || !o ? false : a.node.childCount === 0 ? (e.transact((s) => { Q$1(s, r.beforePos, { type: "paragraph", props: {}, }); }), true) : a.node.childCount > 0 ? e.transact((s) => (s.deleteSelection(), He$1(s, s.selection.from, true))) : false ); }; function ee(e, t, n) { var d, p, h; const o = DOMParser$1.fromSchema(t), r = e, a = document.createElement("div"); a.setAttribute("data-node-type", "blockGroup"); for (const f of Array.from(r.childNodes)) a.appendChild(f.cloneNode(true)); let s = o.parse(a, { topNode: t.nodes.blockGroup.create(), }); ((p = (d = s.firstChild) == null ? void 0 : d.firstChild) == null ? void 0 : p.type.name) === "checkListItem" && (s = s.copy(s.content.cut(s.firstChild.firstChild.nodeSize + 2))); const i = (h = s.firstChild) == null ? void 0 : h.firstChild; if (!(i != null && i.isTextblock)) return Fragment.from(s); const c = t.nodes[n].create({}, i.content), l = s.content.cut( // +2 for the `blockGroup` node's start and end markers i.nodeSize + 2, ); if (l.size > 0) { const f = s.copy(l); return c.content.addToEnd(f); } return c.content; } const cn$1 = () => ({ type: "bulletListItem", propSchema: { ...m$1, }, content: "inline", }), ln$1 = v$1( cn$1, { meta: { isolating: false, }, parse(e) { var n; if (e.tagName !== "LI") return; const t = e.parentElement; if (t !== null && (t.tagName === "UL" || (t.tagName === "DIV" && ((n = t.parentElement) == null ? void 0 : n.tagName) === "UL"))) return C(e); }, // As `li` elements can contain multiple paragraphs, we need to merge their contents // into a single one so that ProseMirror can parse everything correctly. parseContent: ({ el: e, schema: t }) => ee(e, t, "bulletListItem"), render() { const e = document.createElement("p"); return { dom: e, contentDOM: e, }; }, toExternalHTML(e) { const t = document.createElement("li"), n = document.createElement("p"); return ( A$2(e.props, t), t.appendChild(n), { dom: t, contentDOM: n, } ); }, }, [ a$1({ key: "bullet-list-item-shortcuts", keyboardShortcuts: { "Enter": ({ editor: e }) => q$1(e, "bulletListItem"), "Mod-Shift-8": ({ editor: e }) => { const t = e.getTextCursorPosition(); return e.schema.blockSchema[t.block.type].content !== "inline" ? false : (e.updateBlock(t.block, { type: "bulletListItem", props: {}, }), true); }, }, inputRules: [ { find: /^\s?[-+*]\s$/, replace({ editor: e }) { if (Tt$2(e.prosemirrorState).blockNoteType !== "heading") return { type: "bulletListItem", props: {}, }; }, }, ], }), ], ), dn$1 = () => ({ type: "checkListItem", propSchema: { ...m$1, checked: { default: false, type: "boolean" }, }, content: "inline", }), un$1 = v$1( dn$1, { meta: { isolating: false, }, parse(e) { var n; if (e.tagName === "input") return ( e.closest("[data-content-type]") || e.closest("li") ? void 0 : e.type === "checkbox" ? { checked: e.checked } : void 0 ); if (e.tagName !== "LI") return; const t = e.parentElement; if (t !== null && (t.tagName === "UL" || (t.tagName === "DIV" && ((n = t.parentElement) == null ? void 0 : n.tagName) === "UL"))) { const o = e.querySelector("input[type=checkbox]") || null; return o === null ? void 0 : { ...C(e), checked: o.checked }; } }, // As `li` elements can contain multiple paragraphs, we need to merge their contents // into a single one so that ProseMirror can parse everything correctly. parseContent: ({ el: e, schema: t }) => ee(e, t, "checkListItem"), render(e, t) { const n = document.createDocumentFragment(), o = document.createElement("input"); ((o.type = "checkbox"), (o.checked = e.props.checked), e.props.checked && o.setAttribute("checked", ""), (o.disabled = !t.isEditable), o.addEventListener("change", () => { t.isEditable && t.updateBlock(e, { props: { checked: !e.props.checked } }); })); const r = document.createElement("p"), a = document.createElement("div"); return ( (a.contentEditable = "false"), a.appendChild(o), n.appendChild(a), n.appendChild(r), { dom: n, contentDOM: r, } ); }, toExternalHTML(e) { const t = document.createElement("li"), n = document.createElement("input"); ((n.type = "checkbox"), (n.checked = e.props.checked), e.props.checked && n.setAttribute("checked", "")); const o = document.createElement("p"); return ( A$2(e.props, t), t.appendChild(n), t.appendChild(o), { dom: t, contentDOM: o, } ); }, runsBefore: ["bulletListItem"], }, [ a$1({ key: "check-list-item-shortcuts", keyboardShortcuts: { "Enter": ({ editor: e }) => q$1(e, "checkListItem"), "Mod-Shift-9": ({ editor: e }) => { const t = e.getTextCursorPosition(); return e.schema.blockSchema[t.block.type].content !== "inline" ? false : (e.updateBlock(t.block, { type: "checkListItem", props: {}, }), true); }, }, inputRules: [ { find: /^\s?\[\s*\]\s$/, replace() { return { type: "checkListItem", props: { checked: false, }, }; }, }, { find: /^\s?\[[Xx]\]\s$/, replace() { return { type: "checkListItem", props: { checked: true, }, }; }, }, ], }), ], ); function De$2(e, t, n, o) { let r = e.firstChild.attrs.start || 1, a = true; const s = !!e.firstChild.attrs.start, i = Z({ posBeforeNode: t, node: e, }); if (!i.isBlockContainer) throw new Error("impossible"); const c = n.doc.resolve(i.bnBlock.beforePos).nodeBefore, l = c ? o.get(c) : void 0; return ( l !== void 0 ? ((r = l + 1), (a = false)) : c && Z({ posBeforeNode: i.bnBlock.beforePos - c.nodeSize, node: c, }).blockNoteType === "numberedListItem" && ((r = De$2(c, i.bnBlock.beforePos - c.nodeSize, n, o).index + 1), (a = false)), o.set(e, r), { index: r, isFirst: a, hasStart: s } ); } function ge(e, t) { const n = /* @__PURE__ */ new Map(), o = t.decorations.map(e.mapping, e.doc), r = []; e.doc.nodesBetween(0, e.doc.nodeSize - 2, (s, i) => { if (s.type.name === "blockContainer" && s.firstChild.type.name === "numberedListItem") { const { index: c, isFirst: l, hasStart: u } = De$2(s, i, e, n); if (o.find(i, i + s.nodeSize, (p) => p.index === c && p.isFirst === l && p.hasStart === u).length === 0) { const p = e.doc.nodeAt(i + 1); r.push( // move in by 1 to account for the block container Decoration.node(i + 1, i + 1 + p.nodeSize, { "data-index": c.toString(), }), ); } } }); const a = r.flatMap((s) => o.find(s.from, s.to)); return { decorations: o.remove(a).add(e.doc, r), }; } const pn$1 = () => new Plugin({ key: new PluginKey("numbered-list-indexing-decorations"), state: { init(e, t) { return ge(t.tr, { decorations: DecorationSet.empty, }); }, apply(e, t) { return !e.docChanged && !e.selectionSet && t.decorations ? t : ge(e, t); }, }, props: { decorations(e) { var t; return ((t = this.getState(e)) == null ? void 0 : t.decorations) ?? DecorationSet.empty; }, }, }), fn$1 = () => ({ type: "numberedListItem", propSchema: { ...m$1, start: { default: void 0, type: "number" }, }, content: "inline", }), mn$1 = v$1( fn$1, { meta: { isolating: false, }, parse(e) { var n; if (e.tagName !== "LI") return; const t = e.parentElement; if (t !== null && (t.tagName === "OL" || (t.tagName === "DIV" && ((n = t.parentElement) == null ? void 0 : n.tagName) === "OL"))) { const o = parseInt(t.getAttribute("start") || "1"), r = C(e); return e.previousElementSibling || o === 1 ? r : { ...r, start: o, }; } }, // As `li` elements can contain multiple paragraphs, we need to merge their contents // into a single one so that ProseMirror can parse everything correctly. parseContent: ({ el: e, schema: t }) => ee(e, t, "numberedListItem"), render() { const e = document.createElement("p"); return { dom: e, contentDOM: e, }; }, toExternalHTML(e) { const t = document.createElement("li"), n = document.createElement("p"); return ( A$2(e.props, t), t.appendChild(n), { dom: t, contentDOM: n, } ); }, }, [ a$1({ key: "numbered-list-item-shortcuts", inputRules: [ { find: /^\s?(\d+)\.\s$/, replace({ match: e, editor: t }) { if (Tt$2(t.prosemirrorState).blockNoteType === "heading") return; const o = parseInt(e[1]); return { type: "numberedListItem", props: { start: o !== 1 ? o : void 0, }, }; }, }, ], keyboardShortcuts: { "Enter": ({ editor: e }) => q$1(e, "numberedListItem"), "Mod-Shift-7": ({ editor: e }) => { const t = e.getTextCursorPosition(); return e.schema.blockSchema[t.block.type].content !== "inline" ? false : (e.updateBlock(t.block, { type: "numberedListItem", props: {}, }), true); }, }, prosemirrorPlugins: [pn$1()], }), ], ), hn$1 = () => ({ type: "toggleListItem", propSchema: { ...m$1, }, content: "inline", }), gn$1 = v$1( hn$1, { meta: { isolating: false, }, parse(e) { var t; if (e.tagName === "DETAILS") return C(e); if (e.tagName === "LI") { const n = e.parentElement; if (n && (n.tagName === "UL" || (n.tagName === "DIV" && ((t = n.parentElement) == null ? void 0 : t.tagName) === "UL")) && e.querySelector(":scope > details")) return C(e); } }, parseContent: ({ el: e, schema: t }) => { const n = e.tagName === "DETAILS" ? e : e.querySelector(":scope > details"); if (!n) throw new Error("No details found in toggleListItem parseContent"); return Ae$2(n, t, "toggleListItem"); }, runsBefore: ["bulletListItem"], render(e, t) { const n = document.createElement("p"); return { ...Ne$1(e, t, n), contentDOM: n }; }, toExternalHTML(e) { const t = document.createElement("li"), n = document.createElement("details"); n.setAttribute("open", ""); const o = document.createElement("summary"), r = document.createElement("p"); return ( o.appendChild(r), n.appendChild(o), A$2(e.props, t), t.appendChild(n), { dom: t, contentDOM: r, childrenDOM: n, } ); }, }, [ a$1({ key: "toggle-list-item-shortcuts", keyboardShortcuts: { "Enter": ({ editor: e }) => q$1(e, "toggleListItem"), "Mod-Shift-6": ({ editor: e }) => { const t = e.getTextCursorPosition(); return e.schema.blockSchema[t.block.type].content !== "inline" ? false : (e.updateBlock(t.block, { type: "toggleListItem", props: {}, }), true); }, }, }), ], ), bn$1 = () => ({ type: "paragraph", propSchema: m$1, content: "inline", }), Cn$1 = v$1( bn$1, { meta: { isolating: false, }, parse: (e) => { var t; if (e.tagName === "P" && (t = e.textContent) != null && t.trim()) return C(e); }, render: () => { const e = document.createElement("p"); return { dom: e, contentDOM: e, }; }, toExternalHTML: (e) => { const t = document.createElement("p"); return ( A$2(e.props, t), { dom: t, contentDOM: t, } ); }, runsBefore: ["default", "heading"], }, [ a$1({ key: "paragraph-shortcuts", keyboardShortcuts: { "Mod-Alt-0": ({ editor: e }) => { const t = e.getTextCursorPosition(); return e.schema.blockSchema[t.block.type].content !== "inline" ? false : (e.updateBlock(t.block, { type: "paragraph", props: {}, }), true); }, }, }), ], ), kn$1 = () => ({ type: "quote", propSchema: { backgroundColor: m$1.backgroundColor, textColor: m$1.textColor, }, content: "inline", }), yn$1 = v$1( kn$1, { meta: { isolating: false, }, parse(e) { if (e.tagName === "BLOCKQUOTE") { const { backgroundColor: t, textColor: n } = C(e); return { backgroundColor: t, textColor: n }; } }, render() { const e = document.createElement("blockquote"); return { dom: e, contentDOM: e, }; }, toExternalHTML(e) { const t = document.createElement("blockquote"); return ( A$2(e.props, t), { dom: t, contentDOM: t, } ); }, }, [ a$1({ key: "quote-block-shortcuts", keyboardShortcuts: { "Mod-Alt-q": ({ editor: e }) => { const t = e.getTextCursorPosition(); return e.schema.blockSchema[t.block.type].content !== "inline" ? false : (e.updateBlock(t.block, { type: "quote", props: {}, }), true); }, }, inputRules: [ { find: new RegExp("^>\\s$"), replace() { return { type: "quote", props: {}, }; }, }, ], }), ], ), vn$1 = 35, Oe$2 = 120, Lo$2 = 31, En$1 = Extension.create({ name: "BlockNoteTableExtension", addProseMirrorPlugins: () => [ columnResizing({ cellMinWidth: vn$1, defaultCellMinWidth: Oe$2, // We set this to null as we implement our own node view in the table // block content. This node view is the same as what's used by default, // but is wrapped in a `blockContent` HTML element. View: null, }), tableEditing(), ], addKeyboardShortcuts() { return { // Makes enter create a new line within the cell. "Enter": () => this.editor.state.selection.empty && this.editor.state.selection.$head.parent.type.name === "tableParagraph" ? (this.editor.commands.insertContent({ type: "hardBreak" }), true) : false, // Ensures that backspace won't delete the table if the text cursor is at // the start of a cell and the selection is empty. "Backspace": () => { const e = this.editor.state.selection, t = e.empty, n = e.$head.parentOffset === 0, o = e.$head.node().type.name === "tableParagraph"; return t && n && o; }, // Enables navigating cells using the tab key. "Tab": () => this.editor.commands.command(({ state: e, dispatch: t, view: n }) => goToNextCell(1)(e, t, n)), "Shift-Tab": () => this.editor.commands.command(({ state: e, dispatch: t, view: n }) => goToNextCell(-1)(e, t, n)), }; }, extendNodeSchema(e) { const t = { name: e.name, options: e.options, storage: e.storage, }; return { tableRole: callOrReturn(getExtensionField(e, "tableRole", t)), }; }, }), Sn$1 = { textColor: m$1.textColor, }, xn$1 = Node3.create({ name: "tableHeader", addOptions() { return { HTMLAttributes: {}, }; }, /** * We allow table headers and cells to have multiple tableContent nodes because * when merging cells, prosemirror-tables will concat the contents of the cells naively. * This would cause that content to overflow into other cells when prosemirror tries to enforce the cell structure. * * So, we manually fix this up when reading back in the `nodeToBlock` and only ever place a single tableContent back into the cell. */ content: "tableContent+", addAttributes() { return { colspan: { default: 1, }, rowspan: { default: 1, }, colwidth: { default: null, parseHTML: (e) => { const t = e.getAttribute("colwidth"); return t ? t.split(",").map((o) => parseInt(o, 10)) : null; }, }, }; }, tableRole: "header_cell", isolating: true, parseHTML() { return [ { tag: "th", // As `th` elements can contain multiple paragraphs, we need to merge their contents // into a single one so that ProseMirror can parse everything correctly. getContent: (e, t) => _e$1(e, t), }, ]; }, renderHTML({ HTMLAttributes: e }) { return ["th", mergeAttributes(this.options.HTMLAttributes, e), 0]; }, }), Mn$1 = Node3.create({ name: "tableCell", addOptions() { return { HTMLAttributes: {}, }; }, content: "tableContent+", addAttributes() { return { colspan: { default: 1, }, rowspan: { default: 1, }, colwidth: { default: null, parseHTML: (e) => { const t = e.getAttribute("colwidth"); return t ? t.split(",").map((o) => parseInt(o, 10)) : null; }, }, }; }, tableRole: "cell", isolating: true, parseHTML() { return [ { tag: "td", // As `td` elements can contain multiple paragraphs, we need to merge their contents // into a single one so that ProseMirror can parse everything correctly. getContent: (e, t) => _e$1(e, t), }, ]; }, renderHTML({ HTMLAttributes: e }) { return ["td", mergeAttributes(this.options.HTMLAttributes, e), 0]; }, }), wn$1 = Node3.create({ name: "table", content: "tableRow+", group: "blockContent", tableRole: "table", marks: "deletion insertion modification", isolating: true, parseHTML() { return [ { tag: "table", }, ]; }, renderHTML({ node: e, HTMLAttributes: t }) { var r, a, s; const n = Ct( this.name, "table", { ...(((r = this.options.domAttributes) == null ? void 0 : r.blockContent) || {}), ...t, }, ((a = this.options.domAttributes) == null ? void 0 : a.inlineContent) || {}, ), o = document.createElement("colgroup"); for (const i of e.children[0].children) if (i.attrs.colwidth) for (const l of i.attrs.colwidth) { const u = document.createElement("col"); (l && (u.style = `width: ${l}px`), o.appendChild(u)); } else o.appendChild(document.createElement("col")); return ((s = n.dom.firstChild) == null || s.appendChild(o), n); }, // This node view is needed for the `columnResizing` plugin. By default, the // plugin adds its own node view, which overrides how the node is rendered vs // `renderHTML`. This means that the wrapping `blockContent` HTML element is // no longer rendered. The `columnResizing` plugin uses the `TableView` as its // default node view. `BlockNoteTableView` extends it by wrapping it in a // `blockContent` element, so the DOM structure is consistent with other block // types. addNodeView() { return ({ node: e, HTMLAttributes: t }) => { var o; class n extends TableView { constructor(a, s, i) { (super(a, s), (this.node = a), (this.cellMinWidth = s), (this.blockContentHTMLAttributes = i)); const c = document.createElement("div"); ((c.className = D$2("bn-block-content", i.class)), c.setAttribute("data-content-type", "table")); for (const [p, h] of Object.entries(i)) p !== "class" && c.setAttribute(p, h); const l = this.dom, u = document.createElement("div"); ((u.className = "tableWrapper-inner"), u.appendChild(l.firstChild), l.appendChild(u), c.appendChild(l)); const d = document.createElement("div"); ((d.className = "table-widgets-container"), (d.style.position = "relative"), l.appendChild(d), (this.dom = c)); } ignoreMutation(a) { return !a.target.closest(".tableWrapper-inner") || super.ignoreMutation(a); } } return new n(e, Oe$2, { ...(((o = this.options.domAttributes) == null ? void 0 : o.blockContent) || {}), ...t, }); }; }, }), Ln$1 = Node3.create({ name: "tableParagraph", group: "tableContent", content: "inline*", parseHTML() { return [ { tag: "p", getAttrs: (e) => { if (typeof e == "string" || !e.textContent || !e.closest("[data-content-type]")) return false; const t = e.parentElement; return ( t === null ? false : t.tagName === "TD" || t.tagName === "TH" ? {} : false ); }, node: "tableParagraph", }, ]; }, renderHTML({ HTMLAttributes: e }) { return ["p", e, 0]; }, }), Tn$1 = Node3.create({ name: "tableRow", addOptions() { return { HTMLAttributes: {}, }; }, content: "(tableCell | tableHeader)+", tableRole: "row", marks: "deletion insertion modification", parseHTML() { return [{ tag: "tr" }]; }, renderHTML({ HTMLAttributes: e }) { return ["tr", mergeAttributes(this.options.HTMLAttributes, e), 0]; }, }); function _e$1(e, t) { const o = DOMParser$1.fromSchema(t).parse(e, { topNode: t.nodes.blockGroup.create(), }), r = []; return ( o.content.descendants((a) => { if (a.isInline) return (r.push(a), false); }), Fragment.fromArray(r) ); } const Bn$1 = () => vt({ node: wn$1, type: "table", content: "table" }, Sn$1, [ a$1({ key: "table-extensions", tiptapExtensions: [En$1, Ln$1, xn$1, Mn$1, Tn$1], }), // Extension for keyboard shortcut which deletes the table if it's empty // and all cells are selected. Uses a separate extension as it needs // priority over keyboard handlers in the `TableExtension`'s // `tableEditing` plugin. a$1({ key: "table-keyboard-delete", keyboardShortcuts: { Backspace: ({ editor: e }) => { if (!(e.prosemirrorState.selection instanceof CellSelection)) return false; const t = e.getTextCursorPosition().block, n = t.content; let o = 0; for (const a of n.rows) for (const s of a.cells) { if (("type" in s && s.content.length > 0) || (!("type" in s) && s.length > 0)) return false; o++; } let r = 0; return ( e.prosemirrorState.selection.forEachCell(() => { r++; }), r < o ? false : ( (e.transact(() => { ((e.getPrevBlock(t) || e.getNextBlock(t)) && e.setTextCursorPosition(t), e.removeBlocks([t])); }), true) ) ); }, }, }), ]), be = (e) => { const t = e.src || void 0, n = e.width || void 0; return { url: t, previewWidth: n }; }, An$1 = '', Nn$1 = (e) => ({ type: "video", propSchema: { textAlignment: m$1.textAlignment, backgroundColor: m$1.backgroundColor, name: { default: "" }, url: { default: "" }, caption: { default: "" }, showPreview: { default: true }, previewWidth: { default: void 0, type: "number" }, }, content: "none", }), Pn$1 = (e) => (t) => { if (t.tagName === "VIDEO") { if (t.closest("figure")) return; const { backgroundColor: n } = C(t); return { ...be(t), backgroundColor: n, }; } if (t.tagName === "FIGURE") { const n = $$2(t, "video"); if (!n) return; const { targetElement: o, caption: r } = n, { backgroundColor: a } = C(t); return { ...be(o), backgroundColor: a, caption: r, }; } }, In$1 = v$1(Nn$1, (e) => ({ meta: { fileBlockAccept: ["video/*"], }, parse: Pn$1(), render(t, n) { const o = document.createElement("div"); o.innerHTML = e.icon ?? An$1; const r = document.createElement("div"); r.className = "bn-visual-media-wrapper"; const a = document.createElement("video"); return ( (a.className = "bn-visual-media"), n.resolveFileUrl ? n.resolveFileUrl(t.props.url).then((s) => { a.src = s; }) : (a.src = t.props.url), (a.controls = true), (a.contentEditable = "false"), (a.draggable = false), (a.width = t.props.previewWidth), r.appendChild(a), Ie$2(t, n, { dom: r }, r, o.firstElementChild) ); }, toExternalHTML(t) { if (!t.props.url) { const o = document.createElement("p"); return ( (o.textContent = "Add video"), { dom: o, } ); } let n; return ( t.props.showPreview ? ((n = document.createElement("video")), (n.src = t.props.url), t.props.previewWidth && (n.width = t.props.previewWidth)) : ((n = document.createElement("a")), (n.href = t.props.url), (n.textContent = t.props.name || t.props.url)), t.props.caption ? t.props.showPreview ? Y(n, t.props.caption) : U(n, t.props.caption) : { dom: n, } ); }, runsBefore: ["file"], })); function b$2(e, t, n) { if (!(t in e.schema.blockSpecs)) return false; if (!n) return true; for (const [o, r] of Object.entries(n)) { if (!(o in e.schema.blockSpecs[t].config.propSchema)) return false; if (typeof r == "string") { if ( (e.schema.blockSpecs[t].config.propSchema[o].default !== void 0 && typeof e.schema.blockSpecs[t].config.propSchema[o].default !== r) || (e.schema.blockSpecs[t].config.propSchema[o].type !== void 0 && e.schema.blockSpecs[t].config.propSchema[o].type !== r) ) return false; } else { if ( e.schema.blockSpecs[t].config.propSchema[o].default !== r.default || (e.schema.blockSpecs[t].config.propSchema[o].default === void 0 && r.default === void 0 && e.schema.blockSpecs[t].config.propSchema[o].type !== r.type) || typeof e.schema.blockSpecs[t].config.propSchema[o].values != typeof r.values ) return false; if (typeof e.schema.blockSpecs[t].config.propSchema[o].values == "object" && typeof r.values == "object") { for (const a of r.values) if (!e.schema.blockSpecs[t].config.propSchema[o].values.includes(a)) return false; } } } return true; } function To$1(e, t, n, o) { return b$2(t, n, o) && e.type === n; } function Bo$1(e) { return e instanceof CellSelection; } const R = /* @__PURE__ */ new Map(); function Hn$1(e) { if (R.has(e)) return R.get(e); const t = new Mapping(); return ( e._tiptapEditor.on("transaction", ({ transaction: n }) => { t.appendMapping(n.mapping); }), e._tiptapEditor.on("destroy", () => { R.delete(e); }), R.set(e, t), t ); } function Dn$1(e, t, n = "left") { const o = ySyncPluginKey.getState(e.prosemirrorState); if (!o) { const a = Hn$1(e), s = a.maps.length; return () => a.slice(s).map(t, n === "left" ? -1 : 1); } const r = absolutePositionToRelativePosition( // Track the position after the position if we are on the right side t + (n === "right" ? 1 : -1), o.binding.type, o.binding.mapping, ); return () => { const a = ySyncPluginKey.getState(e.prosemirrorState), s = relativePositionToAbsolutePosition(a.doc, a.binding.type, r, a.binding.mapping); if (s === null) throw new Error("Position not found, cannot track positions"); return s + (n === "right" ? -1 : 1); }; } const On$1 = findParentNode((e) => e.type.name === "blockContainer"); let _n$1 = class _n { constructor(t, n, o) { w(this, "state"); w(this, "emitUpdate"); w(this, "rootEl"); w(this, "pluginState"); w(this, "handleScroll", () => { var t2, n2; if ((t2 = this.state) != null && t2.show) { const o2 = (n2 = this.rootEl) == null ? void 0 : n2.querySelector(`[data-decoration-id="${this.pluginState.decorationId}"]`); if (!o2) return; ((this.state.referencePos = o2.getBoundingClientRect().toJSON()), this.emitUpdate(this.pluginState.triggerCharacter)); } }); w(this, "closeMenu", () => { this.editor.transact((t2) => t2.setMeta(B$1, null)); }); w(this, "clearQuery", () => { this.pluginState !== void 0 && this.editor._tiptapEditor .chain() .focus() .deleteRange({ from: this.pluginState.queryStartPos() - (this.pluginState.deleteTriggerCharacter ? this.pluginState.triggerCharacter.length : 0), to: this.editor.transact((t2) => t2.selection.from), }) .run(); }); var r; ((this.editor = t), (this.pluginState = void 0), (this.emitUpdate = (a) => { var s; if (!this.state) throw new Error("Attempting to update uninitialized suggestions menu"); n(a, { ...this.state, ignoreQueryLength: (s = this.pluginState) == null ? void 0 : s.ignoreQueryLength, }); }), (this.rootEl = o.root), (r = this.rootEl) == null || r.addEventListener("scroll", this.handleScroll, true)); } update(t, n) { var l; const o = B$1.getState(n), r = B$1.getState(t.state), a = o === void 0 && r !== void 0, s = o !== void 0 && r === void 0; if (!a && !(o !== void 0 && r !== void 0) && !s) return; if (((this.pluginState = s ? o : r), s || !this.editor.isEditable)) { (this.state && (this.state.show = false), this.emitUpdate(this.pluginState.triggerCharacter)); return; } const c = (l = this.rootEl) == null ? void 0 : l.querySelector(`[data-decoration-id="${this.pluginState.decorationId}"]`); this.editor.isEditable && c && ((this.state = { show: true, referencePos: c.getBoundingClientRect().toJSON(), query: this.pluginState.query, }), this.emitUpdate(this.pluginState.triggerCharacter)); } destroy() { var t; (t = this.rootEl) == null || t.removeEventListener("scroll", this.handleScroll, true); } }; const B$1 = new PluginKey("SuggestionMenuPlugin"), Vn = a$1(({ editor: e }) => { const t = /* @__PURE__ */ new Map(); let n; const o = f$2(void 0); return { key: "suggestionMenu", store: o, addSuggestionMenu: (r) => { t.set(r.triggerCharacter, r); }, removeSuggestionMenu: (r) => { t.delete(r); }, closeMenu: () => { n == null || n.closeMenu(); }, clearQuery: () => { n == null || n.clearQuery(); }, shown: () => { var r; return ((r = n == null ? void 0 : n.state) == null ? void 0 : r.show) || false; }, openSuggestionMenu: (r, a) => { e.headless || (e.focus(), e.transact((s) => { (a != null && a.deleteTriggerCharacter && s.insertText(r), s.scrollIntoView().setMeta(B$1, { triggerCharacter: r, deleteTriggerCharacter: (a == null ? void 0 : a.deleteTriggerCharacter) || false, ignoreQueryLength: (a == null ? void 0 : a.ignoreQueryLength) || false, })); })); }, // TODO this whole plugin needs to be refactored (but I've done the minimal) prosemirrorPlugins: [ new Plugin({ key: B$1, view: (r) => ( (n = new _n$1( e, (a, s) => { o.setState({ ...s, triggerCharacter: a }); }, r, )), n ), state: { // Initialize the plugin's internal state. init() {}, // Apply changes to the plugin state from an editor transaction. apply: (r, a, s, i) => { if (r.selection.$from.parent.type.spec.code) return a; const c = r.getMeta(B$1); if (typeof c == "object" && c !== null) { a && (n == null || n.closeMenu()); const u = Dn$1( e, i.selection.from - // Need to account for the trigger char that was inserted, so we offset the position by the length of the trigger character. c.triggerCharacter.length, ); return { triggerCharacter: c.triggerCharacter, deleteTriggerCharacter: c.deleteTriggerCharacter !== false, // When reading the queryStartPos, we offset the result by the length of the trigger character, to make it easy on the caller queryStartPos: () => u() + c.triggerCharacter.length, query: "", decorationId: `id_${Math.floor(Math.random() * 4294967295)}`, ignoreQueryLength: c == null ? void 0 : c.ignoreQueryLength, }; } if (a === void 0) return a; if ( // Highlighting text should hide the menu. i.selection.from !== i.selection.to || // Transactions with plugin metadata should hide the menu. c === null || // Certain mouse events should hide the menu. // TODO: Change to global mousedown listener. r.getMeta("focus") || r.getMeta("blur") || r.getMeta("pointer") || // Moving the caret before the character which triggered the menu should hide it. (a.triggerCharacter !== void 0 && i.selection.from < a.queryStartPos()) || // Moving the caret to a new block should hide the menu. !i.selection.$from.sameParent(i.doc.resolve(a.queryStartPos())) ) return; const l = { ...a }; return ((l.query = i.doc.textBetween(a.queryStartPos(), i.selection.from)), l); }, }, props: { handleTextInput(r, a, s, i) { if (a === s) { const c = r.state.doc; for (const [l, u] of t) { const d = l.length > 1 ? c.textBetween(a - l.length, a) + i : i; if (l === d) { if (u.shouldOpen && !u.shouldOpen(r.state.tr)) continue; return ( r.dispatch(r.state.tr.insertText(i)), r.dispatch( r.state.tr .setMeta(B$1, { triggerCharacter: d, }) .scrollIntoView(), ), true ); } } } return false; }, // Setup decorator on the currently active suggestion. decorations(r) { const a = this.getState(r); if (a === void 0) return null; if (!a.deleteTriggerCharacter) { const s = On$1(r.selection); if (s) return DecorationSet.create(r.doc, [ Decoration.node(s.pos, s.pos + s.node.nodeSize, { "nodeName": "span", "class": "bn-suggestion-decorator", "data-decoration-id": a.decorationId, }), ]); } return DecorationSet.create(r.doc, [ Decoration.inline(a.queryStartPos() - a.triggerCharacter.length, a.queryStartPos(), { "nodeName": "span", "class": "bn-suggestion-decorator", "data-decoration-id": a.decorationId, }), ]); }, }, }), ], }; }); function Wn(e) { let t = e.getTextCursorPosition().block, n = e.schema.blockSchema[t.type].content; for (; n === "none"; ) { if (((t = e.getTextCursorPosition().nextBlock), t === void 0)) return; ((n = e.schema.blockSchema[t.type].content), e.setTextCursorPosition(t, "end")); } } function k$3(e, t) { const n = e.getTextCursorPosition().block; if (n.content === void 0) throw new Error("Slash Menu open in a block that doesn't contain content."); let o; return ( Array.isArray(n.content) && ((n.content.length === 1 && B$2(n.content[0]) && n.content[0].type === "text" && n.content[0].text === "/") || n.content.length === 0) ? ((o = e.updateBlock(n, t)), e.setTextCursorPosition(o)) : ((o = e.insertBlocks([t], n, "after")[0]), e.setTextCursorPosition(e.getTextCursorPosition().nextBlock)), Wn(e), o ); } function Ao$2(e) { const t = []; return ( b$2(e, "heading", { level: "number" }) && (e.schema.blockSchema.heading.propSchema.level.values || []) .filter((n) => n <= 3) .forEach((n) => { t.push({ onItemClick: () => { k$3(e, { type: "heading", props: { level: n }, }); }, badge: L$1(`Mod-Alt-${n}`), key: n === 1 ? "heading" : `heading_${n}`, ...e.dictionary.slash_menu[n === 1 ? "heading" : `heading_${n}`], }); }), b$2(e, "quote") && t.push({ onItemClick: () => { k$3(e, { type: "quote", }); }, key: "quote", ...e.dictionary.slash_menu.quote, }), b$2(e, "toggleListItem") && t.push({ onItemClick: () => { k$3(e, { type: "toggleListItem", }); }, badge: L$1("Mod-Shift-6"), key: "toggle_list", ...e.dictionary.slash_menu.toggle_list, }), b$2(e, "numberedListItem") && t.push({ onItemClick: () => { k$3(e, { type: "numberedListItem", }); }, badge: L$1("Mod-Shift-7"), key: "numbered_list", ...e.dictionary.slash_menu.numbered_list, }), b$2(e, "bulletListItem") && t.push({ onItemClick: () => { k$3(e, { type: "bulletListItem", }); }, badge: L$1("Mod-Shift-8"), key: "bullet_list", ...e.dictionary.slash_menu.bullet_list, }), b$2(e, "checkListItem") && t.push({ onItemClick: () => { k$3(e, { type: "checkListItem", }); }, badge: L$1("Mod-Shift-9"), key: "check_list", ...e.dictionary.slash_menu.check_list, }), b$2(e, "paragraph") && t.push({ onItemClick: () => { k$3(e, { type: "paragraph", }); }, badge: L$1("Mod-Alt-0"), key: "paragraph", ...e.dictionary.slash_menu.paragraph, }), b$2(e, "codeBlock") && t.push({ onItemClick: () => { k$3(e, { type: "codeBlock", }); }, badge: L$1("Mod-Alt-c"), key: "code_block", ...e.dictionary.slash_menu.code_block, }), b$2(e, "divider") && t.push({ onItemClick: () => { k$3(e, { type: "divider" }); }, key: "divider", ...e.dictionary.slash_menu.divider, }), b$2(e, "table") && t.push({ onItemClick: () => { k$3(e, { type: "table", content: { type: "tableContent", rows: [ { cells: ["", "", ""], }, { cells: ["", "", ""], }, ], }, }); }, badge: void 0, key: "table", ...e.dictionary.slash_menu.table, }), b$2(e, "image", { url: "string" }) && t.push({ onItemClick: () => { var o; const n = k$3(e, { type: "image", }); (o = e.getExtension(H$1)) == null || o.showMenu(n.id); }, key: "image", ...e.dictionary.slash_menu.image, }), b$2(e, "video", { url: "string" }) && t.push({ onItemClick: () => { var o; const n = k$3(e, { type: "video", }); (o = e.getExtension(H$1)) == null || o.showMenu(n.id); }, key: "video", ...e.dictionary.slash_menu.video, }), b$2(e, "audio", { url: "string" }) && t.push({ onItemClick: () => { var o; const n = k$3(e, { type: "audio", }); (o = e.getExtension(H$1)) == null || o.showMenu(n.id); }, key: "audio", ...e.dictionary.slash_menu.audio, }), b$2(e, "file", { url: "string" }) && t.push({ onItemClick: () => { var o; const n = k$3(e, { type: "file", }); (o = e.getExtension(H$1)) == null || o.showMenu(n.id); }, key: "file", ...e.dictionary.slash_menu.file, }), b$2(e, "heading", { level: "number", isToggleable: "boolean", }) && (e.schema.blockSchema.heading.propSchema.level.values || []) .filter((n) => n <= 3) .forEach((n) => { t.push({ onItemClick: () => { k$3(e, { type: "heading", props: { level: n, isToggleable: true }, }); }, key: n === 1 ? "toggle_heading" : `toggle_heading_${n}`, ...e.dictionary.slash_menu[n === 1 ? "toggle_heading" : `toggle_heading_${n}`], }); }), b$2(e, "heading", { level: "number" }) && (e.schema.blockSchema.heading.propSchema.level.values || []) .filter((n) => n > 3) .forEach((n) => { t.push({ onItemClick: () => { k$3(e, { type: "heading", props: { level: n }, }); }, badge: L$1(`Mod-Alt-${n}`), key: `heading_${n}`, ...e.dictionary.slash_menu[`heading_${n}`], }); }), t.push({ onItemClick: () => { var n; (n = e.getExtension(Vn)) == null || n.openSuggestionMenu(":", { deleteTriggerCharacter: true, ignoreQueryLength: true, }); }, key: "emoji", ...e.dictionary.slash_menu.emoji, }), t ); } function No$2(e, t) { return e.filter(({ title: n, aliases: o }) => n.toLowerCase().includes(t.toLowerCase()) || (o && o.filter((r) => r.toLowerCase().includes(t.toLowerCase())).length !== 0)); } const Po$2 = { audio: $t$1(), bulletListItem: ln$1(), checkListItem: un$1(), codeBlock: jt$1(), divider: zt$1(), file: Kt$1(), heading: en$1(), image: sn$1(), numberedListItem: mn$1(), paragraph: Cn$1(), quote: yn$1(), table: Bn$1(), toggleListItem: gn$1(), video: In$1(), }, Rn = Te$2( { type: "textColor", propSchema: "string", }, { render: () => { const e = document.createElement("span"); return { dom: e, contentDOM: e, }; }, toExternalHTML: (e) => { const t = document.createElement("span"); return ( e !== m$1.textColor.default && (t.style.color = e in T ? T[e].text : e), { dom: t, contentDOM: t, } ); }, parse: (e) => { if (e.tagName === "SPAN" && e.style.color) return e.style.color; }, }, ), Fn$1 = Te$2( { type: "backgroundColor", propSchema: "string", }, { render: () => { const e = document.createElement("span"); return { dom: e, contentDOM: e, }; }, toExternalHTML: (e) => { const t = document.createElement("span"); return ( e !== m$1.backgroundColor.default && (t.style.backgroundColor = e in T ? T[e].background : e), { dom: t, contentDOM: t, } ); }, parse: (e) => { if (e.tagName === "SPAN" && e.style.backgroundColor) return e.style.backgroundColor; }, }, ), $n$1 = { bold: I$1(index_default$4, "boolean"), italic: I$1(index_default$3, "boolean"), underline: I$1(index_default$2, "boolean"), strike: I$1(index_default$1, "boolean"), code: I$1(index_default, "boolean"), textColor: Rn, backgroundColor: Fn$1, }; Lt$1($n$1); const Un = { text: { config: "text", implementation: {} }, link: { config: "link", implementation: {} }, }, Ho$2 = Mt$1(Un); var fastDeepEqual; var hasRequiredFastDeepEqual; function requireFastDeepEqual() { if (hasRequiredFastDeepEqual) return fastDeepEqual; hasRequiredFastDeepEqual = 1; // do not edit .js files directly - edit src/index.jst fastDeepEqual = function equal(a, b) { if (a === b) return true; if (a && b && typeof a == "object" && typeof b == "object") { if (a.constructor !== b.constructor) return false; var length, i, keys; if (Array.isArray(a)) { length = a.length; if (length != b.length) return false; for (i = length; i-- !== 0; ) if (!equal(a[i], b[i])) return false; return true; } if (a.constructor === RegExp) return a.source === b.source && a.flags === b.flags; if (a.valueOf !== Object.prototype.valueOf) return a.valueOf() === b.valueOf(); if (a.toString !== Object.prototype.toString) return a.toString() === b.toString(); keys = Object.keys(a); length = keys.length; if (length !== Object.keys(b).length) return false; for (i = length; i-- !== 0; ) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; for (i = length; i-- !== 0; ) { var key = keys[i]; if (!equal(a[key], b[key])) return false; } return true; } // true if both NaN, false otherwise return a !== a && b !== b; }; return fastDeepEqual; } var fastDeepEqualExports = requireFastDeepEqual(); const He = /*@__PURE__*/ getDefaultExportFromCjs(fastDeepEqualExports); function ok$1() {} function unreachable() {} /** * @import {Schema as SchemaType, Space} from 'property-information' */ /** @type {SchemaType} */ class Schema { /** * @param {SchemaType['property']} property * Property. * @param {SchemaType['normal']} normal * Normal. * @param {Space | undefined} [space] * Space. * @returns * Schema. */ constructor(property, normal, space) { this.normal = normal; this.property = property; if (space) { this.space = space; } } } Schema.prototype.normal = {}; Schema.prototype.property = {}; Schema.prototype.space = undefined; /** * @import {Info, Space} from 'property-information' */ /** * @param {ReadonlyArray} definitions * Definitions. * @param {Space | undefined} [space] * Space. * @returns {Schema} * Schema. */ function merge(definitions, space) { /** @type {Record} */ const property = {}; /** @type {Record} */ const normal = {}; for (const definition of definitions) { Object.assign(property, definition.property); Object.assign(normal, definition.normal); } return new Schema(property, normal, space); } /** * Get the cleaned case insensitive form of an attribute or property. * * @param {string} value * An attribute-like or property-like name. * @returns {string} * Value that can be used to look up the properly cased property on a * `Schema`. */ function normalize$1(value) { return value.toLowerCase(); } /** * @import {Info as InfoType} from 'property-information' */ /** @type {InfoType} */ class Info { /** * @param {string} property * Property. * @param {string} attribute * Attribute. * @returns * Info. */ constructor(property, attribute) { this.attribute = attribute; this.property = property; } } Info.prototype.attribute = ""; Info.prototype.booleanish = false; Info.prototype.boolean = false; Info.prototype.commaOrSpaceSeparated = false; Info.prototype.commaSeparated = false; Info.prototype.defined = false; Info.prototype.mustUseProperty = false; Info.prototype.number = false; Info.prototype.overloadedBoolean = false; Info.prototype.property = ""; Info.prototype.spaceSeparated = false; Info.prototype.space = undefined; let powers = 0; const boolean = increment(); const booleanish = increment(); const overloadedBoolean = increment(); const number = increment(); const spaceSeparated = increment(); const commaSeparated = increment(); const commaOrSpaceSeparated = increment(); function increment() { return 2 ** ++powers; } const types = /*#__PURE__*/ Object.freeze( /*#__PURE__*/ Object.defineProperty( { __proto__: null, boolean, booleanish, commaOrSpaceSeparated, commaSeparated, number, overloadedBoolean, spaceSeparated, }, Symbol.toStringTag, { value: "Module" }, ), ); /** * @import {Space} from 'property-information' */ const checks = /** @type {ReadonlyArray} */ (Object.keys(types)); class DefinedInfo extends Info { /** * @constructor * @param {string} property * Property. * @param {string} attribute * Attribute. * @param {number | null | undefined} [mask] * Mask. * @param {Space | undefined} [space] * Space. * @returns * Info. */ constructor(property, attribute, mask, space) { let index = -1; super(property, attribute); mark(this, "space", space); if (typeof mask === "number") { while (++index < checks.length) { const check = checks[index]; mark(this, checks[index], (mask & types[check]) === types[check]); } } } } DefinedInfo.prototype.defined = true; /** * @template {keyof DefinedInfo} Key * Key type. * @param {DefinedInfo} values * Info. * @param {Key} key * Key. * @param {DefinedInfo[Key]} value * Value. * @returns {undefined} * Nothing. */ function mark(values, key, value) { if (value) { values[key] = value; } } /** * @import {Info, Space} from 'property-information' */ /** * @param {Definition} definition * Definition. * @returns {Schema} * Schema. */ function create$3(definition) { /** @type {Record} */ const properties = {}; /** @type {Record} */ const normals = {}; for (const [property, value] of Object.entries(definition.properties)) { const info = new DefinedInfo(property, definition.transform(definition.attributes || {}, property), value, definition.space); if (definition.mustUseProperty && definition.mustUseProperty.includes(property)) { info.mustUseProperty = true; } properties[property] = info; normals[normalize$1(property)] = property; normals[normalize$1(info.attribute)] = property; } return new Schema(properties, normals, definition.space); } const aria = create$3({ properties: { ariaActiveDescendant: null, ariaAtomic: booleanish, ariaAutoComplete: null, ariaBusy: booleanish, ariaChecked: booleanish, ariaColCount: number, ariaColIndex: number, ariaColSpan: number, ariaControls: spaceSeparated, ariaCurrent: null, ariaDescribedBy: spaceSeparated, ariaDetails: null, ariaDisabled: booleanish, ariaDropEffect: spaceSeparated, ariaErrorMessage: null, ariaExpanded: booleanish, ariaFlowTo: spaceSeparated, ariaGrabbed: booleanish, ariaHasPopup: null, ariaHidden: booleanish, ariaInvalid: null, ariaKeyShortcuts: null, ariaLabel: null, ariaLabelledBy: spaceSeparated, ariaLevel: number, ariaLive: null, ariaModal: booleanish, ariaMultiLine: booleanish, ariaMultiSelectable: booleanish, ariaOrientation: null, ariaOwns: spaceSeparated, ariaPlaceholder: null, ariaPosInSet: number, ariaPressed: booleanish, ariaReadOnly: booleanish, ariaRelevant: null, ariaRequired: booleanish, ariaRoleDescription: spaceSeparated, ariaRowCount: number, ariaRowIndex: number, ariaRowSpan: number, ariaSelected: booleanish, ariaSetSize: number, ariaSort: null, ariaValueMax: number, ariaValueMin: number, ariaValueNow: number, ariaValueText: null, role: null, }, transform(_, property) { return property === "role" ? property : "aria-" + property.slice(4).toLowerCase(); }, }); /** * @param {Record} attributes * Attributes. * @param {string} attribute * Attribute. * @returns {string} * Transformed attribute. */ function caseSensitiveTransform(attributes, attribute) { return attribute in attributes ? attributes[attribute] : attribute; } /** * @param {Record} attributes * Attributes. * @param {string} property * Property. * @returns {string} * Transformed property. */ function caseInsensitiveTransform(attributes, property) { return caseSensitiveTransform(attributes, property.toLowerCase()); } const html$5 = create$3({ attributes: { acceptcharset: "accept-charset", classname: "class", htmlfor: "for", httpequiv: "http-equiv", }, mustUseProperty: ["checked", "multiple", "muted", "selected"], properties: { // Standard Properties. abbr: null, accept: commaSeparated, acceptCharset: spaceSeparated, accessKey: spaceSeparated, action: null, allow: null, allowFullScreen: boolean, allowPaymentRequest: boolean, allowUserMedia: boolean, alpha: boolean, alt: null, as: null, async: boolean, autoCapitalize: null, autoComplete: spaceSeparated, autoFocus: boolean, autoPlay: boolean, blocking: spaceSeparated, capture: null, charSet: null, checked: boolean, cite: null, className: spaceSeparated, closedBy: null, colorSpace: null, cols: number, colSpan: number, command: null, commandFor: null, content: null, contentEditable: booleanish, controls: boolean, controlsList: spaceSeparated, coords: number | commaSeparated, crossOrigin: null, data: null, dateTime: null, decoding: null, default: boolean, defer: boolean, dir: null, dirName: null, disabled: boolean, download: overloadedBoolean, draggable: booleanish, encType: null, enterKeyHint: null, fetchPriority: null, form: null, formAction: null, formEncType: null, formMethod: null, formNoValidate: boolean, formTarget: null, headers: spaceSeparated, height: number, hidden: overloadedBoolean, high: number, href: null, hrefLang: null, htmlFor: spaceSeparated, httpEquiv: spaceSeparated, id: null, imageSizes: null, imageSrcSet: null, inert: boolean, inputMode: null, integrity: null, is: null, isMap: boolean, itemId: null, itemProp: spaceSeparated, itemRef: spaceSeparated, itemScope: boolean, itemType: spaceSeparated, kind: null, label: null, lang: null, language: null, list: null, loading: null, loop: boolean, low: number, manifest: null, max: null, maxLength: number, media: null, method: null, min: null, minLength: number, multiple: boolean, muted: boolean, name: null, nonce: null, noModule: boolean, noValidate: boolean, onAbort: null, onAfterPrint: null, onAuxClick: null, onBeforeMatch: null, onBeforePrint: null, onBeforeToggle: null, onBeforeUnload: null, onBlur: null, onCancel: null, onCanPlay: null, onCanPlayThrough: null, onChange: null, onClick: null, onClose: null, onContextLost: null, onContextMenu: null, onContextRestored: null, onCopy: null, onCueChange: null, onCut: null, onDblClick: null, onDrag: null, onDragEnd: null, onDragEnter: null, onDragExit: null, onDragLeave: null, onDragOver: null, onDragStart: null, onDrop: null, onDurationChange: null, onEmptied: null, onEnded: null, onError: null, onFocus: null, onFormData: null, onHashChange: null, onInput: null, onInvalid: null, onKeyDown: null, onKeyPress: null, onKeyUp: null, onLanguageChange: null, onLoad: null, onLoadedData: null, onLoadedMetadata: null, onLoadEnd: null, onLoadStart: null, onMessage: null, onMessageError: null, onMouseDown: null, onMouseEnter: null, onMouseLeave: null, onMouseMove: null, onMouseOut: null, onMouseOver: null, onMouseUp: null, onOffline: null, onOnline: null, onPageHide: null, onPageShow: null, onPaste: null, onPause: null, onPlay: null, onPlaying: null, onPopState: null, onProgress: null, onRateChange: null, onRejectionHandled: null, onReset: null, onResize: null, onScroll: null, onScrollEnd: null, onSecurityPolicyViolation: null, onSeeked: null, onSeeking: null, onSelect: null, onSlotChange: null, onStalled: null, onStorage: null, onSubmit: null, onSuspend: null, onTimeUpdate: null, onToggle: null, onUnhandledRejection: null, onUnload: null, onVolumeChange: null, onWaiting: null, onWheel: null, open: boolean, optimum: number, pattern: null, ping: spaceSeparated, placeholder: null, playsInline: boolean, popover: null, popoverTarget: null, popoverTargetAction: null, poster: null, preload: null, readOnly: boolean, referrerPolicy: null, rel: spaceSeparated, required: boolean, reversed: boolean, rows: number, rowSpan: number, sandbox: spaceSeparated, scope: null, scoped: boolean, seamless: boolean, selected: boolean, shadowRootClonable: boolean, shadowRootCustomElementRegistry: boolean, shadowRootDelegatesFocus: boolean, shadowRootMode: null, shadowRootSerializable: boolean, shape: null, size: number, sizes: null, slot: null, span: number, spellCheck: booleanish, src: null, srcDoc: null, srcLang: null, srcSet: null, start: number, step: null, style: null, tabIndex: number, target: null, title: null, translate: null, type: null, typeMustMatch: boolean, useMap: null, value: booleanish, width: number, wrap: null, writingSuggestions: null, // Legacy. // See: https://html.spec.whatwg.org/#other-elements,-attributes-and-apis align: null, // Several. Use CSS `text-align` instead, aLink: null, // ``. Use CSS `a:active {color}` instead archive: spaceSeparated, // ``. List of URIs to archives axis: null, // `` and ``. Use `scope` on `` background: null, // ``. Use CSS `background-image` instead bgColor: null, // `` and table elements. Use CSS `background-color` instead border: number, // ``. Use CSS `border-width` instead, borderColor: null, // `
`. Use CSS `border-color` instead, bottomMargin: number, // `` cellPadding: null, // `
` cellSpacing: null, // `
` char: null, // Several table elements. When `align=char`, sets the character to align on charOff: null, // Several table elements. When `char`, offsets the alignment classId: null, // `` clear: null, // `
`. Use CSS `clear` instead code: null, // `` codeBase: null, // `` codeType: null, // `` color: null, // `` and `
`. Use CSS instead compact: boolean, // Lists. Use CSS to reduce space between items instead declare: boolean, // `` event: null, // ` * ^ * ``` * * @type {State} */ function continuationRawTagOpen(code) { if (code === 47) { effects.consume(code); buffer = ""; return continuationRawEndTag; } return continuation(code); } /** * In raw continuation, after ` | * ^^^^^^ * ``` * * @type {State} */ function continuationRawEndTag(code) { if (code === 62) { const name = buffer.toLowerCase(); if (htmlRawNames.includes(name)) { effects.consume(code); return continuationClose; } return continuation(code); } if (asciiAlpha(code) && buffer.length < 8) { // Always the case. effects.consume(code); buffer += String.fromCharCode(code); return continuationRawEndTag; } return continuation(code); } /** * In cdata continuation, after `]`, expecting `]>`. * * ```markdown * > | &<]]> * ^ * ``` * * @type {State} */ function continuationCdataInside(code) { if (code === 93) { effects.consume(code); return continuationDeclarationInside; } return continuation(code); } /** * In declaration or instruction continuation, at `>`. * * ```markdown * > | * ^ * > | * ^ * > | * ^ * > | * ^ * > | &<]]> * ^ * ``` * * @type {State} */ function continuationDeclarationInside(code) { if (code === 62) { effects.consume(code); return continuationClose; } // More dashes. if (code === 45 && marker === 2) { effects.consume(code); return continuationDeclarationInside; } return continuation(code); } /** * In closed continuation: everything we get until the eol/eof is part of it. * * ```markdown * > | * ^ * ``` * * @type {State} */ function continuationClose(code) { if (code === null || markdownLineEnding(code)) { effects.exit("htmlFlowData"); return continuationAfter(code); } effects.consume(code); return continuationClose; } /** * Done. * * ```markdown * > | * ^ * ``` * * @type {State} */ function continuationAfter(code) { effects.exit("htmlFlow"); // // Feel free to interrupt. // tokenizer.interrupt = false // // No longer concrete. // tokenizer.concrete = false return ok(code); } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeNonLazyContinuationStart(effects, ok, nok) { const self = this; return start; /** * At eol, before continuation. * * ```markdown * > | * ```js * ^ * | b * ``` * * @type {State} */ function start(code) { if (markdownLineEnding(code)) { effects.enter("lineEnding"); effects.consume(code); effects.exit("lineEnding"); return after; } return nok(code); } /** * A continuation. * * ```markdown * | * ```js * > | b * ^ * ``` * * @type {State} */ function after(code) { return self.parser.lazy[self.now().line] ? nok(code) : ok(code); } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeBlankLineBefore(effects, ok, nok) { return start; /** * Before eol, expecting blank line. * * ```markdown * > |
* ^ * | * ``` * * @type {State} */ function start(code) { effects.enter("lineEnding"); effects.consume(code); effects.exit("lineEnding"); return effects.attempt(blankLine, ok, nok); } } /** * @import { * Code, * Construct, * State, * TokenizeContext, * Tokenizer * } from 'micromark-util-types' */ /** @type {Construct} */ const htmlText = { name: "htmlText", tokenize: tokenizeHtmlText, }; /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeHtmlText(effects, ok, nok) { const self = this; /** @type {NonNullable | undefined} */ let marker; /** @type {number} */ let index; /** @type {State} */ let returnState; return start; /** * Start of HTML (text). * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function start(code) { effects.enter("htmlText"); effects.enter("htmlTextData"); effects.consume(code); return open; } /** * After `<`, at tag name or other stuff. * * ```markdown * > | a c * ^ * > | a c * ^ * > | a c * ^ * ``` * * @type {State} */ function open(code) { if (code === 33) { effects.consume(code); return declarationOpen; } if (code === 47) { effects.consume(code); return tagCloseStart; } if (code === 63) { effects.consume(code); return instruction; } // ASCII alphabetical. if (asciiAlpha(code)) { effects.consume(code); return tagOpen; } return nok(code); } /** * After ` | a c * ^ * > | a c * ^ * > | a &<]]> c * ^ * ``` * * @type {State} */ function declarationOpen(code) { if (code === 45) { effects.consume(code); return commentOpenInside; } if (code === 91) { effects.consume(code); index = 0; return cdataOpenInside; } if (asciiAlpha(code)) { effects.consume(code); return declaration; } return nok(code); } /** * In a comment, after ` | a c * ^ * ``` * * @type {State} */ function commentOpenInside(code) { if (code === 45) { effects.consume(code); return commentEnd; } return nok(code); } /** * In comment. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function comment(code) { if (code === null) { return nok(code); } if (code === 45) { effects.consume(code); return commentClose; } if (markdownLineEnding(code)) { returnState = comment; return lineEndingBefore(code); } effects.consume(code); return comment; } /** * In comment, after `-`. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function commentClose(code) { if (code === 45) { effects.consume(code); return commentEnd; } return comment(code); } /** * In comment, after `--`. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function commentEnd(code) { return ( code === 62 ? end(code) : code === 45 ? commentClose(code) : comment(code) ); } /** * After ` | a &<]]> b * ^^^^^^ * ``` * * @type {State} */ function cdataOpenInside(code) { const value = "CDATA["; if (code === value.charCodeAt(index++)) { effects.consume(code); return index === value.length ? cdata : cdataOpenInside; } return nok(code); } /** * In CDATA. * * ```markdown * > | a &<]]> b * ^^^ * ``` * * @type {State} */ function cdata(code) { if (code === null) { return nok(code); } if (code === 93) { effects.consume(code); return cdataClose; } if (markdownLineEnding(code)) { returnState = cdata; return lineEndingBefore(code); } effects.consume(code); return cdata; } /** * In CDATA, after `]`, at another `]`. * * ```markdown * > | a &<]]> b * ^ * ``` * * @type {State} */ function cdataClose(code) { if (code === 93) { effects.consume(code); return cdataEnd; } return cdata(code); } /** * In CDATA, after `]]`, at `>`. * * ```markdown * > | a &<]]> b * ^ * ``` * * @type {State} */ function cdataEnd(code) { if (code === 62) { return end(code); } if (code === 93) { effects.consume(code); return cdataEnd; } return cdata(code); } /** * In declaration. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function declaration(code) { if (code === null || code === 62) { return end(code); } if (markdownLineEnding(code)) { returnState = declaration; return lineEndingBefore(code); } effects.consume(code); return declaration; } /** * In instruction. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function instruction(code) { if (code === null) { return nok(code); } if (code === 63) { effects.consume(code); return instructionClose; } if (markdownLineEnding(code)) { returnState = instruction; return lineEndingBefore(code); } effects.consume(code); return instruction; } /** * In instruction, after `?`, at `>`. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function instructionClose(code) { return code === 62 ? end(code) : instruction(code); } /** * After ` | a c * ^ * ``` * * @type {State} */ function tagCloseStart(code) { // ASCII alphabetical. if (asciiAlpha(code)) { effects.consume(code); return tagClose; } return nok(code); } /** * After ` | a c * ^ * ``` * * @type {State} */ function tagClose(code) { // ASCII alphanumerical and `-`. if (code === 45 || asciiAlphanumeric(code)) { effects.consume(code); return tagClose; } return tagCloseBetween(code); } /** * In closing tag, after tag name. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function tagCloseBetween(code) { if (markdownLineEnding(code)) { returnState = tagCloseBetween; return lineEndingBefore(code); } if (markdownSpace(code)) { effects.consume(code); return tagCloseBetween; } return end(code); } /** * After ` | a c * ^ * ``` * * @type {State} */ function tagOpen(code) { // ASCII alphanumerical and `-`. if (code === 45 || asciiAlphanumeric(code)) { effects.consume(code); return tagOpen; } if (code === 47 || code === 62 || markdownLineEndingOrSpace(code)) { return tagOpenBetween(code); } return nok(code); } /** * In opening tag, after tag name. * * ```markdown * > | a c * ^ * ``` * * @type {State} */ function tagOpenBetween(code) { if (code === 47) { effects.consume(code); return end; } // ASCII alphabetical and `:` and `_`. if (code === 58 || code === 95 || asciiAlpha(code)) { effects.consume(code); return tagOpenAttributeName; } if (markdownLineEnding(code)) { returnState = tagOpenBetween; return lineEndingBefore(code); } if (markdownSpace(code)) { effects.consume(code); return tagOpenBetween; } return end(code); } /** * In attribute name. * * ```markdown * > | a d * ^ * ``` * * @type {State} */ function tagOpenAttributeName(code) { // ASCII alphabetical and `-`, `.`, `:`, and `_`. if (code === 45 || code === 46 || code === 58 || code === 95 || asciiAlphanumeric(code)) { effects.consume(code); return tagOpenAttributeName; } return tagOpenAttributeNameAfter(code); } /** * After attribute name, before initializer, the end of the tag, or * whitespace. * * ```markdown * > | a d * ^ * ``` * * @type {State} */ function tagOpenAttributeNameAfter(code) { if (code === 61) { effects.consume(code); return tagOpenAttributeValueBefore; } if (markdownLineEnding(code)) { returnState = tagOpenAttributeNameAfter; return lineEndingBefore(code); } if (markdownSpace(code)) { effects.consume(code); return tagOpenAttributeNameAfter; } return tagOpenBetween(code); } /** * Before unquoted, double quoted, or single quoted attribute value, allowing * whitespace. * * ```markdown * > | a e * ^ * ``` * * @type {State} */ function tagOpenAttributeValueBefore(code) { if (code === null || code === 60 || code === 61 || code === 62 || code === 96) { return nok(code); } if (code === 34 || code === 39) { effects.consume(code); marker = code; return tagOpenAttributeValueQuoted; } if (markdownLineEnding(code)) { returnState = tagOpenAttributeValueBefore; return lineEndingBefore(code); } if (markdownSpace(code)) { effects.consume(code); return tagOpenAttributeValueBefore; } effects.consume(code); return tagOpenAttributeValueUnquoted; } /** * In double or single quoted attribute value. * * ```markdown * > | a e * ^ * ``` * * @type {State} */ function tagOpenAttributeValueQuoted(code) { if (code === marker) { effects.consume(code); marker = undefined; return tagOpenAttributeValueQuotedAfter; } if (code === null) { return nok(code); } if (markdownLineEnding(code)) { returnState = tagOpenAttributeValueQuoted; return lineEndingBefore(code); } effects.consume(code); return tagOpenAttributeValueQuoted; } /** * In unquoted attribute value. * * ```markdown * > | a e * ^ * ``` * * @type {State} */ function tagOpenAttributeValueUnquoted(code) { if (code === null || code === 34 || code === 39 || code === 60 || code === 61 || code === 96) { return nok(code); } if (code === 47 || code === 62 || markdownLineEndingOrSpace(code)) { return tagOpenBetween(code); } effects.consume(code); return tagOpenAttributeValueUnquoted; } /** * After double or single quoted attribute value, before whitespace or the end * of the tag. * * ```markdown * > | a e * ^ * ``` * * @type {State} */ function tagOpenAttributeValueQuotedAfter(code) { if (code === 47 || code === 62 || markdownLineEndingOrSpace(code)) { return tagOpenBetween(code); } return nok(code); } /** * In certain circumstances of a tag where only an `>` is allowed. * * ```markdown * > | a e * ^ * ``` * * @type {State} */ function end(code) { if (code === 62) { effects.consume(code); effects.exit("htmlTextData"); effects.exit("htmlText"); return ok; } return nok(code); } /** * At eol. * * > 👉 **Note**: we can’t have blank lines in text, so no need to worry about * > empty tokens. * * ```markdown * > | a * ``` * * @type {State} */ function lineEndingBefore(code) { effects.exit("htmlTextData"); effects.enter("lineEnding"); effects.consume(code); effects.exit("lineEnding"); return lineEndingAfter; } /** * After eol, at optional whitespace. * * > 👉 **Note**: we can’t have blank lines in text, so no need to worry about * > empty tokens. * * ```markdown * | a * ^ * ``` * * @type {State} */ function lineEndingAfter(code) { // Always populated by defaults. return markdownSpace(code) ? factorySpace(effects, lineEndingAfterPrefix, "linePrefix", self.parser.constructs.disable.null.includes("codeIndented") ? undefined : 4)(code) : lineEndingAfterPrefix(code); } /** * After eol, after optional whitespace. * * > 👉 **Note**: we can’t have blank lines in text, so no need to worry about * > empty tokens. * * ```markdown * | a * ^ * ``` * * @type {State} */ function lineEndingAfterPrefix(code) { effects.enter("htmlTextData"); return returnState(code); } } /** * @import { * Construct, * Event, * Resolver, * State, * TokenizeContext, * Tokenizer, * Token * } from 'micromark-util-types' */ /** @type {Construct} */ const labelEnd = { name: "labelEnd", resolveAll: resolveAllLabelEnd, resolveTo: resolveToLabelEnd, tokenize: tokenizeLabelEnd, }; /** @type {Construct} */ const resourceConstruct = { tokenize: tokenizeResource, }; /** @type {Construct} */ const referenceFullConstruct = { tokenize: tokenizeReferenceFull, }; /** @type {Construct} */ const referenceCollapsedConstruct = { tokenize: tokenizeReferenceCollapsed, }; /** @type {Resolver} */ function resolveAllLabelEnd(events) { let index = -1; /** @type {Array} */ const newEvents = []; while (++index < events.length) { const token = events[index][1]; newEvents.push(events[index]); if (token.type === "labelImage" || token.type === "labelLink" || token.type === "labelEnd") { // Remove the marker. const offset = token.type === "labelImage" ? 4 : 2; token.type = "data"; index += offset; } } // If the events are equal, we don't have to copy newEvents to events if (events.length !== newEvents.length) { splice(events, 0, events.length, newEvents); } return events; } /** @type {Resolver} */ function resolveToLabelEnd(events, context) { let index = events.length; let offset = 0; /** @type {Token} */ let token; /** @type {number | undefined} */ let open; /** @type {number | undefined} */ let close; /** @type {Array} */ let media; // Find an opening. while (index--) { token = events[index][1]; if (open) { // If we see another link, or inactive link label, we’ve been here before. if (token.type === "link" || (token.type === "labelLink" && token._inactive)) { break; } // Mark other link openings as inactive, as we can’t have links in // links. if (events[index][0] === "enter" && token.type === "labelLink") { token._inactive = true; } } else if (close) { if (events[index][0] === "enter" && (token.type === "labelImage" || token.type === "labelLink") && !token._balanced) { open = index; if (token.type !== "labelLink") { offset = 2; break; } } } else if (token.type === "labelEnd") { close = index; } } const group = { type: events[open][1].type === "labelLink" ? "link" : "image", start: { ...events[open][1].start, }, end: { ...events[events.length - 1][1].end, }, }; const label = { type: "label", start: { ...events[open][1].start, }, end: { ...events[close][1].end, }, }; const text = { type: "labelText", start: { ...events[open + offset + 2][1].end, }, end: { ...events[close - 2][1].start, }, }; media = [ ["enter", group, context], ["enter", label, context], ]; // Opening marker. media = push(media, events.slice(open + 1, open + offset + 3)); // Text open. media = push(media, [["enter", text, context]]); // Always populated by defaults. // Between. media = push(media, resolveAll(context.parser.constructs.insideSpan.null, events.slice(open + offset + 4, close - 3), context)); // Text close, marker close, label close. media = push(media, [["exit", text, context], events[close - 2], events[close - 1], ["exit", label, context]]); // Reference, resource, or so. media = push(media, events.slice(close + 1)); // Media close. media = push(media, [["exit", group, context]]); splice(events, open, events.length, media); return events; } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeLabelEnd(effects, ok, nok) { const self = this; let index = self.events.length; /** @type {Token} */ let labelStart; /** @type {boolean} */ let defined; // Find an opening. while (index--) { if ((self.events[index][1].type === "labelImage" || self.events[index][1].type === "labelLink") && !self.events[index][1]._balanced) { labelStart = self.events[index][1]; break; } } return start; /** * Start of label end. * * ```markdown * > | [a](b) c * ^ * > | [a][b] c * ^ * > | [a][] b * ^ * > | [a] b * ``` * * @type {State} */ function start(code) { // If there is not an okay opening. if (!labelStart) { return nok(code); } // If the corresponding label (link) start is marked as inactive, // it means we’d be wrapping a link, like this: // // ```markdown // > | a [b [c](d) e](f) g. // ^ // ``` // // We can’t have that, so it’s just balanced brackets. if (labelStart._inactive) { return labelEndNok(code); } defined = self.parser.defined.includes( normalizeIdentifier( self.sliceSerialize({ start: labelStart.end, end: self.now(), }), ), ); effects.enter("labelEnd"); effects.enter("labelMarker"); effects.consume(code); effects.exit("labelMarker"); effects.exit("labelEnd"); return after; } /** * After `]`. * * ```markdown * > | [a](b) c * ^ * > | [a][b] c * ^ * > | [a][] b * ^ * > | [a] b * ^ * ``` * * @type {State} */ function after(code) { // Note: `markdown-rs` also parses GFM footnotes here, which for us is in // an extension. // Resource (`[asd](fgh)`)? if (code === 40) { return effects.attempt(resourceConstruct, labelEndOk, defined ? labelEndOk : labelEndNok)(code); } // Full (`[asd][fgh]`) or collapsed (`[asd][]`) reference? if (code === 91) { return effects.attempt(referenceFullConstruct, labelEndOk, defined ? referenceNotFull : labelEndNok)(code); } // Shortcut (`[asd]`) reference? return defined ? labelEndOk(code) : labelEndNok(code); } /** * After `]`, at `[`, but not at a full reference. * * > 👉 **Note**: we only get here if the label is defined. * * ```markdown * > | [a][] b * ^ * > | [a] b * ^ * ``` * * @type {State} */ function referenceNotFull(code) { return effects.attempt(referenceCollapsedConstruct, labelEndOk, labelEndNok)(code); } /** * Done, we found something. * * ```markdown * > | [a](b) c * ^ * > | [a][b] c * ^ * > | [a][] b * ^ * > | [a] b * ^ * ``` * * @type {State} */ function labelEndOk(code) { // Note: `markdown-rs` does a bunch of stuff here. return ok(code); } /** * Done, it’s nothing. * * There was an okay opening, but we didn’t match anything. * * ```markdown * > | [a](b c * ^ * > | [a][b c * ^ * > | [a] b * ^ * ``` * * @type {State} */ function labelEndNok(code) { labelStart._balanced = true; return nok(code); } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeResource(effects, ok, nok) { return resourceStart; /** * At a resource. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceStart(code) { effects.enter("resource"); effects.enter("resourceMarker"); effects.consume(code); effects.exit("resourceMarker"); return resourceBefore; } /** * In resource, after `(`, at optional whitespace. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceBefore(code) { return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceOpen)(code) : resourceOpen(code); } /** * In resource, after optional whitespace, at `)` or a destination. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceOpen(code) { if (code === 41) { return resourceEnd(code); } return factoryDestination( effects, resourceDestinationAfter, resourceDestinationMissing, "resourceDestination", "resourceDestinationLiteral", "resourceDestinationLiteralMarker", "resourceDestinationRaw", "resourceDestinationString", 32, )(code); } /** * In resource, after destination, at optional whitespace. * * ```markdown * > | [a](b) c * ^ * ``` * * @type {State} */ function resourceDestinationAfter(code) { return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceBetween)(code) : resourceEnd(code); } /** * At invalid destination. * * ```markdown * > | [a](<<) b * ^ * ``` * * @type {State} */ function resourceDestinationMissing(code) { return nok(code); } /** * In resource, after destination and whitespace, at `(` or title. * * ```markdown * > | [a](b ) c * ^ * ``` * * @type {State} */ function resourceBetween(code) { if (code === 34 || code === 39 || code === 40) { return factoryTitle(effects, resourceTitleAfter, nok, "resourceTitle", "resourceTitleMarker", "resourceTitleString")(code); } return resourceEnd(code); } /** * In resource, after title, at optional whitespace. * * ```markdown * > | [a](b "c") d * ^ * ``` * * @type {State} */ function resourceTitleAfter(code) { return markdownLineEndingOrSpace(code) ? factoryWhitespace(effects, resourceEnd)(code) : resourceEnd(code); } /** * In resource, at `)`. * * ```markdown * > | [a](b) d * ^ * ``` * * @type {State} */ function resourceEnd(code) { if (code === 41) { effects.enter("resourceMarker"); effects.consume(code); effects.exit("resourceMarker"); effects.exit("resource"); return ok; } return nok(code); } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeReferenceFull(effects, ok, nok) { const self = this; return referenceFull; /** * In a reference (full), at the `[`. * * ```markdown * > | [a][b] d * ^ * ``` * * @type {State} */ function referenceFull(code) { return factoryLabel.call(self, effects, referenceFullAfter, referenceFullMissing, "reference", "referenceMarker", "referenceString")(code); } /** * In a reference (full), after `]`. * * ```markdown * > | [a][b] d * ^ * ``` * * @type {State} */ function referenceFullAfter(code) { return self.parser.defined.includes(normalizeIdentifier(self.sliceSerialize(self.events[self.events.length - 1][1]).slice(1, -1))) ? ok(code) : nok(code); } /** * In reference (full) that was missing. * * ```markdown * > | [a][b d * ^ * ``` * * @type {State} */ function referenceFullMissing(code) { return nok(code); } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeReferenceCollapsed(effects, ok, nok) { return referenceCollapsedStart; /** * In reference (collapsed), at `[`. * * > 👉 **Note**: we only get here if the label is defined. * * ```markdown * > | [a][] d * ^ * ``` * * @type {State} */ function referenceCollapsedStart(code) { // We only attempt a collapsed label if there’s a `[`. effects.enter("reference"); effects.enter("referenceMarker"); effects.consume(code); effects.exit("referenceMarker"); return referenceCollapsedOpen; } /** * In reference (collapsed), at `]`. * * > 👉 **Note**: we only get here if the label is defined. * * ```markdown * > | [a][] d * ^ * ``` * * @type {State} */ function referenceCollapsedOpen(code) { if (code === 93) { effects.enter("referenceMarker"); effects.consume(code); effects.exit("referenceMarker"); effects.exit("reference"); return ok; } return nok(code); } } /** * @import { * Construct, * State, * TokenizeContext, * Tokenizer * } from 'micromark-util-types' */ /** @type {Construct} */ const labelStartImage = { name: "labelStartImage", resolveAll: labelEnd.resolveAll, tokenize: tokenizeLabelStartImage, }; /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeLabelStartImage(effects, ok, nok) { const self = this; return start; /** * Start of label (image) start. * * ```markdown * > | a ![b] c * ^ * ``` * * @type {State} */ function start(code) { effects.enter("labelImage"); effects.enter("labelImageMarker"); effects.consume(code); effects.exit("labelImageMarker"); return open; } /** * After `!`, at `[`. * * ```markdown * > | a ![b] c * ^ * ``` * * @type {State} */ function open(code) { if (code === 91) { effects.enter("labelMarker"); effects.consume(code); effects.exit("labelMarker"); effects.exit("labelImage"); return after; } return nok(code); } /** * After `![`. * * ```markdown * > | a ![b] c * ^ * ``` * * This is needed in because, when GFM footnotes are enabled, images never * form when started with a `^`. * Instead, links form: * * ```markdown * ![^a](b) * * ![^a][b] * * [b]: c * ``` * * ```html *

!^a

*

!^a

* ``` * * @type {State} */ function after(code) { // To do: use a new field to do this, this is still needed for // `micromark-extension-gfm-footnote`, but the `label-start-link` // behavior isn’t. // Hidden footnotes hook. /* c8 ignore next 3 */ return code === 94 && "_hiddenFootnoteSupport" in self.parser.constructs ? nok(code) : ok(code); } } /** * @import { * Construct, * State, * TokenizeContext, * Tokenizer * } from 'micromark-util-types' */ /** @type {Construct} */ const labelStartLink = { name: "labelStartLink", resolveAll: labelEnd.resolveAll, tokenize: tokenizeLabelStartLink, }; /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeLabelStartLink(effects, ok, nok) { const self = this; return start; /** * Start of label (link) start. * * ```markdown * > | a [b] c * ^ * ``` * * @type {State} */ function start(code) { effects.enter("labelLink"); effects.enter("labelMarker"); effects.consume(code); effects.exit("labelMarker"); effects.exit("labelLink"); return after; } /** @type {State} */ function after(code) { // To do: this isn’t needed in `micromark-extension-gfm-footnote`, // remove. // Hidden footnotes hook. /* c8 ignore next 3 */ return code === 94 && "_hiddenFootnoteSupport" in self.parser.constructs ? nok(code) : ok(code); } } /** * @import { * Construct, * State, * TokenizeContext, * Tokenizer * } from 'micromark-util-types' */ /** @type {Construct} */ const lineEnding = { name: "lineEnding", tokenize: tokenizeLineEnding, }; /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeLineEnding(effects, ok) { return start; /** @type {State} */ function start(code) { effects.enter("lineEnding"); effects.consume(code); effects.exit("lineEnding"); return factorySpace(effects, ok, "linePrefix"); } } /** * @import { * Code, * Construct, * State, * TokenizeContext, * Tokenizer * } from 'micromark-util-types' */ /** @type {Construct} */ const thematicBreak$1 = { name: "thematicBreak", tokenize: tokenizeThematicBreak, }; /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeThematicBreak(effects, ok, nok) { let size = 0; /** @type {NonNullable} */ let marker; return start; /** * Start of thematic break. * * ```markdown * > | *** * ^ * ``` * * @type {State} */ function start(code) { effects.enter("thematicBreak"); // To do: parse indent like `markdown-rs`. return before(code); } /** * After optional whitespace, at marker. * * ```markdown * > | *** * ^ * ``` * * @type {State} */ function before(code) { marker = code; return atBreak(code); } /** * After something, before something else. * * ```markdown * > | *** * ^ * ``` * * @type {State} */ function atBreak(code) { if (code === marker) { effects.enter("thematicBreakSequence"); return sequence(code); } if (size >= 3 && (code === null || markdownLineEnding(code))) { effects.exit("thematicBreak"); return ok(code); } return nok(code); } /** * In sequence. * * ```markdown * > | *** * ^ * ``` * * @type {State} */ function sequence(code) { if (code === marker) { effects.consume(code); size++; return sequence; } effects.exit("thematicBreakSequence"); return markdownSpace(code) ? factorySpace(effects, atBreak, "whitespace")(code) : atBreak(code); } } /** * @import { * Code, * Construct, * Exiter, * State, * TokenizeContext, * Tokenizer * } from 'micromark-util-types' */ /** @type {Construct} */ const list$1 = { continuation: { tokenize: tokenizeListContinuation, }, exit: tokenizeListEnd, name: "list", tokenize: tokenizeListStart, }; /** @type {Construct} */ const listItemPrefixWhitespaceConstruct = { partial: true, tokenize: tokenizeListItemPrefixWhitespace, }; /** @type {Construct} */ const indentConstruct = { partial: true, tokenize: tokenizeIndent$1, }; // To do: `markdown-rs` parses list items on their own and later stitches them // together. /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeListStart(effects, ok, nok) { const self = this; const tail = self.events[self.events.length - 1]; let initialSize = tail && tail[1].type === "linePrefix" ? tail[2].sliceSerialize(tail[1], true).length : 0; let size = 0; return start; /** @type {State} */ function start(code) { const kind = self.containerState.type || (code === 42 || code === 43 || code === 45 ? "listUnordered" : "listOrdered"); if (kind === "listUnordered" ? !self.containerState.marker || code === self.containerState.marker : asciiDigit(code)) { if (!self.containerState.type) { self.containerState.type = kind; effects.enter(kind, { _container: true, }); } if (kind === "listUnordered") { effects.enter("listItemPrefix"); return code === 42 || code === 45 ? effects.check(thematicBreak$1, nok, atMarker)(code) : atMarker(code); } if (!self.interrupt || code === 49) { effects.enter("listItemPrefix"); effects.enter("listItemValue"); return inside(code); } } return nok(code); } /** @type {State} */ function inside(code) { if (asciiDigit(code) && ++size < 10) { effects.consume(code); return inside; } if ((!self.interrupt || size < 2) && (self.containerState.marker ? code === self.containerState.marker : code === 41 || code === 46)) { effects.exit("listItemValue"); return atMarker(code); } return nok(code); } /** * @type {State} **/ function atMarker(code) { effects.enter("listItemMarker"); effects.consume(code); effects.exit("listItemMarker"); self.containerState.marker = self.containerState.marker || code; return effects.check( blankLine, // Can’t be empty when interrupting. self.interrupt ? nok : onBlank, effects.attempt(listItemPrefixWhitespaceConstruct, endOfPrefix, otherPrefix), ); } /** @type {State} */ function onBlank(code) { self.containerState.initialBlankLine = true; initialSize++; return endOfPrefix(code); } /** @type {State} */ function otherPrefix(code) { if (markdownSpace(code)) { effects.enter("listItemPrefixWhitespace"); effects.consume(code); effects.exit("listItemPrefixWhitespace"); return endOfPrefix; } return nok(code); } /** @type {State} */ function endOfPrefix(code) { self.containerState.size = initialSize + self.sliceSerialize(effects.exit("listItemPrefix"), true).length; return ok(code); } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeListContinuation(effects, ok, nok) { const self = this; self.containerState._closeFlow = undefined; return effects.check(blankLine, onBlank, notBlank); /** @type {State} */ function onBlank(code) { self.containerState.furtherBlankLines = self.containerState.furtherBlankLines || self.containerState.initialBlankLine; // We have a blank line. // Still, try to consume at most the items size. return factorySpace(effects, ok, "listItemIndent", self.containerState.size + 1)(code); } /** @type {State} */ function notBlank(code) { if (self.containerState.furtherBlankLines || !markdownSpace(code)) { self.containerState.furtherBlankLines = undefined; self.containerState.initialBlankLine = undefined; return notInCurrentItem(code); } self.containerState.furtherBlankLines = undefined; self.containerState.initialBlankLine = undefined; return effects.attempt(indentConstruct, ok, notInCurrentItem)(code); } /** @type {State} */ function notInCurrentItem(code) { // While we do continue, we signal that the flow should be closed. self.containerState._closeFlow = true; // As we’re closing flow, we’re no longer interrupting. self.interrupt = undefined; // Always populated by defaults. return factorySpace(effects, effects.attempt(list$1, ok, nok), "linePrefix", self.parser.constructs.disable.null.includes("codeIndented") ? undefined : 4)(code); } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeIndent$1(effects, ok, nok) { const self = this; return factorySpace(effects, afterPrefix, "listItemIndent", self.containerState.size + 1); /** @type {State} */ function afterPrefix(code) { const tail = self.events[self.events.length - 1]; return tail && tail[1].type === "listItemIndent" && tail[2].sliceSerialize(tail[1], true).length === self.containerState.size ? ok(code) : nok(code); } } /** * @this {TokenizeContext} * Context. * @type {Exiter} */ function tokenizeListEnd(effects) { effects.exit(this.containerState.type); } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeListItemPrefixWhitespace(effects, ok, nok) { const self = this; // Always populated by defaults. return factorySpace(effects, afterPrefix, "listItemPrefixWhitespace", self.parser.constructs.disable.null.includes("codeIndented") ? undefined : 4 + 1); /** @type {State} */ function afterPrefix(code) { const tail = self.events[self.events.length - 1]; return !markdownSpace(code) && tail && tail[1].type === "listItemPrefixWhitespace" ? ok(code) : nok(code); } } /** * @import { * Code, * Construct, * Resolver, * State, * TokenizeContext, * Tokenizer * } from 'micromark-util-types' */ /** @type {Construct} */ const setextUnderline = { name: "setextUnderline", resolveTo: resolveToSetextUnderline, tokenize: tokenizeSetextUnderline, }; /** @type {Resolver} */ function resolveToSetextUnderline(events, context) { // To do: resolve like `markdown-rs`. let index = events.length; /** @type {number | undefined} */ let content; /** @type {number | undefined} */ let text; /** @type {number | undefined} */ let definition; // Find the opening of the content. // It’ll always exist: we don’t tokenize if it isn’t there. while (index--) { if (events[index][0] === "enter") { if (events[index][1].type === "content") { content = index; break; } if (events[index][1].type === "paragraph") { text = index; } } // Exit else { if (events[index][1].type === "content") { // Remove the content end (if needed we’ll add it later) events.splice(index, 1); } if (!definition && events[index][1].type === "definition") { definition = index; } } } const heading = { type: "setextHeading", start: { ...events[content][1].start, }, end: { ...events[events.length - 1][1].end, }, }; // Change the paragraph to setext heading text. events[text][1].type = "setextHeadingText"; // If we have definitions in the content, we’ll keep on having content, // but we need move it. if (definition) { events.splice(text, 0, ["enter", heading, context]); events.splice(definition + 1, 0, ["exit", events[content][1], context]); events[content][1].end = { ...events[definition][1].end, }; } else { events[content][1] = heading; } // Add the heading exit at the end. events.push(["exit", heading, context]); return events; } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} */ function tokenizeSetextUnderline(effects, ok, nok) { const self = this; /** @type {NonNullable} */ let marker; return start; /** * At start of heading (setext) underline. * * ```markdown * | aa * > | == * ^ * ``` * * @type {State} */ function start(code) { let index = self.events.length; /** @type {boolean | undefined} */ let paragraph; // Find an opening. while (index--) { // Skip enter/exit of line ending, line prefix, and content. // We can now either have a definition or a paragraph. if (self.events[index][1].type !== "lineEnding" && self.events[index][1].type !== "linePrefix" && self.events[index][1].type !== "content") { paragraph = self.events[index][1].type === "paragraph"; break; } } // To do: handle lazy/pierce like `markdown-rs`. // To do: parse indent like `markdown-rs`. if (!self.parser.lazy[self.now().line] && (self.interrupt || paragraph)) { effects.enter("setextHeadingLine"); marker = code; return before(code); } return nok(code); } /** * After optional whitespace, at `-` or `=`. * * ```markdown * | aa * > | == * ^ * ``` * * @type {State} */ function before(code) { effects.enter("setextHeadingLineSequence"); return inside(code); } /** * In sequence. * * ```markdown * | aa * > | == * ^ * ``` * * @type {State} */ function inside(code) { if (code === marker) { effects.consume(code); return inside; } effects.exit("setextHeadingLineSequence"); return markdownSpace(code) ? factorySpace(effects, after, "lineSuffix")(code) : after(code); } /** * After sequence, after optional whitespace. * * ```markdown * | aa * > | == * ^ * ``` * * @type {State} */ function after(code) { if (code === null || markdownLineEnding(code)) { effects.exit("setextHeadingLine"); return ok(code); } return nok(code); } } /** * @import {Event, Exiter, Extension, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types' */ const indent = { tokenize: tokenizeIndent, partial: true, }; // To do: micromark should support a `_hiddenGfmFootnoteSupport`, which only // affects label start (image). // That will let us drop `tokenizePotentialGfmFootnote*`. // It currently has a `_hiddenFootnoteSupport`, which affects that and more. // That can be removed when `micromark-extension-footnote` is archived. /** * Create an extension for `micromark` to enable GFM footnote syntax. * * @returns {Extension} * Extension for `micromark` that can be passed in `extensions` to * enable GFM footnote syntax. */ function gfmFootnote() { /** @type {Extension} */ return { document: { [91]: { name: "gfmFootnoteDefinition", tokenize: tokenizeDefinitionStart, continuation: { tokenize: tokenizeDefinitionContinuation, }, exit: gfmFootnoteDefinitionEnd, }, }, text: { [91]: { name: "gfmFootnoteCall", tokenize: tokenizeGfmFootnoteCall, }, [93]: { name: "gfmPotentialFootnoteCall", add: "after", tokenize: tokenizePotentialGfmFootnoteCall, resolveTo: resolveToPotentialGfmFootnoteCall, }, }, }; } // To do: remove after micromark update. /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizePotentialGfmFootnoteCall(effects, ok, nok) { const self = this; let index = self.events.length; const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []); /** @type {Token} */ let labelStart; // Find an opening. while (index--) { const token = self.events[index][1]; if (token.type === "labelImage") { labelStart = token; break; } // Exit if we’ve walked far enough. if (token.type === "gfmFootnoteCall" || token.type === "labelLink" || token.type === "label" || token.type === "image" || token.type === "link") { break; } } return start; /** * @type {State} */ function start(code) { if (!labelStart || !labelStart._balanced) { return nok(code); } const id = normalizeIdentifier( self.sliceSerialize({ start: labelStart.end, end: self.now(), }), ); if (id.codePointAt(0) !== 94 || !defined.includes(id.slice(1))) { return nok(code); } effects.enter("gfmFootnoteCallLabelMarker"); effects.consume(code); effects.exit("gfmFootnoteCallLabelMarker"); return ok(code); } } // To do: remove after micromark update. /** @type {Resolver} */ function resolveToPotentialGfmFootnoteCall(events, context) { let index = events.length; // Find an opening. while (index--) { if (events[index][1].type === "labelImage" && events[index][0] === "enter") { events[index][1]; break; } } // Change the `labelImageMarker` to a `data`. events[index + 1][1].type = "data"; events[index + 3][1].type = "gfmFootnoteCallLabelMarker"; // The whole (without `!`): /** @type {Token} */ const call = { type: "gfmFootnoteCall", start: Object.assign({}, events[index + 3][1].start), end: Object.assign({}, events[events.length - 1][1].end), }; // The `^` marker /** @type {Token} */ const marker = { type: "gfmFootnoteCallMarker", start: Object.assign({}, events[index + 3][1].end), end: Object.assign({}, events[index + 3][1].end), }; // Increment the end 1 character. marker.end.column++; marker.end.offset++; marker.end._bufferIndex++; /** @type {Token} */ const string = { type: "gfmFootnoteCallString", start: Object.assign({}, marker.end), end: Object.assign({}, events[events.length - 1][1].start), }; /** @type {Token} */ const chunk = { type: "chunkString", contentType: "string", start: Object.assign({}, string.start), end: Object.assign({}, string.end), }; /** @type {Array} */ const replacement = [ // Take the `labelImageMarker` (now `data`, the `!`) events[index + 1], events[index + 2], ["enter", call, context], // The `[` events[index + 3], events[index + 4], // The `^`. ["enter", marker, context], ["exit", marker, context], // Everything in between. ["enter", string, context], ["enter", chunk, context], ["exit", chunk, context], ["exit", string, context], // The ending (`]`, properly parsed and labelled). events[events.length - 2], events[events.length - 1], ["exit", call, context], ]; events.splice(index, events.length - index + 1, ...replacement); return events; } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeGfmFootnoteCall(effects, ok, nok) { const self = this; const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []); let size = 0; /** @type {boolean} */ let data; // Note: the implementation of `markdown-rs` is different, because it houses // core *and* extensions in one project. // Therefore, it can include footnote logic inside `label-end`. // We can’t do that, but luckily, we can parse footnotes in a simpler way than // needed for labels. return start; /** * Start of footnote label. * * ```markdown * > | a [^b] c * ^ * ``` * * @type {State} */ function start(code) { effects.enter("gfmFootnoteCall"); effects.enter("gfmFootnoteCallLabelMarker"); effects.consume(code); effects.exit("gfmFootnoteCallLabelMarker"); return callStart; } /** * After `[`, at `^`. * * ```markdown * > | a [^b] c * ^ * ``` * * @type {State} */ function callStart(code) { if (code !== 94) return nok(code); effects.enter("gfmFootnoteCallMarker"); effects.consume(code); effects.exit("gfmFootnoteCallMarker"); effects.enter("gfmFootnoteCallString"); effects.enter("chunkString").contentType = "string"; return callData; } /** * In label. * * ```markdown * > | a [^b] c * ^ * ``` * * @type {State} */ function callData(code) { if ( // Too long. size > 999 || // Closing brace with nothing. (code === 93 && !data) || // Space or tab is not supported by GFM for some reason. // `\n` and `[` not being supported makes sense. code === null || code === 91 || markdownLineEndingOrSpace(code) ) { return nok(code); } if (code === 93) { effects.exit("chunkString"); const token = effects.exit("gfmFootnoteCallString"); if (!defined.includes(normalizeIdentifier(self.sliceSerialize(token)))) { return nok(code); } effects.enter("gfmFootnoteCallLabelMarker"); effects.consume(code); effects.exit("gfmFootnoteCallLabelMarker"); effects.exit("gfmFootnoteCall"); return ok; } if (!markdownLineEndingOrSpace(code)) { data = true; } size++; effects.consume(code); return code === 92 ? callEscape : callData; } /** * On character after escape. * * ```markdown * > | a [^b\c] d * ^ * ``` * * @type {State} */ function callEscape(code) { if (code === 91 || code === 92 || code === 93) { effects.consume(code); size++; return callData; } return callData(code); } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeDefinitionStart(effects, ok, nok) { const self = this; const defined = self.parser.gfmFootnotes || (self.parser.gfmFootnotes = []); /** @type {string} */ let identifier; let size = 0; /** @type {boolean | undefined} */ let data; return start; /** * Start of GFM footnote definition. * * ```markdown * > | [^a]: b * ^ * ``` * * @type {State} */ function start(code) { effects.enter("gfmFootnoteDefinition")._container = true; effects.enter("gfmFootnoteDefinitionLabel"); effects.enter("gfmFootnoteDefinitionLabelMarker"); effects.consume(code); effects.exit("gfmFootnoteDefinitionLabelMarker"); return labelAtMarker; } /** * In label, at caret. * * ```markdown * > | [^a]: b * ^ * ``` * * @type {State} */ function labelAtMarker(code) { if (code === 94) { effects.enter("gfmFootnoteDefinitionMarker"); effects.consume(code); effects.exit("gfmFootnoteDefinitionMarker"); effects.enter("gfmFootnoteDefinitionLabelString"); effects.enter("chunkString").contentType = "string"; return labelInside; } return nok(code); } /** * In label. * * > 👉 **Note**: `cmark-gfm` prevents whitespace from occurring in footnote * > definition labels. * * ```markdown * > | [^a]: b * ^ * ``` * * @type {State} */ function labelInside(code) { if ( // Too long. size > 999 || // Closing brace with nothing. (code === 93 && !data) || // Space or tab is not supported by GFM for some reason. // `\n` and `[` not being supported makes sense. code === null || code === 91 || markdownLineEndingOrSpace(code) ) { return nok(code); } if (code === 93) { effects.exit("chunkString"); const token = effects.exit("gfmFootnoteDefinitionLabelString"); identifier = normalizeIdentifier(self.sliceSerialize(token)); effects.enter("gfmFootnoteDefinitionLabelMarker"); effects.consume(code); effects.exit("gfmFootnoteDefinitionLabelMarker"); effects.exit("gfmFootnoteDefinitionLabel"); return labelAfter; } if (!markdownLineEndingOrSpace(code)) { data = true; } size++; effects.consume(code); return code === 92 ? labelEscape : labelInside; } /** * After `\`, at a special character. * * > 👉 **Note**: `cmark-gfm` currently does not support escaped brackets: * > * * ```markdown * > | [^a\*b]: c * ^ * ``` * * @type {State} */ function labelEscape(code) { if (code === 91 || code === 92 || code === 93) { effects.consume(code); size++; return labelInside; } return labelInside(code); } /** * After definition label. * * ```markdown * > | [^a]: b * ^ * ``` * * @type {State} */ function labelAfter(code) { if (code === 58) { effects.enter("definitionMarker"); effects.consume(code); effects.exit("definitionMarker"); if (!defined.includes(identifier)) { defined.push(identifier); } // Any whitespace after the marker is eaten, forming indented code // is not possible. // No space is also fine, just like a block quote marker. return factorySpace(effects, whitespaceAfter, "gfmFootnoteDefinitionWhitespace"); } return nok(code); } /** * After definition prefix. * * ```markdown * > | [^a]: b * ^ * ``` * * @type {State} */ function whitespaceAfter(code) { // `markdown-rs` has a wrapping token for the prefix that is closed here. return ok(code); } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeDefinitionContinuation(effects, ok, nok) { /// Start of footnote definition continuation. /// /// ```markdown /// | [^a]: b /// > | c /// ^ /// ``` // // Either a blank line, which is okay, or an indented thing. return effects.check(blankLine, ok, effects.attempt(indent, ok, nok)); } /** @type {Exiter} */ function gfmFootnoteDefinitionEnd(effects) { effects.exit("gfmFootnoteDefinition"); } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeIndent(effects, ok, nok) { const self = this; return factorySpace(effects, afterPrefix, "gfmFootnoteDefinitionIndent", 4 + 1); /** * @type {State} */ function afterPrefix(code) { const tail = self.events[self.events.length - 1]; return tail && tail[1].type === "gfmFootnoteDefinitionIndent" && tail[2].sliceSerialize(tail[1], true).length === 4 ? ok(code) : nok(code); } } /** * @import {Options} from 'micromark-extension-gfm-strikethrough' * @import {Event, Extension, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types' */ /** * Create an extension for `micromark` to enable GFM strikethrough syntax. * * @param {Options | null | undefined} [options={}] * Configuration. * @returns {Extension} * Extension for `micromark` that can be passed in `extensions`, to * enable GFM strikethrough syntax. */ function gfmStrikethrough(options) { const options_ = options || {}; let single = options_.singleTilde; const tokenizer = { name: "strikethrough", tokenize: tokenizeStrikethrough, resolveAll: resolveAllStrikethrough, }; if (single === null || single === undefined) { single = true; } return { text: { [126]: tokenizer, }, insideSpan: { null: [tokenizer], }, attentionMarkers: { null: [126], }, }; /** * Take events and resolve strikethrough. * * @type {Resolver} */ function resolveAllStrikethrough(events, context) { let index = -1; // Walk through all events. while (++index < events.length) { // Find a token that can close. if (events[index][0] === "enter" && events[index][1].type === "strikethroughSequenceTemporary" && events[index][1]._close) { let open = index; // Now walk back to find an opener. while (open--) { // Find a token that can open the closer. if ( events[open][0] === "exit" && events[open][1].type === "strikethroughSequenceTemporary" && events[open][1]._open && // If the sizes are the same: events[index][1].end.offset - events[index][1].start.offset === events[open][1].end.offset - events[open][1].start.offset ) { events[index][1].type = "strikethroughSequence"; events[open][1].type = "strikethroughSequence"; /** @type {Token} */ const strikethrough = { type: "strikethrough", start: Object.assign({}, events[open][1].start), end: Object.assign({}, events[index][1].end), }; /** @type {Token} */ const text = { type: "strikethroughText", start: Object.assign({}, events[open][1].end), end: Object.assign({}, events[index][1].start), }; // Opening. /** @type {Array} */ const nextEvents = [ ["enter", strikethrough, context], ["enter", events[open][1], context], ["exit", events[open][1], context], ["enter", text, context], ]; const insideSpan = context.parser.constructs.insideSpan.null; if (insideSpan) { // Between. splice(nextEvents, nextEvents.length, 0, resolveAll(insideSpan, events.slice(open + 1, index), context)); } // Closing. splice(nextEvents, nextEvents.length, 0, [ ["exit", text, context], ["enter", events[index][1], context], ["exit", events[index][1], context], ["exit", strikethrough, context], ]); splice(events, open - 1, index - open + 3, nextEvents); index = open + nextEvents.length - 2; break; } } } } index = -1; while (++index < events.length) { if (events[index][1].type === "strikethroughSequenceTemporary") { events[index][1].type = "data"; } } return events; } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeStrikethrough(effects, ok, nok) { const previous = this.previous; const events = this.events; let size = 0; return start; /** @type {State} */ function start(code) { if (previous === 126 && events[events.length - 1][1].type !== "characterEscape") { return nok(code); } effects.enter("strikethroughSequenceTemporary"); return more(code); } /** @type {State} */ function more(code) { const before = classifyCharacter(previous); if (code === 126) { // If this is the third marker, exit. if (size > 1) return nok(code); effects.consume(code); size++; return more; } if (size < 2 && !single) return nok(code); const token = effects.exit("strikethroughSequenceTemporary"); const after = classifyCharacter(code); token._open = !after || (after === 2 && Boolean(before)); token._close = !before || (before === 2 && Boolean(after)); return ok(code); } } } /** * @import {Event} from 'micromark-util-types' */ // Port of `edit_map.rs` from `markdown-rs`. // This should move to `markdown-js` later. // Deal with several changes in events, batching them together. // // Preferably, changes should be kept to a minimum. // Sometimes, it’s needed to change the list of events, because parsing can be // messy, and it helps to expose a cleaner interface of events to the compiler // and other users. // It can also help to merge many adjacent similar events. // And, in other cases, it’s needed to parse subcontent: pass some events // through another tokenizer and inject the result. /** * @typedef {[number, number, Array]} Change * @typedef {[number, number, number]} Jump */ /** * Tracks a bunch of edits. */ class EditMap { /** * Create a new edit map. */ constructor() { /** * Record of changes. * * @type {Array} */ this.map = []; } /** * Create an edit: a remove and/or add at a certain place. * * @param {number} index * @param {number} remove * @param {Array} add * @returns {undefined} */ add(index, remove, add) { addImplementation(this, index, remove, add); } // To do: add this when moving to `micromark`. // /** // * Create an edit: but insert `add` before existing additions. // * // * @param {number} index // * @param {number} remove // * @param {Array} add // * @returns {undefined} // */ // addBefore(index, remove, add) { // addImplementation(this, index, remove, add, true) // } /** * Done, change the events. * * @param {Array} events * @returns {undefined} */ consume(events) { this.map.sort(function (a, b) { return a[0] - b[0]; }); /* c8 ignore next 3 -- `resolve` is never called without tables, so without edits. */ if (this.map.length === 0) { return; } // To do: if links are added in events, like they are in `markdown-rs`, // this is needed. // // Calculate jumps: where items in the current list move to. // /** @type {Array} */ // const jumps = [] // let index = 0 // let addAcc = 0 // let removeAcc = 0 // while (index < this.map.length) { // const [at, remove, add] = this.map[index] // removeAcc += remove // addAcc += add.length // jumps.push([at, removeAcc, addAcc]) // index += 1 // } // // . shiftLinks(events, jumps) let index = this.map.length; /** @type {Array>} */ const vecs = []; while (index > 0) { index -= 1; vecs.push(events.slice(this.map[index][0] + this.map[index][1]), this.map[index][2]); // Truncate rest. events.length = this.map[index][0]; } vecs.push(events.slice()); events.length = 0; let slice = vecs.pop(); while (slice) { for (const element of slice) { events.push(element); } slice = vecs.pop(); } // Truncate everything. this.map.length = 0; } } /** * Create an edit. * * @param {EditMap} editMap * @param {number} at * @param {number} remove * @param {Array} add * @returns {undefined} */ function addImplementation(editMap, at, remove, add) { let index = 0; /* c8 ignore next 3 -- `resolve` is never called without tables, so without edits. */ if (remove === 0 && add.length === 0) { return; } while (index < editMap.map.length) { if (editMap.map[index][0] === at) { editMap.map[index][1] += remove; // To do: before not used by tables, use when moving to micromark. // if (before) { // add.push(...editMap.map[index][2]) // editMap.map[index][2] = add // } else { editMap.map[index][2].push(...add); // } return; } index += 1; } editMap.map.push([at, remove, add]); } // /** // * Shift `previous` and `next` links according to `jumps`. // * // * This fixes links in case there are events removed or added between them. // * // * @param {Array} events // * @param {Array} jumps // */ // function shiftLinks(events, jumps) { // let jumpIndex = 0 // let index = 0 // let add = 0 // let rm = 0 // while (index < events.length) { // const rmCurr = rm // while (jumpIndex < jumps.length && jumps[jumpIndex][0] <= index) { // add = jumps[jumpIndex][2] // rm = jumps[jumpIndex][1] // jumpIndex += 1 // } // // Ignore items that will be removed. // if (rm > rmCurr) { // index += rm - rmCurr // } else { // // ? // // if let Some(link) = &events[index].link { // // if let Some(next) = link.next { // // events[next].link.as_mut().unwrap().previous = Some(index + add - rm); // // while jumpIndex < jumps.len() && jumps[jumpIndex].0 <= next { // // add = jumps[jumpIndex].2; // // rm = jumps[jumpIndex].1; // // jumpIndex += 1; // // } // // events[index].link.as_mut().unwrap().next = Some(next + add - rm); // // index = next; // // continue; // // } // // } // index += 1 // } // } // } /** * @import {Event} from 'micromark-util-types' */ /** * @typedef {'center' | 'left' | 'none' | 'right'} Align */ /** * Figure out the alignment of a GFM table. * * @param {Readonly>} events * List of events. * @param {number} index * Table enter event. * @returns {Array} * List of aligns. */ function gfmTableAlign(events, index) { let inDelimiterRow = false; /** @type {Array} */ const align = []; while (index < events.length) { const event = events[index]; if (inDelimiterRow) { if (event[0] === "enter") { // Start of alignment value: set a new column. // To do: `markdown-rs` uses `tableDelimiterCellValue`. if (event[1].type === "tableContent") { align.push(events[index + 1][1].type === "tableDelimiterMarker" ? "left" : "none"); } } // Exits: // End of alignment value: change the column. // To do: `markdown-rs` uses `tableDelimiterCellValue`. else if (event[1].type === "tableContent") { if (events[index - 1][1].type === "tableDelimiterMarker") { const alignIndex = align.length - 1; align[alignIndex] = align[alignIndex] === "left" ? "center" : "right"; } } // Done! else if (event[1].type === "tableDelimiterRow") { break; } } else if (event[0] === "enter" && event[1].type === "tableDelimiterRow") { inDelimiterRow = true; } index += 1; } return align; } /** * @import {Event, Extension, Point, Resolver, State, Token, TokenizeContext, Tokenizer} from 'micromark-util-types' */ /** * Create an HTML extension for `micromark` to support GitHub tables syntax. * * @returns {Extension} * Extension for `micromark` that can be passed in `extensions` to enable GFM * table syntax. */ function gfmTable() { return { flow: { null: { name: "table", tokenize: tokenizeTable, resolveAll: resolveTable, }, }, }; } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeTable(effects, ok, nok) { const self = this; let size = 0; let sizeB = 0; /** @type {boolean | undefined} */ let seen; return start; /** * Start of a GFM table. * * If there is a valid table row or table head before, then we try to parse * another row. * Otherwise, we try to parse a head. * * ```markdown * > | | a | * ^ * | | - | * > | | b | * ^ * ``` * @type {State} */ function start(code) { let index = self.events.length - 1; while (index > -1) { const type = self.events[index][1].type; if ( type === "lineEnding" || // Note: markdown-rs uses `whitespace` instead of `linePrefix` type === "linePrefix" ) index--; else break; } const tail = index > -1 ? self.events[index][1].type : null; const next = tail === "tableHead" || tail === "tableRow" ? bodyRowStart : headRowBefore; // Don’t allow lazy body rows. if (next === bodyRowStart && self.parser.lazy[self.now().line]) { return nok(code); } return next(code); } /** * Before table head row. * * ```markdown * > | | a | * ^ * | | - | * | | b | * ``` * * @type {State} */ function headRowBefore(code) { effects.enter("tableHead"); effects.enter("tableRow"); return headRowStart(code); } /** * Before table head row, after whitespace. * * ```markdown * > | | a | * ^ * | | - | * | | b | * ``` * * @type {State} */ function headRowStart(code) { if (code === 124) { return headRowBreak(code); } // To do: micromark-js should let us parse our own whitespace in extensions, // like `markdown-rs`: // // ```js // // 4+ spaces. // if (markdownSpace(code)) { // return nok(code) // } // ``` seen = true; // Count the first character, that isn’t a pipe, double. sizeB += 1; return headRowBreak(code); } /** * At break in table head row. * * ```markdown * > | | a | * ^ * ^ * ^ * | | - | * | | b | * ``` * * @type {State} */ function headRowBreak(code) { if (code === null) { // Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t. return nok(code); } if (markdownLineEnding(code)) { // If anything other than one pipe (ignoring whitespace) was used, it’s fine. if (sizeB > 1) { sizeB = 0; // To do: check if this works. // Feel free to interrupt: self.interrupt = true; effects.exit("tableRow"); effects.enter("lineEnding"); effects.consume(code); effects.exit("lineEnding"); return headDelimiterStart; } // Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t. return nok(code); } if (markdownSpace(code)) { // To do: check if this is fine. // effects.attempt(State::Next(StateName::GfmTableHeadRowBreak), State::Nok) // State::Retry(space_or_tab(tokenizer)) return factorySpace(effects, headRowBreak, "whitespace")(code); } sizeB += 1; if (seen) { seen = false; // Header cell count. size += 1; } if (code === 124) { effects.enter("tableCellDivider"); effects.consume(code); effects.exit("tableCellDivider"); // Whether a delimiter was seen. seen = true; return headRowBreak; } // Anything else is cell data. effects.enter("data"); return headRowData(code); } /** * In table head row data. * * ```markdown * > | | a | * ^ * | | - | * | | b | * ``` * * @type {State} */ function headRowData(code) { if (code === null || code === 124 || markdownLineEndingOrSpace(code)) { effects.exit("data"); return headRowBreak(code); } effects.consume(code); return code === 92 ? headRowEscape : headRowData; } /** * In table head row escape. * * ```markdown * > | | a\-b | * ^ * | | ---- | * | | c | * ``` * * @type {State} */ function headRowEscape(code) { if (code === 92 || code === 124) { effects.consume(code); return headRowData; } return headRowData(code); } /** * Before delimiter row. * * ```markdown * | | a | * > | | - | * ^ * | | b | * ``` * * @type {State} */ function headDelimiterStart(code) { // Reset `interrupt`. self.interrupt = false; // Note: in `markdown-rs`, we need to handle piercing here too. if (self.parser.lazy[self.now().line]) { return nok(code); } effects.enter("tableDelimiterRow"); // Track if we’ve seen a `:` or `|`. seen = false; if (markdownSpace(code)) { return factorySpace(effects, headDelimiterBefore, "linePrefix", self.parser.constructs.disable.null.includes("codeIndented") ? undefined : 4)(code); } return headDelimiterBefore(code); } /** * Before delimiter row, after optional whitespace. * * Reused when a `|` is found later, to parse another cell. * * ```markdown * | | a | * > | | - | * ^ * | | b | * ``` * * @type {State} */ function headDelimiterBefore(code) { if (code === 45 || code === 58) { return headDelimiterValueBefore(code); } if (code === 124) { seen = true; // If we start with a pipe, we open a cell marker. effects.enter("tableCellDivider"); effects.consume(code); effects.exit("tableCellDivider"); return headDelimiterCellBefore; } // More whitespace / empty row not allowed at start. return headDelimiterNok(code); } /** * After `|`, before delimiter cell. * * ```markdown * | | a | * > | | - | * ^ * ``` * * @type {State} */ function headDelimiterCellBefore(code) { if (markdownSpace(code)) { return factorySpace(effects, headDelimiterValueBefore, "whitespace")(code); } return headDelimiterValueBefore(code); } /** * Before delimiter cell value. * * ```markdown * | | a | * > | | - | * ^ * ``` * * @type {State} */ function headDelimiterValueBefore(code) { // Align: left. if (code === 58) { sizeB += 1; seen = true; effects.enter("tableDelimiterMarker"); effects.consume(code); effects.exit("tableDelimiterMarker"); return headDelimiterLeftAlignmentAfter; } // Align: none. if (code === 45) { sizeB += 1; // To do: seems weird that this *isn’t* left aligned, but that state is used? return headDelimiterLeftAlignmentAfter(code); } if (code === null || markdownLineEnding(code)) { return headDelimiterCellAfter(code); } return headDelimiterNok(code); } /** * After delimiter cell left alignment marker. * * ```markdown * | | a | * > | | :- | * ^ * ``` * * @type {State} */ function headDelimiterLeftAlignmentAfter(code) { if (code === 45) { effects.enter("tableDelimiterFiller"); return headDelimiterFiller(code); } // Anything else is not ok after the left-align colon. return headDelimiterNok(code); } /** * In delimiter cell filler. * * ```markdown * | | a | * > | | - | * ^ * ``` * * @type {State} */ function headDelimiterFiller(code) { if (code === 45) { effects.consume(code); return headDelimiterFiller; } // Align is `center` if it was `left`, `right` otherwise. if (code === 58) { seen = true; effects.exit("tableDelimiterFiller"); effects.enter("tableDelimiterMarker"); effects.consume(code); effects.exit("tableDelimiterMarker"); return headDelimiterRightAlignmentAfter; } effects.exit("tableDelimiterFiller"); return headDelimiterRightAlignmentAfter(code); } /** * After delimiter cell right alignment marker. * * ```markdown * | | a | * > | | -: | * ^ * ``` * * @type {State} */ function headDelimiterRightAlignmentAfter(code) { if (markdownSpace(code)) { return factorySpace(effects, headDelimiterCellAfter, "whitespace")(code); } return headDelimiterCellAfter(code); } /** * After delimiter cell. * * ```markdown * | | a | * > | | -: | * ^ * ``` * * @type {State} */ function headDelimiterCellAfter(code) { if (code === 124) { return headDelimiterBefore(code); } if (code === null || markdownLineEnding(code)) { // Exit when: // * there was no `:` or `|` at all (it’s a thematic break or setext // underline instead) // * the header cell count is not the delimiter cell count if (!seen || size !== sizeB) { return headDelimiterNok(code); } // Note: in markdown-rs`, a reset is needed here. effects.exit("tableDelimiterRow"); effects.exit("tableHead"); // To do: in `markdown-rs`, resolvers need to be registered manually. // effects.register_resolver(ResolveName::GfmTable) return ok(code); } return headDelimiterNok(code); } /** * In delimiter row, at a disallowed byte. * * ```markdown * | | a | * > | | x | * ^ * ``` * * @type {State} */ function headDelimiterNok(code) { // Note: in `markdown-rs`, we need to reset, in `micromark-js` we don‘t. return nok(code); } /** * Before table body row. * * ```markdown * | | a | * | | - | * > | | b | * ^ * ``` * * @type {State} */ function bodyRowStart(code) { // Note: in `markdown-rs` we need to manually take care of a prefix, // but in `micromark-js` that is done for us, so if we’re here, we’re // never at whitespace. effects.enter("tableRow"); return bodyRowBreak(code); } /** * At break in table body row. * * ```markdown * | | a | * | | - | * > | | b | * ^ * ^ * ^ * ``` * * @type {State} */ function bodyRowBreak(code) { if (code === 124) { effects.enter("tableCellDivider"); effects.consume(code); effects.exit("tableCellDivider"); return bodyRowBreak; } if (code === null || markdownLineEnding(code)) { effects.exit("tableRow"); return ok(code); } if (markdownSpace(code)) { return factorySpace(effects, bodyRowBreak, "whitespace")(code); } // Anything else is cell content. effects.enter("data"); return bodyRowData(code); } /** * In table body row data. * * ```markdown * | | a | * | | - | * > | | b | * ^ * ``` * * @type {State} */ function bodyRowData(code) { if (code === null || code === 124 || markdownLineEndingOrSpace(code)) { effects.exit("data"); return bodyRowBreak(code); } effects.consume(code); return code === 92 ? bodyRowEscape : bodyRowData; } /** * In table body row escape. * * ```markdown * | | a | * | | ---- | * > | | b\-c | * ^ * ``` * * @type {State} */ function bodyRowEscape(code) { if (code === 92 || code === 124) { effects.consume(code); return bodyRowData; } return bodyRowData(code); } } /** @type {Resolver} */ function resolveTable(events, context) { let index = -1; let inFirstCellAwaitingPipe = true; /** @type {RowKind} */ let rowKind = 0; /** @type {Range} */ let lastCell = [0, 0, 0, 0]; /** @type {Range} */ let cell = [0, 0, 0, 0]; let afterHeadAwaitingFirstBodyRow = false; let lastTableEnd = 0; /** @type {Token | undefined} */ let currentTable; /** @type {Token | undefined} */ let currentBody; /** @type {Token | undefined} */ let currentCell; const map = new EditMap(); while (++index < events.length) { const event = events[index]; const token = event[1]; if (event[0] === "enter") { // Start of head. if (token.type === "tableHead") { afterHeadAwaitingFirstBodyRow = false; // Inject previous (body end and) table end. if (lastTableEnd !== 0) { flushTableEnd(map, context, lastTableEnd, currentTable, currentBody); currentBody = undefined; lastTableEnd = 0; } // Inject table start. currentTable = { type: "table", start: Object.assign({}, token.start), // Note: correct end is set later. end: Object.assign({}, token.end), }; map.add(index, 0, [["enter", currentTable, context]]); } else if (token.type === "tableRow" || token.type === "tableDelimiterRow") { inFirstCellAwaitingPipe = true; currentCell = undefined; lastCell = [0, 0, 0, 0]; cell = [0, index + 1, 0, 0]; // Inject table body start. if (afterHeadAwaitingFirstBodyRow) { afterHeadAwaitingFirstBodyRow = false; currentBody = { type: "tableBody", start: Object.assign({}, token.start), // Note: correct end is set later. end: Object.assign({}, token.end), }; map.add(index, 0, [["enter", currentBody, context]]); } rowKind = token.type === "tableDelimiterRow" ? 2 : currentBody ? 3 : 1; } // Cell data. else if (rowKind && (token.type === "data" || token.type === "tableDelimiterMarker" || token.type === "tableDelimiterFiller")) { inFirstCellAwaitingPipe = false; // First value in cell. if (cell[2] === 0) { if (lastCell[1] !== 0) { cell[0] = cell[1]; currentCell = flushCell(map, context, lastCell, rowKind, undefined, currentCell); lastCell = [0, 0, 0, 0]; } cell[2] = index; } } else if (token.type === "tableCellDivider") { if (inFirstCellAwaitingPipe) { inFirstCellAwaitingPipe = false; } else { if (lastCell[1] !== 0) { cell[0] = cell[1]; currentCell = flushCell(map, context, lastCell, rowKind, undefined, currentCell); } lastCell = cell; cell = [lastCell[1], index, 0, 0]; } } } // Exit events. else if (token.type === "tableHead") { afterHeadAwaitingFirstBodyRow = true; lastTableEnd = index; } else if (token.type === "tableRow" || token.type === "tableDelimiterRow") { lastTableEnd = index; if (lastCell[1] !== 0) { cell[0] = cell[1]; currentCell = flushCell(map, context, lastCell, rowKind, index, currentCell); } else if (cell[1] !== 0) { currentCell = flushCell(map, context, cell, rowKind, index, currentCell); } rowKind = 0; } else if (rowKind && (token.type === "data" || token.type === "tableDelimiterMarker" || token.type === "tableDelimiterFiller")) { cell[3] = index; } } if (lastTableEnd !== 0) { flushTableEnd(map, context, lastTableEnd, currentTable, currentBody); } map.consume(context.events); // To do: move this into `html`, when events are exposed there. // That’s what `markdown-rs` does. // That needs updates to `mdast-util-gfm-table`. index = -1; while (++index < context.events.length) { const event = context.events[index]; if (event[0] === "enter" && event[1].type === "table") { event[1]._align = gfmTableAlign(context.events, index); } } return events; } /** * Generate a cell. * * @param {EditMap} map * @param {Readonly} context * @param {Readonly} range * @param {RowKind} rowKind * @param {number | undefined} rowEnd * @param {Token | undefined} previousCell * @returns {Token | undefined} */ // eslint-disable-next-line max-params function flushCell(map, context, range, rowKind, rowEnd, previousCell) { // `markdown-rs` uses: // rowKind === 2 ? 'tableDelimiterCell' : 'tableCell' const groupName = rowKind === 1 ? "tableHeader" : rowKind === 2 ? "tableDelimiter" : "tableData"; // `markdown-rs` uses: // rowKind === 2 ? 'tableDelimiterCellValue' : 'tableCellText' const valueName = "tableContent"; // Insert an exit for the previous cell, if there is one. // // ```markdown // > | | aa | bb | cc | // ^-- exit // ^^^^-- this cell // ``` if (range[0] !== 0) { previousCell.end = Object.assign({}, getPoint(context.events, range[0])); map.add(range[0], 0, [["exit", previousCell, context]]); } // Insert enter of this cell. // // ```markdown // > | | aa | bb | cc | // ^-- enter // ^^^^-- this cell // ``` const now = getPoint(context.events, range[1]); previousCell = { type: groupName, start: Object.assign({}, now), // Note: correct end is set later. end: Object.assign({}, now), }; map.add(range[1], 0, [["enter", previousCell, context]]); // Insert text start at first data start and end at last data end, and // remove events between. // // ```markdown // > | | aa | bb | cc | // ^-- enter // ^-- exit // ^^^^-- this cell // ``` if (range[2] !== 0) { const relatedStart = getPoint(context.events, range[2]); const relatedEnd = getPoint(context.events, range[3]); /** @type {Token} */ const valueToken = { type: valueName, start: Object.assign({}, relatedStart), end: Object.assign({}, relatedEnd), }; map.add(range[2], 0, [["enter", valueToken, context]]); if (rowKind !== 2) { // Fix positional info on remaining events const start = context.events[range[2]]; const end = context.events[range[3]]; start[1].end = Object.assign({}, end[1].end); start[1].type = "chunkText"; start[1].contentType = "text"; // Remove if needed. if (range[3] > range[2] + 1) { const a = range[2] + 1; const b = range[3] - range[2] - 1; map.add(a, b, []); } } map.add(range[3] + 1, 0, [["exit", valueToken, context]]); } // Insert an exit for the last cell, if at the row end. // // ```markdown // > | | aa | bb | cc | // ^-- exit // ^^^^^^-- this cell (the last one contains two “between” parts) // ``` if (rowEnd !== undefined) { previousCell.end = Object.assign({}, getPoint(context.events, rowEnd)); map.add(rowEnd, 0, [["exit", previousCell, context]]); previousCell = undefined; } return previousCell; } /** * Generate table end (and table body end). * * @param {Readonly} map * @param {Readonly} context * @param {number} index * @param {Token} table * @param {Token | undefined} tableBody */ // eslint-disable-next-line max-params function flushTableEnd(map, context, index, table, tableBody) { /** @type {Array} */ const exits = []; const related = getPoint(context.events, index); if (tableBody) { tableBody.end = Object.assign({}, related); exits.push(["exit", tableBody, context]); } table.end = Object.assign({}, related); exits.push(["exit", table, context]); map.add(index + 1, 0, exits); } /** * @param {Readonly>} events * @param {number} index * @returns {Readonly} */ function getPoint(events, index) { const event = events[index]; const side = event[0] === "enter" ? "start" : "end"; return event[1][side]; } /** * @import {Extension, State, TokenizeContext, Tokenizer} from 'micromark-util-types' */ const tasklistCheck = { name: "tasklistCheck", tokenize: tokenizeTasklistCheck, }; /** * Create an HTML extension for `micromark` to support GFM task list items * syntax. * * @returns {Extension} * Extension for `micromark` that can be passed in `htmlExtensions` to * support GFM task list items when serializing to HTML. */ function gfmTaskListItem() { return { text: { [91]: tasklistCheck, }, }; } /** * @this {TokenizeContext} * @type {Tokenizer} */ function tokenizeTasklistCheck(effects, ok, nok) { const self = this; return open; /** * At start of task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function open(code) { if ( // Exit if there’s stuff before. self.previous !== null || // Exit if not in the first content that is the first child of a list // item. !self._gfmTasklistFirstContentOfListItem ) { return nok(code); } effects.enter("taskListCheck"); effects.enter("taskListCheckMarker"); effects.consume(code); effects.exit("taskListCheckMarker"); return inside; } /** * In task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function inside(code) { // Currently we match how GH works in files. // To match how GH works in comments, use `markdownSpace` (`[\t ]`) instead // of `markdownLineEndingOrSpace` (`[\t\n\r ]`). if (markdownLineEndingOrSpace(code)) { effects.enter("taskListCheckValueUnchecked"); effects.consume(code); effects.exit("taskListCheckValueUnchecked"); return close; } if (code === 88 || code === 120) { effects.enter("taskListCheckValueChecked"); effects.consume(code); effects.exit("taskListCheckValueChecked"); return close; } return nok(code); } /** * At close of task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function close(code) { if (code === 93) { effects.enter("taskListCheckMarker"); effects.consume(code); effects.exit("taskListCheckMarker"); effects.exit("taskListCheck"); return after; } return nok(code); } /** * @type {State} */ function after(code) { // EOL in paragraph means there must be something else after it. if (markdownLineEnding(code)) { return ok(code); } // Space or tab? // Check what comes after. if (markdownSpace(code)) { return effects.check( { tokenize: spaceThenNonSpace, }, ok, nok, )(code); } // EOF, or non-whitespace, both wrong. return nok(code); } } /** * @this {TokenizeContext} * @type {Tokenizer} */ function spaceThenNonSpace(effects, ok, nok) { return factorySpace(effects, after, "whitespace"); /** * After whitespace, after task list item check. * * ```markdown * > | * [x] y. * ^ * ``` * * @type {State} */ function after(code) { // EOF means there was nothing, so bad. // EOL means there’s content after it, so good. // Impossible to have more spaces. // Anything else is good. return code === null ? nok(code) : ok(code); } } /** * @typedef {import('micromark-extension-gfm-footnote').HtmlOptions} HtmlOptions * @typedef {import('micromark-extension-gfm-strikethrough').Options} Options * @typedef {import('micromark-util-types').Extension} Extension * @typedef {import('micromark-util-types').HtmlExtension} HtmlExtension */ /** * Create an extension for `micromark` to enable GFM syntax. * * @param {Options | null | undefined} [options] * Configuration (optional). * * Passed to `micromark-extens-gfm-strikethrough`. * @returns {Extension} * Extension for `micromark` that can be passed in `extensions` to enable GFM * syntax. */ function gfm(options) { return combineExtensions([gfmAutolinkLiteral(), gfmFootnote(), gfmStrikethrough(options), gfmTable(), gfmTaskListItem()]); } /** * @import {Root} from 'mdast' * @import {Options} from 'remark-gfm' * @import {} from 'remark-parse' * @import {} from 'remark-stringify' * @import {Processor} from 'unified' */ /** @type {Options} */ const emptyOptions$2 = {}; /** * Add support GFM (autolink literals, footnotes, strikethrough, tables, * tasklists). * * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {undefined} * Nothing. */ function remarkGfm(options) { // @ts-expect-error: TS is wrong about `this`. // eslint-disable-next-line unicorn/no-this-assignment const self = /** @type {Processor} */ (this); const settings = options || emptyOptions$2; const data = self.data(); const micromarkExtensions = data.micromarkExtensions || (data.micromarkExtensions = []); const fromMarkdownExtensions = data.fromMarkdownExtensions || (data.fromMarkdownExtensions = []); const toMarkdownExtensions = data.toMarkdownExtensions || (data.toMarkdownExtensions = []); micromarkExtensions.push(gfm(settings)); fromMarkdownExtensions.push(gfmFromMarkdown()); toMarkdownExtensions.push(gfmToMarkdown(settings)); } /** * @typedef {import('mdast').Root} Root * @typedef {import('mdast-util-to-markdown').Options} ToMarkdownOptions * @typedef {import('unified').Compiler} Compiler * @typedef {import('unified').Processor} Processor */ /** * Add support for serializing to markdown. * * @param {Readonly | null | undefined} [options] * Configuration (optional). * @returns {undefined} * Nothing. */ function remarkStringify(options) { /** @type {Processor} */ // @ts-expect-error: TS in JSDoc generates wrong types if `this` is typed regularly. const self = this; self.compiler = compiler; /** * @type {Compiler} */ function compiler(tree) { return toMarkdown(tree, { ...self.data("settings"), ...options, // Note: this option is not in the readme. // The goal is for it to be set by plugins on `data` instead of being // passed by users. extensions: self.data("toMarkdownExtensions") || [], }); } } /** * Throw a given error. * * @param {Error|null|undefined} [error] * Maybe error. * @returns {asserts error is null|undefined} */ function bail(error) { if (error) { throw error; } } var extend$1; var hasRequiredExtend; function requireExtend() { if (hasRequiredExtend) return extend$1; hasRequiredExtend = 1; var hasOwn = Object.prototype.hasOwnProperty; var toStr = Object.prototype.toString; var defineProperty = Object.defineProperty; var gOPD = Object.getOwnPropertyDescriptor; var isArray = function isArray(arr) { if (typeof Array.isArray === "function") { return Array.isArray(arr); } return toStr.call(arr) === "[object Array]"; }; var isPlainObject = function isPlainObject(obj) { if (!obj || toStr.call(obj) !== "[object Object]") { return false; } var hasOwnConstructor = hasOwn.call(obj, "constructor"); var hasIsPrototypeOf = obj.constructor && obj.constructor.prototype && hasOwn.call(obj.constructor.prototype, "isPrototypeOf"); // Not own constructor property must be Object if (obj.constructor && !hasOwnConstructor && !hasIsPrototypeOf) { return false; } // Own properties are enumerated firstly, so to speed up, // if last one is own, then all properties are own. var key; for (key in obj) { /**/ } return typeof key === "undefined" || hasOwn.call(obj, key); }; // If name is '__proto__', and Object.defineProperty is available, define __proto__ as an own property on target var setProperty = function setProperty(target, options) { if (defineProperty && options.name === "__proto__") { defineProperty(target, options.name, { enumerable: true, configurable: true, value: options.newValue, writable: true, }); } else { target[options.name] = options.newValue; } }; // Return undefined instead of __proto__ if '__proto__' is not an own property var getProperty = function getProperty(obj, name) { if (name === "__proto__") { if (!hasOwn.call(obj, name)) { return void 0; } else if (gOPD) { // In early versions of node, obj['__proto__'] is buggy when obj has // __proto__ as an own property. Object.getOwnPropertyDescriptor() works. return gOPD(obj, name).value; } } return obj[name]; }; extend$1 = function extend() { var options, name, src, copy, copyIsArray, clone; var target = arguments[0]; var i = 1; var length = arguments.length; var deep = false; // Handle a deep copy situation if (typeof target === "boolean") { deep = target; target = arguments[1] || {}; // skip the boolean and the target i = 2; } if (target == null || (typeof target !== "object" && typeof target !== "function")) { target = {}; } for (; i < length; ++i) { options = arguments[i]; // Only deal with non-null/undefined values if (options != null) { // Extend the base object for (name in options) { src = getProperty(target, name); copy = getProperty(options, name); // Prevent never-ending loop if (target !== copy) { // Recurse if we're merging plain objects or arrays if (deep && copy && (isPlainObject(copy) || (copyIsArray = isArray(copy)))) { if (copyIsArray) { copyIsArray = false; clone = src && isArray(src) ? src : []; } else { clone = src && isPlainObject(src) ? src : {}; } // Never move original objects, clone them setProperty(target, { name: name, newValue: extend(deep, clone, copy) }); // Don't bring in undefined values } else if (typeof copy !== "undefined") { setProperty(target, { name: name, newValue: copy }); } } } } } // Return the modified object return target; }; return extend$1; } var extendExports = requireExtend(); const extend = /*@__PURE__*/ getDefaultExportFromCjs(extendExports); function isPlainObject(value) { if (typeof value !== "object" || value === null) { return false; } const prototype = Object.getPrototypeOf(value); return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value); } // To do: remove `void`s // To do: remove `null` from output of our APIs, allow it as user APIs. /** * @typedef {(error?: Error | null | undefined, ...output: Array) => void} Callback * Callback. * * @typedef {(...input: Array) => any} Middleware * Ware. * * @typedef Pipeline * Pipeline. * @property {Run} run * Run the pipeline. * @property {Use} use * Add middleware. * * @typedef {(...input: Array) => void} Run * Call all middleware. * * Calls `done` on completion with either an error or the output of the * last middleware. * * > 👉 **Note**: as the length of input defines whether async functions get a * > `next` function, * > it’s recommended to keep `input` at one value normally. * * @typedef {(fn: Middleware) => Pipeline} Use * Add middleware. */ /** * Create new middleware. * * @returns {Pipeline} * Pipeline. */ function trough() { /** @type {Array} */ const fns = []; /** @type {Pipeline} */ const pipeline = { run, use }; return pipeline; /** @type {Run} */ function run(...values) { let middlewareIndex = -1; /** @type {Callback} */ const callback = values.pop(); if (typeof callback !== "function") { throw new TypeError("Expected function as last argument, not " + callback); } next(null, ...values); /** * Run the next `fn`, or we’re done. * * @param {Error | null | undefined} error * @param {Array} output */ function next(error, ...output) { const fn = fns[++middlewareIndex]; let index = -1; if (error) { callback(error); return; } // Copy non-nullish input into values. while (++index < values.length) { if (output[index] === null || output[index] === undefined) { output[index] = values[index]; } } // Save the newly created `output` for the next call. values = output; // Next or done. if (fn) { wrap$1(fn, next)(...output); } else { callback(null, ...output); } } } /** @type {Use} */ function use(middelware) { if (typeof middelware !== "function") { throw new TypeError("Expected `middelware` to be a function, not " + middelware); } fns.push(middelware); return pipeline; } } /** * Wrap `middleware` into a uniform interface. * * You can pass all input to the resulting function. * `callback` is then called with the output of `middleware`. * * If `middleware` accepts more arguments than the later given in input, * an extra `done` function is passed to it after that input, * which must be called by `middleware`. * * The first value in `input` is the main input value. * All other input values are the rest input values. * The values given to `callback` are the input values, * merged with every non-nullish output value. * * * if `middleware` throws an error, * returns a promise that is rejected, * or calls the given `done` function with an error, * `callback` is called with that error * * if `middleware` returns a value or returns a promise that is resolved, * that value is the main output value * * if `middleware` calls `done`, * all non-nullish values except for the first one (the error) overwrite the * output values * * @param {Middleware} middleware * Function to wrap. * @param {Callback} callback * Callback called with the output of `middleware`. * @returns {Run} * Wrapped middleware. */ function wrap$1(middleware, callback) { /** @type {boolean} */ let called; return wrapped; /** * Call `middleware`. * @this {any} * @param {Array} parameters * @returns {void} */ function wrapped(...parameters) { const fnExpectsCallback = middleware.length > parameters.length; /** @type {any} */ let result; if (fnExpectsCallback) { parameters.push(done); } try { result = middleware.apply(this, parameters); } catch (error) { const exception = /** @type {Error} */ (error); // Well, this is quite the pickle. // `middleware` received a callback and called it synchronously, but that // threw an error. // The only thing left to do is to throw the thing instead. if (fnExpectsCallback && called) { throw exception; } return done(exception); } if (!fnExpectsCallback) { if (result && result.then && typeof result.then === "function") { result.then(then, done); } else if (result instanceof Error) { done(result); } else { then(result); } } } /** * Call `callback`, only once. * * @type {Callback} */ function done(error, ...output) { if (!called) { called = true; callback(error, ...output); } } /** * Call `done` with one value. * * @param {any} [value] */ function then(value) { done(null, value); } } const CallableInstance = /** * @type {new , Result>(property: string | symbol) => (...parameters: Parameters) => Result} */ ( /** @type {unknown} */ ( /** * @this {Function} * @param {string | symbol} property * @returns {(...parameters: Array) => unknown} */ function (property) { const self = this; const constr = self.constructor; const proto = /** @type {Record} */ ( // Prototypes do exist. // type-coverage:ignore-next-line constr.prototype ); const value = proto[property]; /** @type {(...parameters: Array) => unknown} */ const apply = function () { return value.apply(apply, arguments); }; Object.setPrototypeOf(apply, proto); // Not needed for us in `unified`: we only call this on the `copy` // function, // and we don't need to add its fields (`length`, `name`) // over. // See also: GH-246. // const names = Object.getOwnPropertyNames(value) // // for (const p of names) { // const descriptor = Object.getOwnPropertyDescriptor(value, p) // if (descriptor) Object.defineProperty(apply, p, descriptor) // } return apply; } ) ); /** * @typedef {import('trough').Pipeline} Pipeline * * @typedef {import('unist').Node} Node * * @typedef {import('vfile').Compatible} Compatible * @typedef {import('vfile').Value} Value * * @typedef {import('../index.js').CompileResultMap} CompileResultMap * @typedef {import('../index.js').Data} Data * @typedef {import('../index.js').Settings} Settings */ // To do: next major: drop `Compiler`, `Parser`: prefer lowercase. // To do: we could start yielding `never` in TS when a parser is missing and // `parse` is called. // Currently, we allow directly setting `processor.parser`, which is untyped. const own$4 = {}.hasOwnProperty; /** * @template {Node | undefined} [ParseTree=undefined] * Output of `parse` (optional). * @template {Node | undefined} [HeadTree=undefined] * Input for `run` (optional). * @template {Node | undefined} [TailTree=undefined] * Output for `run` (optional). * @template {Node | undefined} [CompileTree=undefined] * Input of `stringify` (optional). * @template {CompileResults | undefined} [CompileResult=undefined] * Output of `stringify` (optional). * @extends {CallableInstance<[], Processor>} */ class Processor extends CallableInstance { /** * Create a processor. */ constructor() { // If `Processor()` is called (w/o new), `copy` is called instead. super("copy"); /** * Compiler to use (deprecated). * * @deprecated * Use `compiler` instead. * @type {( * Compiler< * CompileTree extends undefined ? Node : CompileTree, * CompileResult extends undefined ? CompileResults : CompileResult * > | * undefined * )} */ this.Compiler = undefined; /** * Parser to use (deprecated). * * @deprecated * Use `parser` instead. * @type {( * Parser | * undefined * )} */ this.Parser = undefined; // Note: the following fields are considered private. // However, they are needed for tests, and TSC generates an untyped // `private freezeIndex` field for, which trips `type-coverage` up. // Instead, we use `@deprecated` to visualize that they shouldn’t be used. /** * Internal list of configured plugins. * * @deprecated * This is a private internal property and should not be used. * @type {Array>>} */ this.attachers = []; /** * Compiler to use. * * @type {( * Compiler< * CompileTree extends undefined ? Node : CompileTree, * CompileResult extends undefined ? CompileResults : CompileResult * > | * undefined * )} */ this.compiler = undefined; /** * Internal state to track where we are while freezing. * * @deprecated * This is a private internal property and should not be used. * @type {number} */ this.freezeIndex = -1; /** * Internal state to track whether we’re frozen. * * @deprecated * This is a private internal property and should not be used. * @type {boolean | undefined} */ this.frozen = undefined; /** * Internal state. * * @deprecated * This is a private internal property and should not be used. * @type {Data} */ this.namespace = {}; /** * Parser to use. * * @type {( * Parser | * undefined * )} */ this.parser = undefined; /** * Internal list of configured transformers. * * @deprecated * This is a private internal property and should not be used. * @type {Pipeline} */ this.transformers = trough(); } /** * Copy a processor. * * @deprecated * This is a private internal method and should not be used. * @returns {Processor} * New *unfrozen* processor ({@linkcode Processor}) that is * configured to work the same as its ancestor. * When the descendant processor is configured in the future it does not * affect the ancestral processor. */ copy() { // Cast as the type parameters will be the same after attaching. const destination = /** @type {Processor} */ (new Processor()); let index = -1; while (++index < this.attachers.length) { const attacher = this.attachers[index]; destination.use(...attacher); } destination.data(extend(true, {}, this.namespace)); return destination; } /** * Configure the processor with info available to all plugins. * Information is stored in an object. * * Typically, options can be given to a specific plugin, but sometimes it * makes sense to have information shared with several plugins. * For example, a list of HTML elements that are self-closing, which is * needed during all phases. * * > **Note**: setting information cannot occur on *frozen* processors. * > Call the processor first to create a new unfrozen processor. * * > **Note**: to register custom data in TypeScript, augment the * > {@linkcode Data} interface. * * @example * This example show how to get and set info: * * ```js * import {unified} from 'unified' * * const processor = unified().data('alpha', 'bravo') * * processor.data('alpha') // => 'bravo' * * processor.data() // => {alpha: 'bravo'} * * processor.data({charlie: 'delta'}) * * processor.data() // => {charlie: 'delta'} * ``` * * @template {keyof Data} Key * * @overload * @returns {Data} * * @overload * @param {Data} dataset * @returns {Processor} * * @overload * @param {Key} key * @returns {Data[Key]} * * @overload * @param {Key} key * @param {Data[Key]} value * @returns {Processor} * * @param {Data | Key} [key] * Key to get or set, or entire dataset to set, or nothing to get the * entire dataset (optional). * @param {Data[Key]} [value] * Value to set (optional). * @returns {unknown} * The current processor when setting, the value at `key` when getting, or * the entire dataset when getting without key. */ data(key, value) { if (typeof key === "string") { // Set `key`. if (arguments.length === 2) { assertUnfrozen("data", this.frozen); this.namespace[key] = value; return this; } // Get `key`. return (own$4.call(this.namespace, key) && this.namespace[key]) || undefined; } // Set space. if (key) { assertUnfrozen("data", this.frozen); this.namespace = key; return this; } // Get space. return this.namespace; } /** * Freeze a processor. * * Frozen processors are meant to be extended and not to be configured * directly. * * When a processor is frozen it cannot be unfrozen. * New processors working the same way can be created by calling the * processor. * * It’s possible to freeze processors explicitly by calling `.freeze()`. * Processors freeze automatically when `.parse()`, `.run()`, `.runSync()`, * `.stringify()`, `.process()`, or `.processSync()` are called. * * @returns {Processor} * The current processor. */ freeze() { if (this.frozen) { return this; } // Cast so that we can type plugins easier. // Plugins are supposed to be usable on different processors, not just on // this exact processor. const self = /** @type {Processor} */ (/** @type {unknown} */ (this)); while (++this.freezeIndex < this.attachers.length) { const [attacher, ...options] = this.attachers[this.freezeIndex]; if (options[0] === false) { continue; } if (options[0] === true) { options[0] = undefined; } const transformer = attacher.call(self, ...options); if (typeof transformer === "function") { this.transformers.use(transformer); } } this.frozen = true; this.freezeIndex = Number.POSITIVE_INFINITY; return this; } /** * Parse text to a syntax tree. * * > **Note**: `parse` freezes the processor if not already *frozen*. * * > **Note**: `parse` performs the parse phase, not the run phase or other * > phases. * * @param {Compatible | undefined} [file] * file to parse (optional); typically `string` or `VFile`; any value * accepted as `x` in `new VFile(x)`. * @returns {ParseTree extends undefined ? Node : ParseTree} * Syntax tree representing `file`. */ parse(file) { this.freeze(); const realFile = vfile(file); const parser = this.parser || this.Parser; assertParser("parse", parser); return parser(String(realFile), realFile); } /** * Process the given file as configured on the processor. * * > **Note**: `process` freezes the processor if not already *frozen*. * * > **Note**: `process` performs the parse, run, and stringify phases. * * @overload * @param {Compatible | undefined} file * @param {ProcessCallback>} done * @returns {undefined} * * @overload * @param {Compatible | undefined} [file] * @returns {Promise>} * * @param {Compatible | undefined} [file] * File (optional); typically `string` or `VFile`]; any value accepted as * `x` in `new VFile(x)`. * @param {ProcessCallback> | undefined} [done] * Callback (optional). * @returns {Promise | undefined} * Nothing if `done` is given. * Otherwise a promise, rejected with a fatal error or resolved with the * processed file. * * The parsed, transformed, and compiled value is available at * `file.value` (see note). * * > **Note**: unified typically compiles by serializing: most * > compilers return `string` (or `Uint8Array`). * > Some compilers, such as the one configured with * > [`rehype-react`][rehype-react], return other values (in this case, a * > React tree). * > If you’re using a compiler that doesn’t serialize, expect different * > result values. * > * > To register custom results in TypeScript, add them to * > {@linkcode CompileResultMap}. * * [rehype-react]: https://github.com/rehypejs/rehype-react */ process(file, done) { const self = this; this.freeze(); assertParser("process", this.parser || this.Parser); assertCompiler("process", this.compiler || this.Compiler); return done ? executor(undefined, done) : new Promise(executor); // Note: `void`s needed for TS. /** * @param {((file: VFileWithOutput) => undefined | void) | undefined} resolve * @param {(error: Error | undefined) => undefined | void} reject * @returns {undefined} */ function executor(resolve, reject) { const realFile = vfile(file); // Assume `ParseTree` (the result of the parser) matches `HeadTree` (the // input of the first transform). const parseTree = /** @type {HeadTree extends undefined ? Node : HeadTree} */ (/** @type {unknown} */ (self.parse(realFile))); self.run(parseTree, realFile, function (error, tree, file) { if (error || !tree || !file) { return realDone(error); } // Assume `TailTree` (the output of the last transform) matches // `CompileTree` (the input of the compiler). const compileTree = /** @type {CompileTree extends undefined ? Node : CompileTree} */ (/** @type {unknown} */ (tree)); const compileResult = self.stringify(compileTree, file); if (looksLikeAValue(compileResult)) { file.value = compileResult; } else { file.result = compileResult; } realDone(error, /** @type {VFileWithOutput} */ (file)); }); /** * @param {Error | undefined} error * @param {VFileWithOutput | undefined} [file] * @returns {undefined} */ function realDone(error, file) { if (error || !file) { reject(error); } else if (resolve) { resolve(file); } else { done(undefined, file); } } } } /** * Process the given file as configured on the processor. * * An error is thrown if asynchronous transforms are configured. * * > **Note**: `processSync` freezes the processor if not already *frozen*. * * > **Note**: `processSync` performs the parse, run, and stringify phases. * * @param {Compatible | undefined} [file] * File (optional); typically `string` or `VFile`; any value accepted as * `x` in `new VFile(x)`. * @returns {VFileWithOutput} * The processed file. * * The parsed, transformed, and compiled value is available at * `file.value` (see note). * * > **Note**: unified typically compiles by serializing: most * > compilers return `string` (or `Uint8Array`). * > Some compilers, such as the one configured with * > [`rehype-react`][rehype-react], return other values (in this case, a * > React tree). * > If you’re using a compiler that doesn’t serialize, expect different * > result values. * > * > To register custom results in TypeScript, add them to * > {@linkcode CompileResultMap}. * * [rehype-react]: https://github.com/rehypejs/rehype-react */ processSync(file) { /** @type {boolean} */ let complete = false; /** @type {VFileWithOutput | undefined} */ let result; this.freeze(); assertParser("processSync", this.parser || this.Parser); assertCompiler("processSync", this.compiler || this.Compiler); this.process(file, realDone); assertDone("processSync", "process", complete); return result; /** * @type {ProcessCallback>} */ function realDone(error, file) { complete = true; bail(error); result = file; } } /** * Run *transformers* on a syntax tree. * * > **Note**: `run` freezes the processor if not already *frozen*. * * > **Note**: `run` performs the run phase, not other phases. * * @overload * @param {HeadTree extends undefined ? Node : HeadTree} tree * @param {RunCallback} done * @returns {undefined} * * @overload * @param {HeadTree extends undefined ? Node : HeadTree} tree * @param {Compatible | undefined} file * @param {RunCallback} done * @returns {undefined} * * @overload * @param {HeadTree extends undefined ? Node : HeadTree} tree * @param {Compatible | undefined} [file] * @returns {Promise} * * @param {HeadTree extends undefined ? Node : HeadTree} tree * Tree to transform and inspect. * @param {( * RunCallback | * Compatible * )} [file] * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. * @param {RunCallback} [done] * Callback (optional). * @returns {Promise | undefined} * Nothing if `done` is given. * Otherwise, a promise rejected with a fatal error or resolved with the * transformed tree. */ run(tree, file, done) { assertNode(tree); this.freeze(); const transformers = this.transformers; if (!done && typeof file === "function") { done = file; file = undefined; } return done ? executor(undefined, done) : new Promise(executor); // Note: `void`s needed for TS. /** * @param {( * ((tree: TailTree extends undefined ? Node : TailTree) => undefined | void) | * undefined * )} resolve * @param {(error: Error) => undefined | void} reject * @returns {undefined} */ function executor(resolve, reject) { const realFile = vfile(file); transformers.run(tree, realFile, realDone); /** * @param {Error | undefined} error * @param {Node} outputTree * @param {VFile} file * @returns {undefined} */ function realDone(error, outputTree, file) { const resultingTree = /** @type {TailTree extends undefined ? Node : TailTree} */ (outputTree || tree); if (error) { reject(error); } else if (resolve) { resolve(resultingTree); } else { done(undefined, resultingTree, file); } } } } /** * Run *transformers* on a syntax tree. * * An error is thrown if asynchronous transforms are configured. * * > **Note**: `runSync` freezes the processor if not already *frozen*. * * > **Note**: `runSync` performs the run phase, not other phases. * * @param {HeadTree extends undefined ? Node : HeadTree} tree * Tree to transform and inspect. * @param {Compatible | undefined} [file] * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. * @returns {TailTree extends undefined ? Node : TailTree} * Transformed tree. */ runSync(tree, file) { /** @type {boolean} */ let complete = false; /** @type {(TailTree extends undefined ? Node : TailTree) | undefined} */ let result; this.run(tree, file, realDone); assertDone("runSync", "run", complete); return result; /** * @type {RunCallback} */ function realDone(error, tree) { bail(error); result = tree; complete = true; } } /** * Compile a syntax tree. * * > **Note**: `stringify` freezes the processor if not already *frozen*. * * > **Note**: `stringify` performs the stringify phase, not the run phase * > or other phases. * * @param {CompileTree extends undefined ? Node : CompileTree} tree * Tree to compile. * @param {Compatible | undefined} [file] * File associated with `node` (optional); any value accepted as `x` in * `new VFile(x)`. * @returns {CompileResult extends undefined ? Value : CompileResult} * Textual representation of the tree (see note). * * > **Note**: unified typically compiles by serializing: most compilers * > return `string` (or `Uint8Array`). * > Some compilers, such as the one configured with * > [`rehype-react`][rehype-react], return other values (in this case, a * > React tree). * > If you’re using a compiler that doesn’t serialize, expect different * > result values. * > * > To register custom results in TypeScript, add them to * > {@linkcode CompileResultMap}. * * [rehype-react]: https://github.com/rehypejs/rehype-react */ stringify(tree, file) { this.freeze(); const realFile = vfile(file); const compiler = this.compiler || this.Compiler; assertCompiler("stringify", compiler); assertNode(tree); return compiler(tree, realFile); } /** * Configure the processor to use a plugin, a list of usable values, or a * preset. * * If the processor is already using a plugin, the previous plugin * configuration is changed based on the options that are passed in. * In other words, the plugin is not added a second time. * * > **Note**: `use` cannot be called on *frozen* processors. * > Call the processor first to create a new unfrozen processor. * * @example * There are many ways to pass plugins to `.use()`. * This example gives an overview: * * ```js * import {unified} from 'unified' * * unified() * // Plugin with options: * .use(pluginA, {x: true, y: true}) * // Passing the same plugin again merges configuration (to `{x: true, y: false, z: true}`): * .use(pluginA, {y: false, z: true}) * // Plugins: * .use([pluginB, pluginC]) * // Two plugins, the second with options: * .use([pluginD, [pluginE, {}]]) * // Preset with plugins and settings: * .use({plugins: [pluginF, [pluginG, {}]], settings: {position: false}}) * // Settings only: * .use({settings: {position: false}}) * ``` * * @template {Array} [Parameters=[]] * @template {Node | string | undefined} [Input=undefined] * @template [Output=Input] * * @overload * @param {Preset | null | undefined} [preset] * @returns {Processor} * * @overload * @param {PluggableList} list * @returns {Processor} * * @overload * @param {Plugin} plugin * @param {...(Parameters | [boolean])} parameters * @returns {UsePlugin} * * @param {PluggableList | Plugin | Preset | null | undefined} value * Usable value. * @param {...unknown} parameters * Parameters, when a plugin is given as a usable value. * @returns {Processor} * Current processor. */ use(value, ...parameters) { const attachers = this.attachers; const namespace = this.namespace; assertUnfrozen("use", this.frozen); if (value === null || value === undefined); else if (typeof value === "function") { addPlugin(value, parameters); } else if (typeof value === "object") { if (Array.isArray(value)) { addList(value); } else { addPreset(value); } } else { throw new TypeError("Expected usable value, not `" + value + "`"); } return this; /** * @param {Pluggable} value * @returns {undefined} */ function add(value) { if (typeof value === "function") { addPlugin(value, []); } else if (typeof value === "object") { if (Array.isArray(value)) { const [plugin, ...parameters] = /** @type {PluginTuple>} */ (value); addPlugin(plugin, parameters); } else { addPreset(value); } } else { throw new TypeError("Expected usable value, not `" + value + "`"); } } /** * @param {Preset} result * @returns {undefined} */ function addPreset(result) { if (!("plugins" in result) && !("settings" in result)) { throw new Error( "Expected usable value but received an empty preset, which is probably a mistake: presets typically come with `plugins` and sometimes with `settings`, but this has neither", ); } addList(result.plugins); if (result.settings) { namespace.settings = extend(true, namespace.settings, result.settings); } } /** * @param {PluggableList | null | undefined} plugins * @returns {undefined} */ function addList(plugins) { let index = -1; if (plugins === null || plugins === undefined); else if (Array.isArray(plugins)) { while (++index < plugins.length) { const thing = plugins[index]; add(thing); } } else { throw new TypeError("Expected a list of plugins, not `" + plugins + "`"); } } /** * @param {Plugin} plugin * @param {Array} parameters * @returns {undefined} */ function addPlugin(plugin, parameters) { let index = -1; let entryIndex = -1; while (++index < attachers.length) { if (attachers[index][0] === plugin) { entryIndex = index; break; } } if (entryIndex === -1) { attachers.push([plugin, ...parameters]); } // Only set if there was at least a `primary` value, otherwise we’d change // `arguments.length`. else if (parameters.length > 0) { let [primary, ...rest] = parameters; const currentPrimary = attachers[entryIndex][1]; if (isPlainObject(currentPrimary) && isPlainObject(primary)) { primary = extend(true, currentPrimary, primary); } attachers[entryIndex] = [plugin, primary, ...rest]; } } } } // Note: this returns a *callable* instance. // That’s why it’s documented as a function. /** * Create a new processor. * * @example * This example shows how a new processor can be created (from `remark`) and linked * to **stdin**(4) and **stdout**(4). * * ```js * import process from 'node:process' * import concatStream from 'concat-stream' * import {remark} from 'remark' * * process.stdin.pipe( * concatStream(function (buf) { * process.stdout.write(String(remark().processSync(buf))) * }) * ) * ``` * * @returns * New *unfrozen* processor (`processor`). * * This processor is configured to work the same as its ancestor. * When the descendant processor is configured in the future it does not * affect the ancestral processor. */ const unified = new Processor().freeze(); /** * Assert a parser is available. * * @param {string} name * @param {unknown} value * @returns {asserts value is Parser} */ function assertParser(name, value) { if (typeof value !== "function") { throw new TypeError("Cannot `" + name + "` without `parser`"); } } /** * Assert a compiler is available. * * @param {string} name * @param {unknown} value * @returns {asserts value is Compiler} */ function assertCompiler(name, value) { if (typeof value !== "function") { throw new TypeError("Cannot `" + name + "` without `compiler`"); } } /** * Assert the processor is not frozen. * * @param {string} name * @param {unknown} frozen * @returns {asserts frozen is false} */ function assertUnfrozen(name, frozen) { if (frozen) { throw new Error("Cannot call `" + name + "` on a frozen processor.\nCreate a new processor first, by calling it: use `processor()` instead of `processor`."); } } /** * Assert `node` is a unist node. * * @param {unknown} node * @returns {asserts node is Node} */ function assertNode(node) { // `isPlainObj` unfortunately uses `any` instead of `unknown`. // type-coverage:ignore-next-line if (!isPlainObject(node) || typeof node.type !== "string") { throw new TypeError("Expected node, got `" + node + "`"); // Fine. } } /** * Assert that `complete` is `true`. * * @param {string} name * @param {string} asyncName * @param {unknown} complete * @returns {asserts complete is true} */ function assertDone(name, asyncName, complete) { if (!complete) { throw new Error("`" + name + "` finished async. Use `" + asyncName + "` instead"); } } /** * @param {Compatible | undefined} [value] * @returns {VFile} */ function vfile(value) { return looksLikeAVFile(value) ? value : new VFile(value); } /** * @param {Compatible | undefined} [value] * @returns {value is VFile} */ function looksLikeAVFile(value) { return Boolean(value && typeof value === "object" && "message" in value && "messages" in value); } /** * @param {unknown} [value] * @returns {value is Value} */ function looksLikeAValue(value) { return typeof value === "string" || isUint8Array(value); } /** * Assert `value` is an `Uint8Array`. * * @param {unknown} value * thing. * @returns {value is Uint8Array} * Whether `value` is an `Uint8Array`. */ function isUint8Array(value) { return Boolean(value && typeof value === "object" && "byteLength" in value && "byteOffset" in value); } /** * @import { * Comment as HastComment, * Doctype as HastDoctype, * Element as HastElement, * Nodes as HastNodes, * RootContent as HastRootContent, * Root as HastRoot, * Text as HastText, * } from 'hast' */ /** * Transform a DOM tree to a hast tree. * * @param {Node} tree * DOM tree to transform. * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {HastNodes} * Equivalent hast node. */ function fromDom(tree, options) { return transform(tree, {}) || { type: "root", children: [] }; } /** * @param {Node} node * DOM node to transform. * @param {Options} options * Configuration. * @returns {HastNodes | undefined} * Equivalent hast node. * * Note that certain legacy DOM nodes (i.e., Attr nodes (2), CDATA, processing instructions) */ function transform(node, options) { const transformed = one$1(node, options); if (transformed && options.afterTransform) options.afterTransform(node, transformed); return transformed; } /** * @param {Node} node * DOM node to transform. * @param {Options} options * Configuration. * @returns {HastNodes | undefined} * Equivalent hast node. */ function one$1(node, options) { switch (node.nodeType) { case 1 /* Element */: { const domNode = /** @type {Element} */ (node); return element$1(domNode, options); } // Ignore: Attr (2). case 3 /* Text */: { const domNode = /** @type {Text} */ (node); return text$4(domNode); } // Ignore: CDATA (4). // Removed: Entity reference (5) // Removed: Entity (6) // Ignore: Processing instruction (7). case 8 /* Comment */: { const domNode = /** @type {Comment} */ (node); return comment$1(domNode); } case 9 /* Document */: { const domNode = /** @type {Document} */ (node); return root$2(domNode, options); } case 10 /* Document type */: { return doctype$1(); } case 11 /* Document fragment */: { const domNode = /** @type {DocumentFragment} */ (node); return root$2(domNode, options); } default: { return undefined; } } } /** * Transform a document. * * @param {Document | DocumentFragment} node * DOM node to transform. * @param {Options} options * Configuration. * @returns {HastRoot} * Equivalent hast node. */ function root$2(node, options) { return { type: "root", children: all$1(node, options) }; } /** * Transform a doctype. * * @returns {HastDoctype} * Equivalent hast node. */ function doctype$1() { return { type: "doctype" }; } /** * Transform a text. * * @param {Text} node * DOM node to transform. * @returns {HastText} * Equivalent hast node. */ function text$4(node) { return { type: "text", value: node.nodeValue || "" }; } /** * Transform a comment. * * @param {Comment} node * DOM node to transform. * @returns {HastComment} * Equivalent hast node. */ function comment$1(node) { return { type: "comment", value: node.nodeValue || "" }; } /** * Transform an element. * * @param {Element} node * DOM node to transform. * @param {Options} options * Configuration. * @returns {HastElement} * Equivalent hast node. */ function element$1(node, options) { const space = node.namespaceURI; const x = space === webNamespaces.svg ? s$1 : h$2; const tagName = space === webNamespaces.html ? node.tagName.toLowerCase() : node.tagName; /** @type {DocumentFragment | Element} */ const content = // @ts-expect-error: DOM types are wrong, content can exist. space === webNamespaces.html && tagName === "template" ? node.content : node; const attributes = node.getAttributeNames(); /** @type {Record} */ const properties = {}; let index = -1; while (++index < attributes.length) { properties[attributes[index]] = node.getAttribute(attributes[index]) || ""; } return x(tagName, properties, all$1(content, options)); } /** * Transform child nodes in a parent. * * @param {Document | DocumentFragment | Element} node * DOM node to transform. * @param {Options} options * Configuration. * @returns {Array} * Equivalent hast nodes. */ function all$1(node, options) { const nodes = node.childNodes; /** @type {Array} */ const children = []; let index = -1; while (++index < nodes.length) { const child = transform(nodes[index], options); if (child !== undefined) { // @ts-expect-error Assume no document inside document. children.push(child); } } return children; } var Ae$1 = Object.defineProperty; var Le$1 = (n, e, o) => (e in n ? Ae$1(n, e, { enumerable: true, configurable: true, writable: true, value: o }) : (n[e] = o)); var k$2 = (n, e, o) => Le$1(n, typeof e != "symbol" ? e + "" : e, o); function xe$1(n) { const e = Array.from(n.classList).filter((o) => !o.startsWith("bn-")) || []; e.length > 0 ? (n.className = e.join(" ")) : n.removeAttribute("class"); } function Ee$1(n, e, o, t) { var a; let r; if (e) if (typeof e == "string") r = T$1([e], n.pmSchema); else if (Array.isArray(e)) r = T$1(e, n.pmSchema); else if (e.type === "tableContent") r = kt$1(e, n.pmSchema); else throw new O(e.type); else throw new Error("blockContent is required"); const i = ((t == null ? void 0 : t.document) ?? document).createDocumentFragment(); for (const c of r) if (c.type.name !== "text" && n.schema.inlineContentSchema[c.type.name]) { const l = n.schema.inlineContentSpecs[c.type.name].implementation; if (l) { const u = wt$1(c, n.schema.inlineContentSchema, n.schema.styleSchema), h = l.toExternalHTML ? l.toExternalHTML(u, n) : l.render.call( { renderType: "dom", props: void 0, }, u, () => {}, n, ); if (h) { if ((i.appendChild(h.dom), h.contentDOM)) { const f = o.serializeFragment(c.content, t); ((h.contentDOM.dataset.editable = ""), h.contentDOM.appendChild(f)); } continue; } } } else if (c.type.name === "text") { let l = document.createTextNode(c.textContent); for (const u of c.marks.toReversed()) if (u.type.name in n.schema.styleSpecs) { const h = (n.schema.styleSpecs[u.type.name].implementation.toExternalHTML ?? n.schema.styleSpecs[u.type.name].implementation.render)(u.attrs.stringValue, n); (h.contentDOM.appendChild(l), (l = h.dom)); } else { const h = u.type.spec.toDOM(u, true), f = DOMSerializer.renderSpec(document, h); (f.contentDOM.appendChild(l), (l = f.dom)); } i.appendChild(l); } else { const l = o.serializeFragment(Fragment.from([c]), t); i.appendChild(l); } return (i.childNodes.length === 1 && ((a = i.firstChild) == null ? void 0 : a.nodeType) === 1 && xe$1(i.firstChild), i); } function Bt(n, e, o, t, r, s, i, a) { var b, y, w, v, x, I, O, M, T; const c = (a == null ? void 0 : a.document) ?? document, l = e.pmSchema.nodes.blockContainer, u = o.props || {}; for (const [C, S] of Object.entries(e.schema.blockSchema[o.type].propSchema)) !(C in u) && S.default !== void 0 && (u[C] = S.default); const h = (y = (b = l.spec) == null ? void 0 : b.toDOM) == null ? void 0 : y.call( b, l.create({ id: o.id, ...u, }), ), f = Array.from(h.dom.attributes), m = e.blockImplementations[o.type].implementation, p = ((w = m.toExternalHTML) == null ? void 0 : w.call({}, { ...o, props: u }, e, { nestingLevel: i, })) || m.render.call({}, { ...o, props: u }, e), d = c.createDocumentFragment(); if (p.dom.classList.contains("bn-block-content")) { const C = [...f, ...Array.from(p.dom.attributes)].filter( (S) => S.name.startsWith("data") && S.name !== "data-content-type" && S.name !== "data-file-block" && S.name !== "data-node-view-wrapper" && S.name !== "data-node-type" && S.name !== "data-id" && S.name !== "data-editable", ); for (const S of C) p.dom.firstChild.setAttribute(S.name, S.value); (xe$1(p.dom.firstChild), i > 0 && p.dom.firstChild.setAttribute("data-nesting-level", i.toString()), d.append(...Array.from(p.dom.childNodes))); } else (d.append(p.dom), i > 0 && p.dom.setAttribute("data-nesting-level", i.toString())); if (p.contentDOM && o.content) { const C = Ee$1( e, o.content, // TODO t, a, ); p.contentDOM.appendChild(C); } let g; if ((r.has(o.type) ? (g = "OL") : s.has(o.type) && (g = "UL"), g)) { if (((v = n.lastChild) == null ? void 0 : v.nodeName) !== g) { const C = c.createElement(g); (g === "OL" && "start" in u && u.start && (u == null ? void 0 : u.start) !== 1 && C.setAttribute("start", u.start + ""), n.append(C)); } n.lastChild.appendChild(d); } else n.append(d); if (o.children && o.children.length > 0) { const C = c.createDocumentFragment(); if ((Ie$1(C, e, o.children, t, r, s, i + 1, a), ((x = n.lastChild) == null ? void 0 : x.nodeName) === "UL" || ((I = n.lastChild) == null ? void 0 : I.nodeName) === "OL")) for (; ((O = C.firstChild) == null ? void 0 : O.nodeName) === "UL" || ((M = C.firstChild) == null ? void 0 : M.nodeName) === "OL"; ) n.lastChild.lastChild.appendChild(C.firstChild); "childrenDOM" in p && p.childrenDOM ? p.childrenDOM.append(C) : e.pmSchema.nodes[o.type].isInGroup("blockContent") ? n.append(C) : (T = p.contentDOM) == null || T.append(C); } } const Ie$1 = (n, e, o, t, r, s, i = 0, a) => { for (const c of o) Bt(n, e, c, t, r, s, i, a); }, Tt = (n, e, o, t, r, s) => { const a = ((s == null ? void 0 : s.document) ?? document).createDocumentFragment(); return (Ie$1(a, n, e, o, t, r, 0, s), a); }, Be = (n, e) => { const o = DOMSerializer.fromSchema(n); return { exportBlocks: (t, r) => { const s = Tt(e, t, o, /* @__PURE__ */ new Set(["numberedListItem"]), /* @__PURE__ */ new Set(["bulletListItem", "checkListItem", "toggleListItem"]), r), i = document.createElement("div"); return (i.append(s), i.innerHTML); }, exportInlineContent: (t, r) => { const s = Ee$1(e, t, o, r), i = document.createElement("div"); return (i.append(s.cloneNode(true)), i.innerHTML); }, }; }; function Dt(n, e) { if (e === 0) return; const o = n.resolve(e); for (let t = o.depth; t > 0; t--) { const r = o.node(t); if (At$1(r)) return r.attrs.id; } } function Ot(n) { return ( n.getMeta("paste") ? { type: "paste" } : n.getMeta("uiEvent") === "drop" ? { type: "drop" } : n.getMeta("history$") ? { type: n.getMeta("history$").redo ? "redo" : "undo", } : n.getMeta("y-sync$") ? n.getMeta("y-sync$").isUndoRedoOperation ? { type: "undo-redo" } : { type: "yjs-remote" } : { type: "local" } ); } function se$1(n) { const e = "__root__", o = {}, t = {}, r = ht(n); return ( n.descendants((s, i) => { if (!At$1(s)) return true; const a = Dt(n, i), c = a ?? e; t[c] || (t[c] = []); const l = L$2(s, r); return ((o[s.attrs.id] = { block: l, parentId: a }), t[c].push(s.attrs.id), true); }), { byId: o, childrenByParent: t } ); } function Pt(n, e) { const o = /* @__PURE__ */ new Set(); if (!n || !e) return o; const t = new Set(n), r = e.filter((d) => t.has(d)), s = n.filter((d) => r.includes(d)); if (s.length <= 1 || r.length <= 1) return o; const i = {}; for (let d = 0; d < s.length; d++) i[s[d]] = d; const a = r.map((d) => i[d]), c = a.length, l = [], u = [], h = new Array(c).fill(-1), f = (d, g) => { let b = 0, y = d.length; for (; b < y; ) { const w = (b + y) >>> 1; d[w] < g ? (b = w + 1) : (y = w); } return b; }; for (let d = 0; d < c; d++) { const g = a[d], b = f(l, g); (b > 0 && (h[d] = u[b - 1]), b === l.length ? (l.push(g), u.push(d)) : ((l[b] = g), (u[b] = d))); } const m = /* @__PURE__ */ new Set(); let p = u[u.length - 1] ?? -1; for (; p !== -1; ) (m.add(p), (p = h[p])); for (let d = 0; d < r.length; d++) m.has(d) || o.add(r[d]); return o; } function Mt(n, e = []) { const o = Ot(n), t = combineTransactionSteps(n.before, [n, ...e]), r = se$1(t.before), s = se$1(t.doc), i = [], a = /* @__PURE__ */ new Set(); (Object.keys(s.byId) .filter((m) => !(m in r.byId)) .forEach((m) => { (i.push({ type: "insert", block: s.byId[m].block, source: o, prevBlock: void 0, }), a.add(m)); }), Object.keys(r.byId) .filter((m) => !(m in s.byId)) .forEach((m) => { (i.push({ type: "delete", block: r.byId[m].block, source: o, prevBlock: void 0, }), a.add(m)); }), Object.keys(s.byId) .filter((m) => m in r.byId) .forEach((m) => { var b, y; const p = r.byId[m], d = s.byId[m]; p.parentId !== d.parentId ? (i.push({ type: "move", block: d.block, prevBlock: p.block, source: o, prevParent: p.parentId ? (b = r.byId[p.parentId]) == null ? void 0 : b.block : void 0, currentParent: d.parentId ? (y = s.byId[d.parentId]) == null ? void 0 : y.block : void 0, }), a.add(m)) : He({ ...p.block, children: void 0 }, { ...d.block, children: void 0 }) || (i.push({ type: "update", block: d.block, prevBlock: p.block, source: o, }), a.add(m)); })); const c = r.childrenByParent, l = s.childrenByParent, u = "__root__", h = /* @__PURE__ */ new Set([...Object.keys(c), ...Object.keys(l)]), f = /* @__PURE__ */ new Set(); return ( h.forEach((m) => { const p = Pt(c[m], l[m]); p.size !== 0 && p.forEach((d) => { var w, v; const g = r.byId[d], b = s.byId[d]; !g || !b || g.parentId !== b.parentId || a.has(d) || (g.parentId ?? u) !== m || f.has(d) || (f.add(d), i.push({ type: "move", block: b.block, prevBlock: g.block, source: o, prevParent: g.parentId ? (w = r.byId[g.parentId]) == null ? void 0 : w.block : void 0, currentParent: b.parentId ? (v = s.byId[b.parentId]) == null ? void 0 : v.block : void 0, }), a.add(d)); }); }), i ); } const Do$1 = a$1(() => { const n = []; return { key: "blockChange", prosemirrorPlugins: [ new Plugin({ key: new PluginKey("blockChange"), filterTransaction: (e) => { let o; return n.reduce( (t, r) => t === false ? t : ( r({ getChanges() { return o || ((o = Mt(e)), o); }, tr: e, }) !== false ), true, ); }, }), ], /** * Subscribe to the block change events. */ subscribe(e) { return ( n.push(e), () => { n.splice(n.indexOf(e), 1); } ); }, }; }); function ie(n) { const e = n.charAt(0) === "#" ? n.substring(1, 7) : n, o = parseInt(e.substring(0, 2), 16), t = parseInt(e.substring(2, 4), 16), r = parseInt(e.substring(4, 6), 16), i = [o / 255, t / 255, r / 255].map((c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4))); return 0.2126 * i[0] + 0.7152 * i[1] + 0.0722 * i[2] <= 0.179; } function At(n) { const e = document.createElement("span"); e.classList.add("bn-collaboration-cursor__base"); const o = document.createElement("span"); (o.setAttribute("contentedEditable", "false"), o.classList.add("bn-collaboration-cursor__caret"), o.setAttribute("style", `background-color: ${n.color}; color: ${ie(n.color) ? "white" : "black"}`)); const t = document.createElement("span"); return ( t.classList.add("bn-collaboration-cursor__label"), t.setAttribute("style", `background-color: ${n.color}; color: ${ie(n.color) ? "white" : "black"}`), t.insertBefore(document.createTextNode(n.name), null), o.insertBefore(t, null), e.insertBefore(document.createTextNode("⁠"), null), e.insertBefore(o, null), e.insertBefore(document.createTextNode("⁠"), null), e ); } const ae$1 = a$1(({ options: n }) => { const e = /* @__PURE__ */ new Map(), o = n.provider && "awareness" in n.provider && typeof n.provider.awareness == "object" ? n.provider.awareness : void 0; return ( o && ("setLocalStateField" in o && typeof o.setLocalStateField == "function" && o.setLocalStateField("user", n.user), "on" in o && typeof o.on == "function" && n.showCursorLabels !== "always" && o.on("change", ({ updated: t }) => { for (const r of t) { const s = e.get(r); s && (setTimeout(() => { s.element.setAttribute("data-active", ""); }, 10), s.hideTimeout && clearTimeout(s.hideTimeout), e.set(r, { element: s.element, hideTimeout: setTimeout(() => { s.element.removeAttribute("data-active"); }, 2e3), })); } })), { key: "yCursor", prosemirrorPlugins: [ o ? yCursorPlugin(o, { selectionBuilder: defaultSelectionBuilder, cursorBuilder(t, r) { let s = e.get(r); if (!s) { const i = (n.renderCursor ?? At)(t); (n.showCursorLabels !== "always" && (i.addEventListener("mouseenter", () => { const a = e.get(r); (a.element.setAttribute("data-active", ""), a.hideTimeout && (clearTimeout(a.hideTimeout), e.set(r, { element: a.element, hideTimeout: void 0, }))); }), i.addEventListener("mouseleave", () => { const a = e.get(r); e.set(r, { element: a.element, hideTimeout: setTimeout(() => { a.element.removeAttribute("data-active"); }, 2e3), }); })), (s = { element: i, hideTimeout: void 0, }), e.set(r, s)); } return s.element; }, }) : void 0, ].filter(Boolean), dependsOn: ["ySync"], updateUser(t) { o == null || o.setLocalStateField("user", t); }, } ); }), K = a$1(({ options: n }) => ({ key: "ySync", prosemirrorPlugins: [ySyncPlugin(n.fragment)], runsBefore: ["default"], })), X$1 = a$1(() => ({ key: "yUndo", prosemirrorPlugins: [yUndoPlugin()], dependsOn: ["yCursor", "ySync"], undoCommand: undoCommand, redoCommand: redoCommand, })); function Lt(n, e) { const o = n.doc; if (n._item === null) { const t = Array.from(o.share.keys()).find((r) => o.share.get(r) === n); if (t == null) throw new Error("type does not exist in other ydoc"); return e.get(t, n.constructor); } else { const t = n._item, r = e.store.clients.get(t.id.client) ?? [], s = findIndexSS(r, t.id.clock); return r[s].content.type; } } const Oo$1 = a$1(({ editor: n, options: e }) => { let o; const t = f$2({ isForked: false }); return { key: "yForkDoc", store: t, /** * Fork the Y.js document from syncing to the remote, * allowing modifications to the document without affecting the remote. * These changes can later be rolled back or applied to the remote. */ fork() { if (o) return; const r = e.fragment; if (!r) throw new Error("No fragment to fork from"); const s = new Doc(); applyUpdate(s, encodeStateAsUpdate(r.doc)); const i = Lt(r, s); ((o = { undoStack: yUndoPluginKey.getState(n.prosemirrorState).undoManager.undoStack, originalFragment: r, forkedFragment: i, }), n.unregisterExtension([X$1, ae$1, K])); const a = { ...e, fragment: i, }; (n.registerExtension([ K(a), // No need to register the cursor plugin again, it's a local fork X$1(), ]), t.setState({ isForked: true })); }, /** * Resume syncing the Y.js document to the remote * If `keepChanges` is true, any changes that have been made to the forked document will be applied to the original document. * Otherwise, the original document will be restored and the changes will be discarded. */ merge({ keepChanges: r }) { if (!o) return; n.unregisterExtension(["ySync", "yCursor", "yUndo"]); const { originalFragment: s, forkedFragment: i, undoStack: a } = o; if ((n.registerExtension([K(e), ae$1(e), X$1()]), (yUndoPluginKey.getState(n.prosemirrorState).undoManager.undoStack = a), r)) { const c = encodeStateAsUpdate(i.doc, encodeStateVector(s.doc)); applyUpdate(s.doc, c, n); } ((o = void 0), t.setState({ isForked: false })); }, }; }), Te$1 = (n, e) => { (e(n), n.forEach((o) => { o instanceof YXmlElement && Te$1(o, e); })); }, Nt = (n, e) => { const o = /* @__PURE__ */ new Map(); return ( n.forEach((t) => { t instanceof YXmlElement && Te$1(t, (r) => { if (r.nodeName === "blockContainer" && r.hasAttribute("id")) { const s = r.getAttribute("textColor"), i = r.getAttribute("backgroundColor"), a = { textColor: s === m$1.textColor.default ? void 0 : s, backgroundColor: i === m$1.backgroundColor.default ? void 0 : i, }; (a.textColor || a.backgroundColor) && o.set(r.getAttribute("id"), a); } }); }), o.size === 0 ? false : (e.doc.descendants((t, r) => { if (t.type.name === "blockContainer" && o.has(t.attrs.id)) { const s = e.doc.nodeAt(r + 1); if (!s) throw new Error("No element found"); e.setNodeMarkup(r + 1, void 0, { // preserve existing attributes ...s.attrs, // add the textColor and backgroundColor attributes ...o.get(t.attrs.id), }); } }), true) ); }, Rt = [Nt], Po$1 = a$1(({ options: n }) => { let e = false; const o = new PluginKey("schemaMigration"); return { key: "schemaMigration", prosemirrorPlugins: [ new Plugin({ key: o, appendTransaction: (t, r, s) => { if ( e || // If any of the transactions are not due to a yjs sync, we don't need to run the migration !t.some((a) => a.getMeta("y-sync$")) || // If none of the transactions result in a document change, we don't need to run the migration t.every((a) => !a.docChanged) || // If the fragment is still empty, we can't run the migration (since it has not yet been applied to the Y.Doc) !n.fragment.firstChild ) return; const i = s.tr; for (const a of Rt) a(n.fragment, i); if (((e = true), !!i.docChanged)) return i; }, }), ], }; }); function le$1(n, e) { return !n || !e ? false : !!n.closest(`.${e}`); } function Vt(n, e, o, t, r) { const s = n.state.doc.resolve(e.pos); if (!!s.parent.inlineContent || e.orientation === "inline") return null; const a = s.nodeBefore, c = s.nodeAfter; if (!a && !c) return null; const l = e.orientation === "block-vertical-left" || e.orientation === "block-vertical-right", u = l ? e.pos : e.pos - (a ? a.nodeSize : 0), h = n.nodeDOM(u); if (!h) return null; const f = h.getBoundingClientRect(); if (l) { const d = (o / 2) * t, g = e.orientation === "block-vertical-left" ? f.left : f.right; return { left: g - d, right: g + d, top: f.top, bottom: f.bottom, }; } let m = a ? f.bottom : f.top; a && c && (m = (m + n.nodeDOM(e.pos).getBoundingClientRect().top) / 2); const p = (o / 2) * r; return { left: f.left, right: f.right, top: m - p, bottom: m + p, }; } function Ht(n, e, o, t) { const r = n.coordsAtPos(e.pos), s = (o / 2) * t; return { left: r.left - s, right: r.left + s, top: r.top, bottom: r.bottom, }; } function $t(n, e) { (n.classList.toggle("prosemirror-dropcursor-inline", e === "inline"), n.classList.toggle("prosemirror-dropcursor-block-horizontal", e === "block-horizontal"), n.classList.toggle("prosemirror-dropcursor-block-vertical-left", e === "block-vertical-left"), n.classList.toggle("prosemirror-dropcursor-block-vertical-right", e === "block-vertical-right"), n.classList.toggle("prosemirror-dropcursor-block", e === "block-horizontal"), n.classList.toggle("prosemirror-dropcursor-vertical", e === "block-vertical-left" || e === "block-vertical-right")); } function Ft(n) { if (!n || (n === document.body && getComputedStyle(n).position === "static")) return { parentLeft: -window.pageXOffset, parentTop: -window.pageYOffset, }; const e = n.getBoundingClientRect(), o = e.width / n.offsetWidth, t = e.height / n.offsetHeight; return { parentLeft: e.left - n.scrollLeft * o, parentTop: e.top - n.scrollTop * t, }; } const Ut = "bn-drag-exclude", Mo$1 = a$1(({ editor: n, options: e }) => { var d, g, b, y; let o = null, t = null, r = -1, s = null; const i = { width: ((d = e.dropCursor) == null ? void 0 : d.width) ?? 5, color: ((g = e.dropCursor) == null ? void 0 : g.color) ?? "#ddeeff", exclude: ((b = e.dropCursor) == null ? void 0 : b.exclude) ?? Ut, hooks: (y = e.dropCursor) == null ? void 0 : y.hooks, }, a = (w) => { ((w == null ? void 0 : w.pos) === (o == null ? void 0 : o.pos) && (w == null ? void 0 : w.orientation) === (o == null ? void 0 : o.orientation)) || ((o = w), w == null ? (t && t.parentNode && t.parentNode.removeChild(t), (t = null)) : c()); }, c = () => { if (!o) return; const w = n.prosemirrorView, v = w.dom, x = v.getBoundingClientRect(), I = x.width / v.offsetWidth, O = x.height / v.offsetHeight, T = Vt(w, o, i.width, I, O) ?? Ht(w, o, i.width, I), C = w.dom.offsetParent; (t || ((t = C.appendChild(document.createElement("div"))), (t.style.cssText = "position: absolute; z-index: 50; pointer-events: none;"), i.color && (t.style.backgroundColor = i.color)), $t(t, o.orientation)); const { parentLeft: S, parentTop: q } = Ft(C); ((t.style.left = (T.left - S) / I + "px"), (t.style.top = (T.top - q) / O + "px"), (t.style.width = (T.right - T.left) / I + "px"), (t.style.height = (T.bottom - T.top) / O + "px")); }, l = (w) => { (clearTimeout(r), (r = window.setTimeout(() => a(null), w))); }, u = (w) => { const v = w; s = v.target instanceof Element ? v.target : null; }, h = (w) => { var C; const v = w; if ((s && le$1(s, i.exclude)) || (v.target instanceof Element && le$1(v.target, i.exclude))) return; const x = n.prosemirrorView; if (!x.editable) return; const I = x.posAtCoords({ left: v.clientX, top: v.clientY, }), O = I && I.inside >= 0 && x.state.doc.nodeAt(I.inside), M = O && O.type.spec.disableDropCursor, T = typeof M == "function" ? M(x, I, v) : M; if (I && !T) { let S = I.pos; if (x.dragging && x.dragging.slice) { const R = dropPoint(x.state.doc, S, x.dragging.slice); R != null && (S = R); } const Me = !x.state.doc.resolve(S).parent.inlineContent, G = { pos: S, orientation: Me ? "block-horizontal" : "inline", }; let J = G; if ((C = i.hooks) != null && C.computeDropPosition) { const R = i.hooks.computeDropPosition({ editor: n, event: v, view: x, defaultPosition: G, }); if (R === null) { a(null); return; } J = R; } (a(J), l(5e3)); } }, f = (w) => { const v = w; (!(v.relatedTarget instanceof Node) || !n.prosemirrorView.dom.contains(v.relatedTarget)) && a(null); }, m = () => { l(20); }, p = () => { (l(20), (s = null)); }; return { key: "dropCursor", mount({ signal: w, dom: v, root: x }) { (x.addEventListener("dragstart", u, { capture: true, signal: w, }), v.addEventListener("dragover", h, { signal: w }), v.addEventListener("dragleave", f, { signal: w }), v.addEventListener("drop", m, { signal: w }), v.addEventListener("dragend", p, { signal: w }), w.addEventListener("abort", () => { (clearTimeout(r), a(null)); })); }, }; }), Ao$1 = a$1(({ editor: n }) => { const e = f$2(false), o = () => n.transact((t) => { var s; if ( t.selection.empty || (t.selection instanceof NodeSelection && (t.selection.node.type.spec.content === "inline*" || ((s = t.selection.node.firstChild) == null ? void 0 : s.type.spec.content) === "inline*")) || (t.selection instanceof TextSelection && t.doc.textBetween(t.selection.from, t.selection.to).length === 0) ) return false; let r = false; return (t.selection.content().content.descendants((i) => (i.type.spec.code && (r = true), !r)), !r); }); return { key: "formattingToolbar", store: e, mount({ dom: t, signal: r }) { let s = false; const i = n.onChange(() => { s || e.setState(o()); }), a = n.onSelectionChange(() => { s || e.setState(o()); }); (t.addEventListener( "pointerdown", () => { ((s = true), e.setState(false)); }, { signal: r }, ), n.prosemirrorView.root.addEventListener( "pointerup", () => { ((s = false), n.isFocused() && e.setState(o())); }, { signal: r, capture: true }, ), t.addEventListener( "pointercancel", () => { s = false; }, { signal: r, capture: true, }, ), r.addEventListener("abort", () => { (i(), a()); })); }, }; }), Lo$1 = a$1(() => ({ key: "history", prosemirrorPlugins: [history()], undoCommand: undo$1, redoCommand: redo$1, })), No$1 = a$1(({ editor: n }) => { function e(r) { let s = n.prosemirrorView.nodeDOM(r); for (; s && s.parentElement; ) { if (s.nodeName === "A") return s; s = s.parentElement; } return null; } function o(r, s) { return n.transact((i) => { const a = i.doc.resolve(r), c = a.marks().find((u) => u.type.name === s); if (!c) return; const l = getMarkRange(a, c.type); if (l) return { range: l, mark: c, get text() { return i.doc.textBetween(l.from, l.to); }, get position() { return posToDOMRect(n.prosemirrorView, l.from, l.to).toJSON(); }, }; }); } function t() { return n.transact((r) => { const s = r.selection; if (s.empty) return o(s.anchor, "link"); }); } return { key: "linkToolbar", getLinkAtSelection: t, getLinkElementAtPos: e, getMarkAtPos: o, getLinkAtElement(r) { return n.transact(() => { const s = n.prosemirrorView.posAtDOM(r, 0) + 1; return o(s, "link"); }); }, editLink(r, s, i = n.transact((a) => a.selection.anchor)) { (n.transact((a) => { const c = ht(a), { range: l } = o(i + 1, "link") || { range: { from: a.selection.from, to: a.selection.to, }, }; l && (a.insertText(s, l.from, l.to), a.addMark(l.from, l.from + s.length, c.mark("link", { href: r }))); }), n.prosemirrorView.focus()); }, deleteLink(r = n.transact((s) => s.selection.anchor)) { (n.transact((s) => { const i = ht(s), { range: a } = o(r + 1, "link") || { range: { from: s.selection.from, to: s.selection.to, }, }; a && s.removeMark(a.from, a.to, i.marks.link).setMeta("preventAutolink", true); }), n.prosemirrorView.focus()); }, }; }), Ro$1 = ["http", "https", "ftp", "ftps", "mailto", "tel", "callto", "sms", "cid", "xmpp"], Vo$1 = "https", _t = new PluginKey("node-selection-keyboard"), Ho$1 = a$1(() => ({ key: "nodeSelectionKeyboard", prosemirrorPlugins: [ new Plugin({ key: _t, props: { handleKeyDown: (n, e) => { if ("node" in n.state.selection) { if (e.ctrlKey || e.metaKey) return false; if (e.key.length === 1) return (e.preventDefault(), true); if (e.key === "Enter" && !e.isComposing && !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey) { const o = n.state.tr; return ( n.dispatch( o .insert(n.state.tr.selection.$to.after(), n.state.schema.nodes.paragraph.createChecked()) .setSelection(new TextSelection(o.doc.resolve(n.state.tr.selection.$to.after() + 1))), ), true ); } } return false; }, }, }), ], })), zt = new PluginKey("blocknote-placeholder"), $o$1 = a$1(({ editor: n, options: e }) => { const o = e.placeholders; return { key: "placeholder", prosemirrorPlugins: [ new Plugin({ key: zt, view: (t) => { const r = `placeholder-selector-${v4()}`; t.dom.classList.add(r); const s = document.createElement("style"), i = n._tiptapEditor.options.injectNonce; (i && s.setAttribute("nonce", i), t.root instanceof window.ShadowRoot ? t.root.append(s) : t.root.head.appendChild(s)); const a = s.sheet, c = (l = "") => `.${r} .bn-block-content${l} .bn-inline-content:has(> .ProseMirror-trailingBreak:only-child):before`; try { const { default: l, emptyDocument: u, ...h } = o || {}; for (const [p, d] of Object.entries(h)) { const g = `[data-content-type="${p}"]`; a.insertRule(`${c(g)} { content: ${JSON.stringify(d)}; }`); } const f = "[data-is-only-empty-block]", m = "[data-is-empty-and-focused]"; (a.insertRule(`${c(f)} { content: ${JSON.stringify(u)}; }`), a.insertRule(`${c(m)} { content: ${JSON.stringify(l)}; }`)); } catch (l) { console.warn( "Failed to insert placeholder CSS rule - this is likely due to the browser not supporting certain CSS pseudo-element selectors (:has, :only-child:, or :before)", l, ); } return { destroy: () => { t.root instanceof window.ShadowRoot ? t.root.removeChild(s) : t.root.head.removeChild(s); }, }; }, props: { decorations: (t) => { const { doc: r, selection: s } = t; if (!n.isEditable || !s.empty || s.$from.parent.type.spec.code) return; const i = []; t.doc.content.size === 6 && i.push( Decoration.node(2, 4, { "data-is-only-empty-block": "true", }), ); const a = s.$anchor, c = a.parent; if (c.content.size === 0) { const l = a.before(); i.push( Decoration.node(l, l + c.nodeSize, { "data-is-empty-and-focused": "true", }), ); } return DecorationSet.create(r, i); }, }, }), ], }; }), ce$1 = new PluginKey("previous-blocks"), Yt = { // Numbered List Items "index": "index", // Headings "level": "level", // All Blocks "type": "type", "depth": "depth", "depth-change": "depth-change", }, Fo$1 = a$1(() => { let n; return { key: "previousBlockType", prosemirrorPlugins: [ new Plugin({ key: ce$1, view(e) { return { update: async (o, t) => { var r; ((r = this.key) == null ? void 0 : r.getState(o.state).updatedBlocks.size) > 0 && (n = setTimeout(() => { o.dispatch(o.state.tr.setMeta(ce$1, { clearUpdate: true })); }, 0)); }, destroy: () => { n && clearTimeout(n); }, }; }, state: { init() { return { // Block attributes, by block ID, from just before the previous transaction. prevTransactionOldBlockAttrs: {}, // Block attributes, by block ID, from just before the current transaction. currentTransactionOldBlockAttrs: {}, // Set of IDs of blocks whose attributes changed from the current transaction. updatedBlocks: /* @__PURE__ */ new Set(), }; }, apply(e, o, t, r) { if (((o.currentTransactionOldBlockAttrs = {}), o.updatedBlocks.clear(), !e.docChanged || t.doc.eq(r.doc))) return o; const s = {}, i = findChildren(t.doc, (l) => l.attrs.id), a = new Map(i.map((l) => [l.node.attrs.id, l])), c = findChildren(r.doc, (l) => l.attrs.id); for (const l of c) { const u = a.get(l.node.attrs.id), h = u == null ? void 0 : u.node.firstChild, f = l.node.firstChild; if (u && h && f) { const m = { index: f.attrs.index, level: f.attrs.level, type: f.type.name, depth: r.doc.resolve(l.pos).depth, }, p = { index: h.attrs.index, level: h.attrs.level, type: h.type.name, depth: t.doc.resolve(u.pos).depth, }; ((s[l.node.attrs.id] = p), (o.currentTransactionOldBlockAttrs[l.node.attrs.id] = p), JSON.stringify(p) !== JSON.stringify(m) && ((p["depth-change"] = p.depth - m.depth), o.updatedBlocks.add(l.node.attrs.id))); } } return ((o.prevTransactionOldBlockAttrs = s), o); }, }, props: { decorations(e) { const o = this.getState(e); if (o.updatedBlocks.size === 0) return; const t = []; return ( e.doc.descendants((r, s) => { if (!r.attrs.id || !o.updatedBlocks.has(r.attrs.id)) return; const i = o.currentTransactionOldBlockAttrs[r.attrs.id], a = {}; for (const [l, u] of Object.entries(i)) a["data-prev-" + Yt[l]] = u || "none"; const c = Decoration.node(s, s + r.nodeSize, { ...a, }); t.push(c); }), DecorationSet.create(e.doc, t) ); }, }, }), ], }; }); function De$1(n, e) { var o, t; for (; n && n.parentElement && n.parentElement !== e.dom && ((o = n.getAttribute) == null ? void 0 : o.call(n, "data-node-type")) !== "blockContainer"; ) n = n.parentElement; if (((t = n.getAttribute) == null ? void 0 : t.call(n, "data-node-type")) === "blockContainer") return { node: n, id: n.getAttribute("data-id") }; } function jt() { const n = (e) => { let o = e.children.length; for (let t = 0; t < o; t++) { const r = e.children[t]; if (r.type === "element" && (n(r), r.tagName === "u")) if (r.children.length > 0) { e.children.splice(t, 1, ...r.children); const s = r.children.length - 1; ((o += s), (t += s)); } else (e.children.splice(t, 1), o--, t--); } }; return n; } function Kt() { const n = (e) => { var o; if (e.children && "length" in e.children && e.children.length) for (let t = e.children.length - 1; t >= 0; t--) { const r = e.children[t], s = t + 1 < e.children.length ? e.children[t + 1] : void 0; r.type === "element" && r.tagName === "input" && ((o = r.properties) == null ? void 0 : o.type) === "checkbox" && (s == null ? void 0 : s.type) === "element" && s.tagName === "p" ? ((s.tagName = "span"), s.children.splice(0, 0, fromDom(document.createTextNode(" ")))) : n(r); } }; return n; } function Xt() { return (n) => { visit(n, "element", (e, o, t) => { var r, s, i, a; if (t && e.tagName === "video") { const c = ((r = e.properties) == null ? void 0 : r.src) || ((s = e.properties) == null ? void 0 : s["data-url"]) || "", l = ((i = e.properties) == null ? void 0 : i.title) || ((a = e.properties) == null ? void 0 : a["data-name"]) || ""; t.children[o] = { type: "text", value: `![${l}](${c})`, }; } }); }; } function Oe$1(n) { return unified() .use(rehypeParse, { fragment: true }) .use(Xt) .use(jt) .use(Kt) .use(rehypeRemark) .use(remarkGfm) .use(remarkStringify, { handlers: { text: (o) => o.value }, }) .processSync(n).value; } function Uo$1(n, e, o, t) { const s = Be(e, o).exportBlocks(n, t); return Oe$1(s); } function Wt(n) { const e = []; return ( n.descendants((o) => { var r, s; const t = ht(o); return ( o.type.name === "blockContainer" && ((r = o.firstChild) == null ? void 0 : r.type.name) === "blockGroup" ? true : o.type.name === "columnList" && o.childCount === 1 ? ((s = o.firstChild) == null || s.forEach((i) => { e.push(L$2(i, t)); }), false) : o.type.isInGroup("bnBlock") ? (e.push(L$2(o, t)), false) : true ); }), e ); } let A$1 = class A extends Selection { constructor(o, t) { super(o, t); k$2(this, "nodes"); const r = o.node(); ((this.nodes = []), o.doc.nodesBetween(o.pos, t.pos, (s, i, a) => { if (a !== null && a.eq(r)) return (this.nodes.push(s), false); })); } static create(o, t, r = t) { return new A(o.resolve(t), o.resolve(r)); } content() { return new Slice(Fragment.from(this.nodes), 0, 0); } eq(o) { if (!(o instanceof A) || this.nodes.length !== o.nodes.length || this.from !== o.from || this.to !== o.to) return false; for (let t = 0; t < this.nodes.length; t++) if (!this.nodes[t].eq(o.nodes[t])) return false; return true; } map(o, t) { const r = t.mapResult(this.from), s = t.mapResult(this.to); return ( s.deleted ? Selection.near(o.resolve(r.pos)) : r.deleted ? Selection.near(o.resolve(s.pos)) : new A(o.resolve(r.pos), o.resolve(s.pos)) ); } toJSON() { return { type: "multiple-node", anchor: this.anchor, head: this.head }; } }; Selection.jsonID("multiple-node", A$1); let D$1; function qt(n, e) { let o, t; const r = e.resolve(n.from).node().type.spec.group === "blockContent", s = e.resolve(n.to).node().type.spec.group === "blockContent", i = Math.min(n.$anchor.depth, n.$head.depth); if (r && s) { const a = n.$from.start(i - 1), c = n.$to.end(i - 1); ((o = e.resolve(a - 1).pos), (t = e.resolve(c + 1).pos)); } else ((o = n.from), (t = n.to)); return { from: o, to: t }; } function de$1(n, e, o = e) { e === o && (o += n.state.doc.resolve(e + 1).node().nodeSize); const t = n.domAtPos(e).node.cloneNode(true), r = n.domAtPos(e).node, s = (h, f) => Array.prototype.indexOf.call(h.children, f), i = s( r, // Expects from position to be just before the first selected block. n.domAtPos(e + 1).node.parentElement, ), a = s( r, // Expects to position to be just after the last selected block. n.domAtPos(o - 1).node.parentElement, ); for (let h = r.childElementCount - 1; h >= 0; h--) (h > a || h < i) && t.removeChild(t.children[h]); (Pe$1(n.root), (D$1 = t)); const c = D$1.getElementsByTagName("iframe"); for (let h = 0; h < c.length; h++) { const f = c[h], m = f.parentElement; m && m.removeChild(f); } const u = n.dom.className .split(" ") .filter((h) => h !== "ProseMirror" && h !== "bn-root" && h !== "bn-editor") .join(" "); ((D$1.className = D$1.className + " bn-drag-preview " + u), n.root instanceof ShadowRoot ? n.root.appendChild(D$1) : n.root.body.appendChild(D$1)); } function Pe$1(n) { D$1 !== void 0 && (n instanceof ShadowRoot ? n.removeChild(D$1) : n.body.removeChild(D$1), (D$1 = void 0)); } function Gt(n, e, o) { if (!n.dataTransfer || o.headless) return; const t = o.prosemirrorView, r = Bt$1(e.id, t.state.doc); if (!r) throw new Error(`Block with ID ${e.id} not found`); const s = r.posBeforeNode; if (s != null) { const i = t.state.selection, a = t.state.doc, { from: c, to: l } = qt(i, a), u = c <= s && s < l, h = i.$anchor.node() !== i.$head.node() || i instanceof A$1; u && h ? (t.dispatch(t.state.tr.setSelection(A$1.create(a, c, l))), de$1(t, c, l)) : (t.dispatch(t.state.tr.setSelection(NodeSelection.create(t.state.doc, s))), de$1(t, s)); const f = t.state.selection.content(), m = o.pmSchema, p = t.serializeForClipboard(f).dom.innerHTML, d = Be(m, o), g = Wt(f.content), b = d.exportBlocks(g, {}), y = Oe$1(b); (n.dataTransfer.clearData(), n.dataTransfer.setData("blocknote/html", p), n.dataTransfer.setData("text/html", b), n.dataTransfer.setData("text/plain", y), (n.dataTransfer.effectAllowed = "move"), n.dataTransfer.setDragImage(D$1, 0, 0)); } } const ue$1 = 250; function W(n, e, o = true) { const t = n.root.elementsFromPoint(e.left, e.top); for (const r of t) if (n.dom.contains(r)) return o && r.closest("[data-node-type=columnList]") ? W( n, { // TODO can we do better than this? left: e.left + 50, // bit hacky, but if we're inside a column, offset x position to right to account for the width of sidemenu itself top: e.top, }, false, ) : De$1(r, n); } function Jt(n, e) { if (!e.dom.firstChild) return; const o = e.dom.firstChild.getBoundingClientRect(), t = { // Clamps the x position to the editor's bounding box. left: Math.min(Math.max(o.left + 10, n.x), o.right - 10), top: n.y, }, r = W(e, t); if (!r) return; const s = r.node.getBoundingClientRect(); return W( e, { left: s.right - 10, top: n.y, }, false, ); } class Qt { constructor(e, o, t) { k$2(this, "state"); k$2(this, "emitUpdate"); k$2(this, "mousePos"); k$2(this, "hoveredBlock"); k$2(this, "menuFrozen", false); k$2(this, "isDragOrigin", false); k$2(this, "updateState", (e) => { ((this.state = e), this.emitUpdate(this.state)); }); k$2(this, "updateStateFromMousePos", () => { var t, r, s, i, a; if (this.menuFrozen || !this.mousePos) return; const e = this.findClosestEditorElement({ clientX: this.mousePos.x, clientY: this.mousePos.y, }); if ((e == null ? void 0 : e.element) !== this.pmView.dom || e.distance > ue$1) { (t = this.state) != null && t.show && ((this.state.show = false), this.updateState(this.state)); return; } const o = Jt(this.mousePos, this.pmView); if (!o || !this.editor.isEditable) { (r = this.state) != null && r.show && ((this.state.show = false), this.updateState(this.state)); return; } if ( !( (s = this.state) != null && s.show && (i = this.hoveredBlock) != null && i.hasAttribute("data-id") && ((a = this.hoveredBlock) == null ? void 0 : a.getAttribute("data-id")) === o.id ) && ((this.hoveredBlock = o.node), this.editor.isEditable) ) { const c = o.node.getBoundingClientRect(), l = o.node.closest("[data-node-type=column]"); ((this.state = { show: true, referencePos: new DOMRect( l ? // We take the first child as column elements have some default // padding. This is a little weird since this child element will // be the first block, but since it's always non-nested and we // only take the x coordinate, it's ok. l.firstElementChild.getBoundingClientRect().x : this.pmView.dom.firstChild.getBoundingClientRect().x, c.y, c.width, c.height, ), block: this.editor.getBlock(this.hoveredBlock.getAttribute("data-id")), }), this.updateState(this.state)); } }); /** * If a block is being dragged, ProseMirror usually gets the context of what's * being dragged from `view.dragging`, which is automatically set when a * `dragstart` event fires in the editor. However, if the user tries to drag * and drop blocks between multiple editors, only the one in which the drag * began has that context, so we need to set it on the others manually. This * ensures that PM always drops the blocks in between other blocks, and not * inside them. * * After the `dragstart` event fires on the drag handle, it sets * `blocknote/html` data on the clipboard. This handler fires right after, * parsing the `blocknote/html` data into nodes and setting them on * `view.dragging`. * * Note: Setting `view.dragging` on `dragover` would be better as the user * could then drag between editors in different windows, but you can only * access `dataTransfer` contents on `dragstart` and `drop` events. */ k$2(this, "onDragStart", (e) => { var i; const o = (i = e.dataTransfer) == null ? void 0 : i.getData("blocknote/html"); if (!o || this.pmView.dragging) return; const t = document.createElement("div"); t.innerHTML = o; const s = DOMParser$1.fromSchema(this.pmView.state.schema).parse(t, { topNode: this.pmView.state.schema.nodes.blockGroup.create(), }); this.pmView.dragging = { slice: new Slice(s.content, 0, 0), move: true, }; }); /** * Finds the closest editor visually to the given coordinates */ k$2(this, "findClosestEditorElement", (e) => { const o = Array.from(this.pmView.root.querySelectorAll(".bn-editor")); if (o.length === 0) return null; let t = o[0], r = Number.MAX_VALUE; return ( o.forEach((s) => { const i = s.querySelector(".bn-block-group").getBoundingClientRect(), a = e.clientX < i.left ? i.left - e.clientX : e.clientX > i.right ? e.clientX - i.right : 0, c = e.clientY < i.top ? i.top - e.clientY : e.clientY > i.bottom ? e.clientY - i.bottom : 0, l = Math.sqrt(Math.pow(a, 2) + Math.pow(c, 2)); l < r && ((r = l), (t = s)); }), { element: t, distance: r, } ); }); /** * This dragover event handler listens at the document level, * and is trying to handle dragover events for all editors. * * It specifically is trying to handle the following cases: * - If the dragover event is within the bounds of any editor, then it does nothing * - If the dragover event is outside the bounds of any editor, but close enough (within DISTANCE_TO_CONSIDER_EDITOR_BOUNDS) to the closest editor, * then it dispatches a synthetic dragover event to the closest editor (which will trigger the drop-cursor to be shown on that editor) * - If the dragover event is outside the bounds of the current editor, then it will dispatch a synthetic dragleave event to the current editor * (which will trigger the drop-cursor to be removed from the current editor) * * The synthetic event is a necessary evil because we do not control prosemirror-dropcursor to be able to show the drop-cursor within the range we want */ k$2(this, "onDragOver", (e) => { var r; if ( e.synthetic || !( this.pmView.dragging !== null || this.isDragOrigin || ((r = e.dataTransfer) == null ? void 0 : r.types.includes("blocknote/html")) || (e.target instanceof Node && this.pmView.dom.contains(e.target)) ) ) return; const t = this.getDragEventContext(e); if (!t || !t.isDropPoint) { this.closeDropCursor(); return; } t.isDropPoint && !t.isDropWithinEditorBounds && this.dispatchSyntheticEvent(e); }); /** * Closes the drop-cursor for the current editor */ k$2(this, "closeDropCursor", () => { const e = new Event("dragleave", { bubbles: false }); ((e.synthetic = true), this.pmView.dom.dispatchEvent(e)); }); /** * It is surprisingly difficult to determine the information we need to know about a drag event * * This function is trying to determine the following: * - Whether the current editor instance is the drop point * - Whether the current editor instance is the drag origin * - Whether the drop event is within the bounds of the current editor instance */ k$2(this, "getDragEventContext", (e) => { var l, u; if ( !( this.pmView.dragging !== null || this.isDragOrigin || ((l = e.dataTransfer) == null ? void 0 : l.types.includes("blocknote/html")) || (e.target instanceof Node && this.pmView.dom.contains(e.target)) ) ) return; const t = !((u = e.dataTransfer) != null && u.types.includes("blocknote/html")) && !!this.pmView.dragging, r = !!this.isDragOrigin, s = t || r, i = this.findClosestEditorElement(e); if (!i || i.distance > ue$1) return; const a = i.element === this.pmView.dom, c = a && i.distance === 0; if (!(!a && !s)) return { isDropPoint: a, isDropWithinEditorBounds: c, isDragOrigin: s, }; }); /** * The drop event handler listens at the document level, * and handles drop events for all editors. * * It specifically handles the following cases: * - If we are both the drag origin and drop point: * - Let normal drop handling take over * - If we are the drop point but not the drag origin: * - Collapse selection to prevent PM from deleting unrelated content * - If drop event is outside our editor bounds, dispatch synthetic drop event to our editor * - If we are the drag origin but not the drop point: * - Delete the dragged content from our editor after a delay */ k$2(this, "onDrop", (e) => { var a; if ( e.synthetic || !( this.pmView.dragging !== null || this.isDragOrigin || ((a = e.dataTransfer) == null ? void 0 : a.types.includes("blocknote/html")) || (e.target instanceof Node && this.pmView.dom.contains(e.target)) ) ) return; const t = this.getDragEventContext(e); if (!t) { this.closeDropCursor(); return; } const { isDropPoint: r, isDropWithinEditorBounds: s, isDragOrigin: i } = t; if ((!s && r && this.dispatchSyntheticEvent(e), r)) { if (this.pmView.dragging) return; this.pmView.dispatch(this.pmView.state.tr.setSelection(TextSelection.create(this.pmView.state.tr.doc, this.pmView.state.tr.selection.anchor))); return; } else if (i) { setTimeout(() => this.pmView.dispatch(this.pmView.state.tr.deleteSelection()), 0); return; } }); k$2(this, "onDragEnd", (e) => { e.synthetic || (this.pmView.dragging = null); }); k$2(this, "onKeyDown", (e) => { var o; (o = this.state) != null && o.show && this.editor.isFocused() && ((this.state.show = false), this.emitUpdate(this.state)); }); k$2(this, "onMouseMove", (e) => { var s; if (this.menuFrozen) return; this.mousePos = { x: e.clientX, y: e.clientY }; const o = this.pmView.dom.getBoundingClientRect(), t = this.mousePos.x > o.left && this.mousePos.x < o.right && this.mousePos.y > o.top && this.mousePos.y < o.bottom, r = this.pmView.dom.parentElement; if ( // Cursor is within the editor area t && // An element is hovered e && e.target && // Element is outside the editor !(r === e.target || r.contains(e.target)) ) { (s = this.state) != null && s.show && ((this.state.show = false), this.emitUpdate(this.state)); return; } this.updateStateFromMousePos(); }); ((this.editor = e), (this.pmView = o), (this.emitUpdate = () => { if (!this.state) throw new Error("Attempting to update uninitialized side menu"); t(this.state); }), this.pmView.root.addEventListener("dragstart", this.onDragStart), this.pmView.root.addEventListener("dragover", this.onDragOver), this.pmView.root.addEventListener("drop", this.onDrop, true), this.pmView.root.addEventListener("dragend", this.onDragEnd, true), this.pmView.root.addEventListener("mousemove", this.onMouseMove, true), this.pmView.root.addEventListener("keydown", this.onKeyDown, true)); } dispatchSyntheticEvent(e) { const o = new Event(e.type, e), t = this.pmView.dom.firstChild.getBoundingClientRect(); ((o.clientX = e.clientX), (o.clientY = e.clientY), (o.clientX = Math.min(Math.max(e.clientX, t.left), t.left + t.width)), (o.clientY = Math.min(Math.max(e.clientY, t.top), t.top + t.height)), (o.dataTransfer = e.dataTransfer), (o.preventDefault = () => e.preventDefault()), (o.synthetic = true), this.pmView.dom.dispatchEvent(o)); } // Needed in cases where the editor state updates without the mouse cursor // moving, as some state updates can require a side menu update. For example, // adding a button to the side menu which removes the block can cause the // block below to jump up into the place of the removed block when clicked, // allowing the user to click the button again without moving the cursor. This // would otherwise not update the side menu, and so clicking the button again // would attempt to remove the same block again, causing an error. update(e, o) { var r; !o.doc.eq(this.pmView.state.doc) && (r = this.state) != null && r.show && this.updateStateFromMousePos(); } destroy() { var e; ((e = this.state) != null && e.show && ((this.state.show = false), this.emitUpdate(this.state)), this.pmView.root.removeEventListener("mousemove", this.onMouseMove, true), this.pmView.root.removeEventListener("dragstart", this.onDragStart), this.pmView.root.removeEventListener("dragover", this.onDragOver), this.pmView.root.removeEventListener("drop", this.onDrop, true), this.pmView.root.removeEventListener("dragend", this.onDragEnd, true), this.pmView.root.removeEventListener("keydown", this.onKeyDown, true)); } } const Zt$1 = new PluginKey("SideMenuPlugin"), _o$1 = a$1(({ editor: n }) => { let e; const o = f$2(void 0); return { key: "sideMenu", store: o, prosemirrorPlugins: [ new Plugin({ key: Zt$1, view: (t) => ( (e = new Qt(n, t, (r) => { o.setState({ ...r }); })), e ), }), ], /** * Handles drag & drop events for blocks. */ blockDragStart(t, r) { (e && (e.isDragOrigin = true), Gt(t, r, n)); }, /** * Handles drag & drop events for blocks. */ blockDragEnd() { (Pe$1(n.prosemirrorView.root), e && (e.isDragOrigin = false), n.blur()); }, /** * Freezes the side menu. When frozen, the side menu will stay * attached to the same block regardless of which block is hovered by the * mouse cursor. */ freezeMenu() { ((e.menuFrozen = true), (e.state.show = true), e.emitUpdate(e.state)); }, /** * Unfreezes the side menu. When frozen, the side menu will stay * attached to the same block regardless of which block is hovered by the * mouse cursor. */ unfreezeMenu() { ((e.menuFrozen = false), (e.state.show = false), e.emitUpdate(e.state)); }, }; }); let $; async function eo$1() { return ( $ || (($ = (async () => { const [n, e] = await Promise.all([ __vitePreload(() => import("./module-CfopjtOo.js"), true ? [] : void 0), // use a dynamic import to encourage bundle-splitting // and a smaller initial client bundle size __vitePreload(() => import("./native-DB2Z31Rx.js"), true ? [] : void 0), ]), o = "default" in n ? n.default : n, t = "default" in e ? e.default : e; return (await o.init({ data: t }), { emojiMart: o, emojiData: t }); })()), $) ); } async function zo$1(n, e) { if (!("text" in n.schema.inlineContentSchema) || n.schema.inlineContentSchema.text !== Ho$2.text) return []; const { emojiData: o, emojiMart: t } = await eo$1(); return (e.trim() === "" ? Object.values(o.emojis) : await t.SearchIndex.search(e)).map((s) => ({ id: s.skins[0].native, onItemClick: () => n.insertInlineContent(s.skins[0].native + " "), })); } let B; function he$1(n) { B || ((B = document.createElement("div")), (B.innerHTML = "_"), (B.style.opacity = "0"), (B.style.height = "1px"), (B.style.width = "1px"), n instanceof Document ? n.body.appendChild(B) : n.appendChild(B)); } function to$1(n) { B && (n instanceof Document ? n.body.removeChild(B) : n.removeChild(B), (B = void 0)); } function F(n) { return Array.prototype.indexOf.call(n.parentElement.childNodes, n); } function oo$1(n) { let e = n; for (; e && e.nodeName !== "TD" && e.nodeName !== "TH" && !e.classList.contains("tableWrapper"); ) { if (e.classList.contains("ProseMirror")) return; const o = e.parentNode; if (!o || !(o instanceof Element)) return; e = o; } return e.nodeName === "TD" || e.nodeName === "TH" ? { type: "cell", domNode: e, tbodyNode: e.closest("tbody"), } : { type: "wrapper", domNode: e, tbodyNode: e.querySelector("tbody"), }; } function no$1(n, e) { const o = e.querySelectorAll(n); for (let t = 0; t < o.length; t++) o[t].style.visibility = "hidden"; } let ro$1 = class ro { constructor(e, o, t) { k$2(this, "state"); k$2(this, "emitUpdate"); k$2(this, "tableId"); k$2(this, "tablePos"); k$2(this, "tableElement"); k$2(this, "menuFrozen", false); k$2(this, "mouseState", "up"); k$2(this, "prevWasEditable", null); k$2(this, "viewMousedownHandler", () => { this.mouseState = "down"; }); k$2(this, "mouseUpHandler", (e) => { ((this.mouseState = "up"), this.mouseMoveHandler(e)); }); k$2(this, "mouseMoveHandler", (e) => { var l, u, h, f, m, p, d, g; if (this.menuFrozen || this.mouseState === "selecting" || !(e.target instanceof Element) || !this.pmView.dom.contains(e.target)) return; const o = oo$1(e.target); if ((o == null ? void 0 : o.type) === "cell" && this.mouseState === "down" && !((l = this.state) != null && l.draggingState)) { ((this.mouseState = "selecting"), (u = this.state) != null && u.show && ((this.state.show = false), (this.state.showAddOrRemoveRowsButton = false), (this.state.showAddOrRemoveColumnsButton = false), this.emitUpdate())); return; } if (!o || !this.editor.isEditable) { (h = this.state) != null && h.show && ((this.state.show = false), (this.state.showAddOrRemoveRowsButton = false), (this.state.showAddOrRemoveColumnsButton = false), this.emitUpdate()); return; } if (!o.tbodyNode) return; const t = o.tbodyNode.getBoundingClientRect(), r = De$1(o.domNode, this.pmView); if (!r) return; this.tableElement = r.node; let s; const i = this.editor.transact((b) => Bt$1(r.id, b.doc)); if (!i) throw new Error(`Block with ID ${r.id} not found`); const a = L$2(i.node, this.editor.pmSchema, this.editor.schema.blockSchema, this.editor.schema.inlineContentSchema, this.editor.schema.styleSchema); if ((b$2(this.editor, "table") && ((this.tablePos = i.posBeforeNode + 1), (s = a)), !s)) return; this.tableId = r.id; const c = (f = o.domNode.closest(".tableWrapper")) == null ? void 0 : f.querySelector(".table-widgets-container"); if ((o == null ? void 0 : o.type) === "wrapper") { const b = e.clientY >= t.bottom - 1 && // -1 to account for fractions of pixels in "bottom" e.clientY < t.bottom + 20, y = e.clientX >= t.right - 1 && e.clientX < t.right + 20, w = // always hide handles when the actively hovered table changed ((m = this.state) == null ? void 0 : m.block.id) !== s.id || // make sure we don't hide existing handles (keep col / row index) when // we're hovering just above or to the right of a table e.clientX > t.right || e.clientY > t.bottom; this.state = { ...this.state, show: true, showAddOrRemoveRowsButton: b, showAddOrRemoveColumnsButton: y, referencePosTable: t, block: s, widgetContainer: c, colIndex: w || (p = this.state) == null ? void 0 : p.colIndex, rowIndex: w || (d = this.state) == null ? void 0 : d.rowIndex, referencePosCell: w || (g = this.state) == null ? void 0 : g.referencePosCell, }; } else { const b = F(o.domNode), y = F(o.domNode.parentElement), w = o.domNode.getBoundingClientRect(); if (this.state !== void 0 && this.state.show && this.tableId === r.id && this.state.rowIndex === y && this.state.colIndex === b) return; this.state = { show: true, showAddOrRemoveColumnsButton: b === s.content.rows[0].cells.length - 1, showAddOrRemoveRowsButton: y === s.content.rows.length - 1, referencePosTable: t, block: s, draggingState: void 0, referencePosCell: w, colIndex: b, rowIndex: y, widgetContainer: c, }; } return (this.emitUpdate(), false); }); k$2(this, "dragOverHandler", (e) => { var f; if (((f = this.state) == null ? void 0 : f.draggingState) === void 0) return; (e.preventDefault(), (e.dataTransfer.dropEffect = "move"), no$1(".prosemirror-dropcursor-block, .prosemirror-dropcursor-inline", this.pmView.root)); const o = { left: Math.min(Math.max(e.clientX, this.state.referencePosTable.left + 1), this.state.referencePosTable.right - 1), top: Math.min(Math.max(e.clientY, this.state.referencePosTable.top + 1), this.state.referencePosTable.bottom - 1), }, t = this.pmView.root.elementsFromPoint(o.left, o.top).filter((m) => m.tagName === "TD" || m.tagName === "TH"); if (t.length === 0) return; const r = t[0]; let s = false; const i = F(r.parentElement), a = F(r), c = this.state.draggingState.draggedCellOrientation === "row" ? this.state.rowIndex : this.state.colIndex, u = (this.state.draggingState.draggedCellOrientation === "row" ? i : a) !== c; (this.state.rowIndex !== i || this.state.colIndex !== a) && ((this.state.rowIndex = i), (this.state.colIndex = a), (this.state.referencePosCell = r.getBoundingClientRect()), (s = true)); const h = this.state.draggingState.draggedCellOrientation === "row" ? o.top : o.left; (this.state.draggingState.mousePos !== h && ((this.state.draggingState.mousePos = h), (s = true)), s && this.emitUpdate(), u && this.editor.transact((m) => m.setMeta(V, true))); }); k$2(this, "dropHandler", (e) => { if (((this.mouseState = "up"), this.state === void 0 || this.state.draggingState === void 0)) return false; if (this.state.rowIndex === void 0 || this.state.colIndex === void 0) throw new Error("Attempted to drop table row or column, but no table block was hovered prior."); e.preventDefault(); const { draggingState: o, colIndex: t, rowIndex: r } = this.state, s = this.state.block.content.columnWidths; if (o.draggedCellOrientation === "row") { if (!Jt$2(this.state.block, o.originalIndex, r)) return false; const i = Pt$2(this.state.block, o.originalIndex, r); this.editor.updateBlock(this.state.block, { type: "table", content: { ...this.state.block.content, rows: i, }, }); } else { if (!_t$3(this.state.block, o.originalIndex, t)) return false; const i = Mt$2(this.state.block, o.originalIndex, t), [a] = s.splice(o.originalIndex, 1); (s.splice(t, 0, a), this.editor.updateBlock(this.state.block, { type: "table", content: { ...this.state.block.content, columnWidths: s, rows: i, }, })); } return (this.editor.setTextCursorPosition(this.state.block.id), true); }); ((this.editor = e), (this.pmView = o), (this.emitUpdate = () => { if (!this.state) throw new Error("Attempting to update uninitialized image toolbar"); t(this.state); }), o.dom.addEventListener("mousemove", this.mouseMoveHandler), o.dom.addEventListener("mousedown", this.viewMousedownHandler), window.addEventListener("mouseup", this.mouseUpHandler), o.root.addEventListener("dragover", this.dragOverHandler), o.root.addEventListener("drop", this.dropHandler)); } // Updates drag handles when the table is modified or removed. update() { var r; if (!this.state || !this.state.show) return; if ( ((this.state.block = this.editor.getBlock(this.state.block.id)), !this.state.block || this.state.block.type !== "table" || // when collaborating, the table element might be replaced and out of date // because yjs replaces the element when for example you change the color via the side menu !((r = this.tableElement) != null && r.isConnected)) ) { ((this.state.show = false), (this.state.showAddOrRemoveRowsButton = false), (this.state.showAddOrRemoveColumnsButton = false), this.emitUpdate()); return; } const { height: e, width: o } = tt(this.state.block); this.state.rowIndex !== void 0 && this.state.colIndex !== void 0 && (this.state.rowIndex >= e && (this.state.rowIndex = e - 1), this.state.colIndex >= o && (this.state.colIndex = o - 1)); const t = this.tableElement.querySelector("tbody"); if (!t) throw new Error("Table block does not contain a 'tbody' HTML element. This should never happen."); if (this.state.rowIndex !== void 0 && this.state.colIndex !== void 0) { const i = t.children[this.state.rowIndex].children[this.state.colIndex]; i ? (this.state.referencePosCell = i.getBoundingClientRect()) : ((this.state.rowIndex = void 0), (this.state.colIndex = void 0)); } ((this.state.referencePosTable = t.getBoundingClientRect()), this.emitUpdate()); } destroy() { (this.pmView.dom.removeEventListener("mousemove", this.mouseMoveHandler), window.removeEventListener("mouseup", this.mouseUpHandler), this.pmView.dom.removeEventListener("mousedown", this.viewMousedownHandler), this.pmView.root.removeEventListener("dragover", this.dragOverHandler), this.pmView.root.removeEventListener("drop", this.dropHandler)); } }; const V = new PluginKey("TableHandlesPlugin"), Yo$1 = a$1(({ editor: n }) => { let e; const o = f$2(void 0); return { key: "tableHandles", store: o, prosemirrorPlugins: [ new Plugin({ key: V, view: (t) => ( (e = new ro$1(n, t, (r) => { o.setState( r.block ? { ...r, draggingState: r.draggingState ? { ...r.draggingState } : void 0, } : void 0, ); })), e ), // We use decorations to render the drop cursor when dragging a table row // or column. The decorations are updated in the `dragOverHandler` method. props: { decorations: (t) => { if (e === void 0 || e.state === void 0 || e.state.draggingState === void 0 || e.tablePos === void 0) return; const r = e.state.draggingState.draggedCellOrientation === "row" ? e.state.rowIndex : e.state.colIndex; if (r === void 0) return; const s = [], { block: i, draggingState: a } = e.state, { originalIndex: c, draggedCellOrientation: l } = a; if (r === c || !i || (l === "row" && !Jt$2(i, c, r)) || (l === "col" && !_t$3(i, c, r))) return DecorationSet.create(t.doc, s); const u = t.doc.resolve(e.tablePos + 1); return ( e.state.draggingState.draggedCellOrientation === "row" ? yt$1(e.state.block, r).forEach(({ row: f, col: m }) => { const p = t.doc.resolve(u.posAtIndex(f) + 1), d = t.doc.resolve(p.posAtIndex(m) + 1), g = d.node(), b = d.pos + (r > c ? g.nodeSize - 2 : 0); s.push( // The widget is a small bar which spans the width of the cell. Decoration.widget(b, () => { const y = document.createElement("div"); return ( (y.className = "bn-table-drop-cursor"), (y.style.left = "0"), (y.style.right = "0"), r > c ? (y.style.bottom = "-2px") : (y.style.top = "-3px"), (y.style.height = "4px"), y ); }), ); }) : Ct$1(e.state.block, r).forEach(({ row: f, col: m }) => { const p = t.doc.resolve(u.posAtIndex(f) + 1), d = t.doc.resolve(p.posAtIndex(m) + 1), g = d.node(), b = d.pos + (r > c ? g.nodeSize - 2 : 0); s.push( // The widget is a small bar which spans the height of the cell. Decoration.widget(b, () => { const y = document.createElement("div"); return ( (y.className = "bn-table-drop-cursor"), (y.style.top = "0"), (y.style.bottom = "0"), r > c ? (y.style.right = "-2px") : (y.style.left = "-3px"), (y.style.width = "4px"), y ); }), ); }), DecorationSet.create(t.doc, s) ); }, }, }), ], /** * Callback that should be set on the `dragStart` event for whichever element * is used as the column drag handle. */ colDragStart(t) { if (e === void 0 || e.state === void 0 || e.state.colIndex === void 0) throw new Error("Attempted to drag table column, but no table block was hovered prior."); ((e.state.draggingState = { draggedCellOrientation: "col", originalIndex: e.state.colIndex, mousePos: t.clientX, }), e.emitUpdate(), n.transact((r) => r.setMeta(V, { draggedCellOrientation: e.state.draggingState.draggedCellOrientation, originalIndex: e.state.colIndex, newIndex: e.state.colIndex, tablePos: e.tablePos, }), ), !n.headless && (he$1(n.prosemirrorView.root), t.dataTransfer.setDragImage(B, 0, 0), (t.dataTransfer.effectAllowed = "move"))); }, /** * Callback that should be set on the `dragStart` event for whichever element * is used as the row drag handle. */ rowDragStart(t) { if (e.state === void 0 || e.state.rowIndex === void 0) throw new Error("Attempted to drag table row, but no table block was hovered prior."); ((e.state.draggingState = { draggedCellOrientation: "row", originalIndex: e.state.rowIndex, mousePos: t.clientY, }), e.emitUpdate(), n.transact((r) => r.setMeta(V, { draggedCellOrientation: e.state.draggingState.draggedCellOrientation, originalIndex: e.state.rowIndex, newIndex: e.state.rowIndex, tablePos: e.tablePos, }), ), !n.headless && (he$1(n.prosemirrorView.root), t.dataTransfer.setDragImage(B, 0, 0), (t.dataTransfer.effectAllowed = "copyMove"))); }, /** * Callback that should be set on the `dragEnd` event for both the element * used as the row drag handle, and the one used as the column drag handle. */ dragEnd() { if (e.state === void 0) throw new Error("Attempted to drag table row, but no table block was hovered prior."); ((e.state.draggingState = void 0), e.emitUpdate(), n.transact((t) => t.setMeta(V, null)), !n.headless && to$1(n.prosemirrorView.root)); }, /** * Freezes the drag handles. When frozen, they will stay attached to the same * cell regardless of which cell is hovered by the mouse cursor. */ freezeHandles() { e.menuFrozen = true; }, /** * Unfreezes the drag handles. When frozen, they will stay attached to the * same cell regardless of which cell is hovered by the mouse cursor. */ unfreezeHandles() { e.menuFrozen = false; }, getCellsAtRowHandle(t, r) { return yt$1(t, r); }, /** * Get all the cells in a column of the table block. */ getCellsAtColumnHandle(t, r) { return Ct$1(t, r); }, /** * Sets the selection to the given cell or a range of cells. * @returns The new state after the selection has been set. */ setCellSelection(t, r, s = r) { if (!e) throw new Error("Table handles view not initialized"); const i = t.doc.resolve(e.tablePos + 1), a = t.doc.resolve(i.posAtIndex(r.row) + 1), c = t.doc.resolve( // No need for +1, since CellSelection expects the position before the cell a.posAtIndex(r.col), ), l = t.doc.resolve(i.posAtIndex(s.row) + 1), u = t.doc.resolve( // No need for +1, since CellSelection expects the position before the cell l.posAtIndex(s.col), ), h = t.tr; return (h.setSelection(new CellSelection(c, u)), t.apply(h)); }, /** * Adds a row or column to the table using prosemirror-table commands */ addRowOrColumn(t, r) { n.exec((s, i) => { const a = this.setCellSelection(s, r.orientation === "row" ? { row: t, col: 0 } : { row: 0, col: t }); return ( r.orientation === "row" ? r.side === "above" ? addRowBefore(a, i) : addRowAfter(a, i) : r.side === "left" ? addColumnBefore(a, i) : addColumnAfter(a, i) ); }); }, /** * Removes a row or column from the table using prosemirror-table commands */ removeRowOrColumn(t, r) { return r === "row" ? n.exec((s, i) => { const a = this.setCellSelection(s, { row: t, col: 0, }); return deleteRow(a, i); }) : n.exec((s, i) => { const a = this.setCellSelection(s, { row: 0, col: t, }); return deleteColumn(a, i); }); }, /** * Merges the cells in the table block. */ mergeCells(t) { return n.exec((r, s) => { const i = t ? this.setCellSelection(r, t.relativeStartCell, t.relativeEndCell) : r; return mergeCells(i, s); }); }, /** * Splits the cell in the table block. * If no cell is provided, the current cell selected will be split. */ splitCell(t) { return n.exec((r, s) => { const i = t ? this.setCellSelection(r, t) : r; return splitCell(i, s); }); }, /** * Gets the start and end cells of the current cell selection. * @returns The start and end cells of the current cell selection. */ getCellSelection() { return n.transact((t) => { const r = t.selection; let s = r.$from, i = r.$to; if (Bo$1(r)) { const { ranges: d } = r; d.forEach((g) => { ((s = g.$from.min(s ?? g.$from)), (i = g.$to.max(i ?? g.$to))); }); } else if (((s = t.doc.resolve(r.$from.pos - r.$from.parentOffset - 1)), (i = t.doc.resolve(r.$to.pos - r.$to.parentOffset - 1)), s.pos === 0 || i.pos === 0)) return; const a = t.doc.resolve(s.pos - s.parentOffset - 1), c = t.doc.resolve(i.pos - i.parentOffset - 1), l = t.doc.resolve(a.pos - a.parentOffset - 1), u = s.index(a.depth), h = a.index(l.depth), f = i.index(c.depth), m = c.index(l.depth), p = []; for (let d = h; d <= m; d++) for (let g = u; g <= f; g++) p.push({ row: d, col: g }); return { from: { row: h, col: u, }, to: { row: m, col: f, }, cells: p, }; }); }, /** * Gets the direction of the merge based on the current cell selection. * * Returns undefined when there is no cell selection, or the selection is not within a table. */ getMergeDirection(t) { return n.transact((r) => { const s = Bo$1(r.selection) ? r.selection : void 0; if ( !s || !t || // Only offer the merge button if there is more than one cell selected. s.ranges.length <= 1 ) return; const i = this.getCellSelection(); if (i) return Ht$2(i.from, i.to, t) ? "vertical" : "horizontal"; }); }, cropEmptyRowsOrColumns(t, r) { return Rt$2(t, r); }, addRowsOrColumns(t, r, s) { return $t$2(t, r, s); }, }; }), me = new PluginKey("trailingNode"), jo$1 = a$1(() => ({ key: "trailingNode", prosemirrorPlugins: [ new Plugin({ key: me, appendTransaction: (n, e, o) => { const { doc: t, tr: r, schema: s } = o, i = me.getState(o), a = t.content.size - 2, c = s.nodes.blockContainer, l = s.nodes.paragraph; if (i) return r.insert(a, c.create(void 0, l.create())); }, state: { init: (n, e) => {}, apply: (n, e) => { if (!n.docChanged) return e; let o = n.doc.lastChild; if (!o || o.type.name !== "blockGroup") throw new Error("Expected blockGroup"); if (((o = o.lastChild), !o || o.type.name !== "blockContainer")) return true; const t = o.firstChild; if (!t) throw new Error("Expected blockContent"); return o.nodeSize > 4 || t.type.spec.content !== "inline*"; }, }, }), ], })); var u = Object.defineProperty; var k$1 = (t, e, n) => (e in t ? u(t, e, { enumerable: true, configurable: true, writable: true, value: n }) : (t[e] = n)); var i$1 = (t, e, n) => k$1(t, typeof e != "symbol" ? e + "" : e, n); function E(t) { const e = v(t); let { roots: n, nonRoots: r } = f$1(e); const s = []; for (; n.size; ) { s.push(n); const o = /* @__PURE__ */ new Set(); for (const c of n) { const a = t.get(c); if (a) for (const l of a) { const p = e.get(l); if (p === void 0) continue; const d = p - 1; (e.set(l, d), d === 0 && o.add(l)); } } n = o; } if (((r = f$1(e).nonRoots), r.size)) throw new Error(`Cycle(s) detected; toposort only works on acyclic graphs. Cyclic nodes: ${Array.from(r).join(", ")}`); return s; } function D(t) { const e = I(t); return E(e); } function v(t) { const e = /* @__PURE__ */ new Map(); for (const [n, r] of t.entries()) { e.has(n) || e.set(n, 0); for (const s of r) { const o = e.get(s) ?? 0; e.set(s, o + 1); } } return e; } function f$1(t) { const e = /* @__PURE__ */ new Set(), n = /* @__PURE__ */ new Set(); for (const [r, s] of t.entries()) s === 0 ? e.add(r) : n.add(r); return { roots: e, nonRoots: n }; } function I(t) { const e = /* @__PURE__ */ new Map(); for (const [n, r] of t.entries()) { e.has(n) || e.set(n, /* @__PURE__ */ new Set()); for (const s of r) (e.has(s) || e.set(s, /* @__PURE__ */ new Set()), e.get(s).add(n)); } return e; } function A() { return /* @__PURE__ */ new Map(); } function m(t, e, n) { return (t.has(e) || t.set(e, /* @__PURE__ */ new Set()), t.get(e).add(n), t); } function P(t) { const e = A(); for (const s of t) Array.isArray(s.runsBefore) && s.runsBefore.length > 0 ? s.runsBefore.forEach((o) => { m(e, s.key, o); }) : m(e, "default", s.key); const n = D(e), r = n.findIndex((s) => s.has("default")); return (s) => 91 + (n.findIndex((c) => c.has(s)) + r) * 10; } function S(t) { return t && Object.fromEntries(Object.entries(t).filter(([, e]) => e !== void 0)); } class N { constructor(e) { // Helper so that you can use typeof schema.BlockNoteEditor i$1(this, "BlockNoteEditor", "only for types"); i$1(this, "Block", "only for types"); i$1(this, "PartialBlock", "only for types"); i$1(this, "inlineContentSpecs"); i$1(this, "styleSpecs"); i$1(this, "blockSpecs"); i$1(this, "blockSchema"); i$1(this, "inlineContentSchema"); i$1(this, "styleSchema"); this.opts = e; const { blockSpecs: n, inlineContentSpecs: r, styleSpecs: s, blockSchema: o, inlineContentSchema: c, styleSchema: a } = this.init(); ((this.blockSpecs = n), (this.styleSpecs = s), (this.styleSchema = a), (this.inlineContentSpecs = r), (this.blockSchema = o), (this.inlineContentSchema = c)); } init() { const e = P( Object.entries({ ...this.opts.blockSpecs, ...this.opts.inlineContentSpecs, ...this.opts.styleSpecs, }).map(([o, c]) => { var a; return { key: o, runsBefore: ((a = c.implementation) == null ? void 0 : a.runsBefore) ?? [], }; }), ), n = Object.fromEntries(Object.entries(this.opts.blockSpecs).map(([o, c]) => [o, ho$1(c.config, c.implementation, c.extensions, e(o))])), r = Object.fromEntries( Object.entries(this.opts.inlineContentSpecs).map(([o, c]) => { var a; return typeof c.config != "object" ? [o, c] : [ o, { ...c, implementation: { ...c.implementation, node: (a = c.implementation) == null ? void 0 : a.node.extend({ priority: e(o), }), }, }, ]; }), ), s = Object.fromEntries( Object.entries(this.opts.styleSpecs).map(([o, c]) => { var a; return [ o, { ...c, implementation: { ...c.implementation, mark: (a = c.implementation) == null ? void 0 : a.mark.extend({ priority: e(o), }), }, }, ]; }), ); return { blockSpecs: n, blockSchema: Object.fromEntries(Object.entries(n).map(([o, c]) => [o, c.config])), inlineContentSpecs: S(r), styleSpecs: S(s), inlineContentSchema: Mt$1(r), styleSchema: Lt$1(s), }; } /** * Adds additional block specs to the current schema in a builder pattern. * This method allows extending the schema after it has been created. * * @param additionalBlockSpecs - Additional block specs to add to the schema * @returns The current schema instance for chaining */ extend(e) { (Object.assign(this.opts.blockSpecs, e.blockSpecs), Object.assign(this.opts.inlineContentSpecs, e.inlineContentSpecs), Object.assign(this.opts.styleSpecs, e.styleSpecs)); const { blockSpecs: n, inlineContentSpecs: r, styleSpecs: s, blockSchema: o, inlineContentSchema: c, styleSchema: a } = this.init(); return ((this.blockSpecs = n), (this.styleSpecs = s), (this.styleSchema = a), (this.inlineContentSpecs = r), (this.blockSchema = o), (this.inlineContentSchema = c), this); } } let h$1 = class h extends N { static create(e) { return new h({ blockSpecs: (e == null ? void 0 : e.blockSpecs) ?? Po$2, inlineContentSpecs: (e == null ? void 0 : e.inlineContentSpecs) ?? Un, styleSpecs: (e == null ? void 0 : e.styleSpecs) ?? $n$1, }); } }; var h = Object.defineProperty; var b$1 = (a, s, l) => (s in a ? h(a, s, { enumerable: true, configurable: true, writable: true, value: l }) : (a[s] = l)); var t = (a, s, l) => b$1(a, s + "", l); class f { constructor() { // eslint-disable-next-line @typescript-eslint/ban-types t(this, "callbacks", {}); } on(s, l) { return (this.callbacks[s] || (this.callbacks[s] = []), this.callbacks[s].push(l), () => this.off(s, l)); } emit(s, ...l) { const c = this.callbacks[s]; c && c.forEach((i) => i.apply(this, l)); } off(s, l) { const c = this.callbacks[s]; c && (l ? (this.callbacks[s] = c.filter((i) => i !== l)) : delete this.callbacks[s]); } removeAllListeners() { this.callbacks = {}; } } const i = { slash_menu: { heading: { title: "Heading 1", subtext: "Top-level heading", aliases: ["h", "heading1", "h1"], group: "Headings", }, heading_2: { title: "Heading 2", subtext: "Key section heading", aliases: ["h2", "heading2", "subheading"], group: "Headings", }, heading_3: { title: "Heading 3", subtext: "Subsection and group heading", aliases: ["h3", "heading3", "subheading"], group: "Headings", }, heading_4: { title: "Heading 4", subtext: "Minor subsection heading", aliases: ["h4", "heading4", "subheading4"], group: "Subheadings", }, heading_5: { title: "Heading 5", subtext: "Small subsection heading", aliases: ["h5", "heading5", "subheading5"], group: "Subheadings", }, heading_6: { title: "Heading 6", subtext: "Lowest-level heading", aliases: ["h6", "heading6", "subheading6"], group: "Subheadings", }, toggle_heading: { title: "Toggle Heading 1", subtext: "Toggleable top-level heading", aliases: ["h", "heading1", "h1", "collapsable"], group: "Subheadings", }, toggle_heading_2: { title: "Toggle Heading 2", subtext: "Toggleable key section heading", aliases: ["h2", "heading2", "subheading", "collapsable"], group: "Subheadings", }, toggle_heading_3: { title: "Toggle Heading 3", subtext: "Toggleable subsection and group heading", aliases: ["h3", "heading3", "subheading", "collapsable"], group: "Subheadings", }, quote: { title: "Quote", subtext: "Quote or excerpt", aliases: ["quotation", "blockquote", "bq"], group: "Basic blocks", }, toggle_list: { title: "Toggle List", subtext: "List with hideable sub-items", aliases: ["li", "list", "toggleList", "toggle list", "collapsable list"], group: "Basic blocks", }, numbered_list: { title: "Numbered List", subtext: "List with ordered items", aliases: ["ol", "li", "list", "numberedlist", "numbered list"], group: "Basic blocks", }, bullet_list: { title: "Bullet List", subtext: "List with unordered items", aliases: ["ul", "li", "list", "bulletlist", "bullet list"], group: "Basic blocks", }, check_list: { title: "Check List", subtext: "List with checkboxes", aliases: ["ul", "li", "list", "checklist", "check list", "checked list", "checkbox"], group: "Basic blocks", }, paragraph: { title: "Paragraph", subtext: "The body of your document", aliases: ["p", "paragraph"], group: "Basic blocks", }, code_block: { title: "Code Block", subtext: "Code block with syntax highlighting", aliases: ["code", "pre"], group: "Basic blocks", }, page_break: { title: "Page Break", subtext: "Page separator", aliases: ["page", "break", "separator"], group: "Basic blocks", }, table: { title: "Table", subtext: "Table with editable cells", aliases: ["table"], group: "Advanced", }, image: { title: "Image", subtext: "Resizable image with caption", aliases: ["image", "imageUpload", "upload", "img", "picture", "media", "url"], group: "Media", }, video: { title: "Video", subtext: "Resizable video with caption", aliases: ["video", "videoUpload", "upload", "mp4", "film", "media", "url"], group: "Media", }, audio: { title: "Audio", subtext: "Embedded audio with caption", aliases: ["audio", "audioUpload", "upload", "mp3", "sound", "media", "url"], group: "Media", }, file: { title: "File", subtext: "Embedded file", aliases: ["file", "upload", "embed", "media", "url"], group: "Media", }, emoji: { title: "Emoji", subtext: "Search for and insert an emoji", aliases: ["emoji", "emote", "emotion", "face"], group: "Others", }, divider: { title: "Divider", subtext: "Visually divide blocks", aliases: ["divider", "hr", "line", "horizontal rule"], group: "Basic blocks", }, }, placeholders: { default: "Enter text or type '/' for commands", heading: "Heading", toggleListItem: "Toggle", bulletListItem: "List", numberedListItem: "List", checkListItem: "List", emptyDocument: void 0, new_comment: "Write a comment...", edit_comment: "Edit comment...", comment_reply: "Add comment...", }, file_blocks: { add_button_text: { image: "Add image", video: "Add video", audio: "Add audio", file: "Add file", }, }, toggle_blocks: { add_block_button: "Empty toggle. Click to add a block.", }, // from react package: side_menu: { add_block_label: "Add block", drag_handle_label: "Open block menu", }, drag_handle: { delete_menuitem: "Delete", colors_menuitem: "Colors", header_row_menuitem: "Header row", header_column_menuitem: "Header column", }, table_handle: { delete_column_menuitem: "Delete column", delete_row_menuitem: "Delete row", add_left_menuitem: "Add column left", add_right_menuitem: "Add column right", add_above_menuitem: "Add row above", add_below_menuitem: "Add row below", split_cell_menuitem: "Split cell", merge_cells_menuitem: "Merge cells", background_color_menuitem: "Background color", }, suggestion_menu: { no_items_title: "No items found", }, color_picker: { text_title: "Text", background_title: "Background", colors: { default: "Default", gray: "Gray", brown: "Brown", red: "Red", orange: "Orange", yellow: "Yellow", green: "Green", blue: "Blue", purple: "Purple", pink: "Pink", }, }, formatting_toolbar: { bold: { tooltip: "Bold", secondary_tooltip: "Mod+B", }, italic: { tooltip: "Italic", secondary_tooltip: "Mod+I", }, underline: { tooltip: "Underline", secondary_tooltip: "Mod+U", }, strike: { tooltip: "Strike", secondary_tooltip: "Mod+Shift+S", }, code: { tooltip: "Code", secondary_tooltip: "", }, colors: { tooltip: "Colors", }, link: { tooltip: "Create link", secondary_tooltip: "Mod+K", }, file_caption: { tooltip: "Edit caption", input_placeholder: "Edit caption", }, file_replace: { tooltip: { image: "Replace image", video: "Replace video", audio: "Replace audio", file: "Replace file", }, }, file_rename: { tooltip: { image: "Rename image", video: "Rename video", audio: "Rename audio", file: "Rename file", }, input_placeholder: { image: "Rename image", video: "Rename video", audio: "Rename audio", file: "Rename file", }, }, file_download: { tooltip: { image: "Download image", video: "Download video", audio: "Download audio", file: "Download file", }, }, file_delete: { tooltip: { image: "Delete image", video: "Delete video", audio: "Delete audio", file: "Delete file", }, }, file_preview_toggle: { tooltip: "Toggle preview", }, nest: { tooltip: "Nest block", secondary_tooltip: "Tab", }, unnest: { tooltip: "Unnest block", secondary_tooltip: "Shift+Tab", }, align_left: { tooltip: "Align text left", }, align_center: { tooltip: "Align text center", }, align_right: { tooltip: "Align text right", }, align_justify: { tooltip: "Justify text", }, table_cell_merge: { tooltip: "Merge cells", }, comment: { tooltip: "Add comment", }, }, file_panel: { upload: { title: "Upload", file_placeholder: { image: "Upload image", video: "Upload video", audio: "Upload audio", file: "Upload file", }, upload_error: "Error: Upload failed", }, embed: { title: "Embed", embed_button: { image: "Embed image", video: "Embed video", audio: "Embed audio", file: "Embed file", }, url_placeholder: "Enter URL", }, }, link_toolbar: { delete: { tooltip: "Remove link", }, edit: { text: "Edit link", tooltip: "Edit", }, open: { tooltip: "Open in new tab", }, form: { title_placeholder: "Edit title", url_placeholder: "Edit URL", }, }, comments: { edited: "edited", save_button_text: "Save", cancel_button_text: "Cancel", actions: { add_reaction: "Add reaction", resolve: "Resolve", edit_comment: "Edit comment", delete_comment: "Delete comment", more_actions: "More actions", }, reactions: { reacted_by: "Reacted by", }, sidebar: { marked_as_resolved: "Marked as resolved", more_replies: (e) => `${e} more replies`, }, }, generic: { ctrl_shortcut: "Ctrl", }, }; /// Input rules are regular expressions describing a piece of text /// that, when typed, causes something to happen. This might be /// changing two dashes into an emdash, wrapping a paragraph starting /// with `"> "` into a blockquote, or something entirely different. class InputRule { match; /// @internal handler; /// @internal undoable; inCode; inCodeMark; /// Create an input rule. The rule applies when the user typed /// something and the text directly in front of the cursor matches /// `match`, which should end with `$`. /// /// The `handler` can be a string, in which case the matched text, or /// the first matched group in the regexp, is replaced by that /// string. /// /// Or a it can be a function, which will be called with the match /// array produced by /// [`RegExp.exec`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec), /// as well as the start and end of the matched range, and which can /// return a [transaction](#state.Transaction) that describes the /// rule's effect, or null to indicate the input was not handled. constructor( /// @internal match, handler, options = {}, ) { this.match = match; this.match = match; this.handler = typeof handler == "string" ? stringHandler(handler) : handler; this.undoable = options.undoable !== false; this.inCode = options.inCode || false; this.inCodeMark = options.inCodeMark !== false; } } function stringHandler(string) { return function (state, match, start, end) { let insert = string; if (match[1]) { let offset = match[0].lastIndexOf(match[1]); insert += match[0].slice(offset + match[1].length); start += offset; let cutOff = start - end; if (cutOff > 0) { insert = match[0].slice(offset - cutOff, offset) + insert; start = end; } } return state.tr.insertText(insert, start, end); }; } const MAX_MATCH = 500; /// Create an input rules plugin. When enabled, it will cause text /// input that matches any of the given rules to trigger the rule's /// action. function inputRules({ rules }) { let plugin = new Plugin({ state: { init() { return null; }, apply(tr, prev) { let stored = tr.getMeta(this); if (stored) return stored; return tr.selectionSet || tr.docChanged ? null : prev; }, }, props: { handleTextInput(view, from, to, text) { return run(view, from, to, text, rules, plugin); }, handleDOMEvents: { compositionend: (view) => { setTimeout(() => { let { $cursor } = view.state.selection; if ($cursor) run(view, $cursor.pos, $cursor.pos, "", rules, plugin); }); }, }, }, isInputRules: true, }); return plugin; } function run(view, from, to, text, rules, plugin) { if (view.composing) return false; // Actually insert the text so that its insertion // can be tracked by, e.g., the history plugin const insertTr = view.state.tr.insertText(text, from, to); const mappedFrom = insertTr.mapping.map(from); const mappedTo = insertTr.mapping.map(to); let state = null, $from = insertTr.doc.resolve(mappedFrom); let textBefore = $from.parent.textBetween(Math.max(0, $from.parentOffset - MAX_MATCH), $from.parentOffset, null, "\ufffc"); for (let i = 0; i < rules.length; i++) { let rule = rules[i]; if (!rule.inCodeMark && $from.marks().some((m) => m.type.spec.code)) continue; if ($from.parent.type.spec.code) { if (!rule.inCode) continue; } else if (rule.inCode === "only") { continue; } let match = rule.match.exec(textBefore); if (!match || match[0].length < text.length) continue; state ??= view.state.apply(insertTr); let tr = rule.handler(state, match, mappedFrom - match[0].length, mappedTo); if (!tr) continue; view.dispatch(insertTr); view.dispatch(closeHistory(view.state.tr)); if (rule.undoable) tr.setMeta(plugin, { transform: tr, from: mappedFrom, to: mappedTo, text, }); view.dispatch(tr); return true; } return false; } /// Converts double dashes to an emdash. new InputRule(/--$/, "—", { inCodeMark: false }); /// Converts three dots to an ellipsis character. new InputRule(/\.\.\.$/, "…", { inCodeMark: false }); /// “Smart” opening double quotes. new InputRule(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(")$/, "“", { inCodeMark: false }); /// “Smart” closing double quotes. new InputRule(/"$/, "”", { inCodeMark: false }); /// “Smart” opening single quotes. new InputRule(/(?:^|[\s\{\[\(\<'"\u2018\u201C])(')$/, "‘", { inCodeMark: false }); /// “Smart” closing single quotes. new InputRule(/'$/, "’", { inCodeMark: false }); // src/gap-cursor/gap-cursor.ts var Gapcursor = Extension.create({ name: "gapCursor", addProseMirrorPlugins() { return [gapCursor()]; }, extendNodeSchema(extension) { var _a; const context = { name: extension.name, options: extension.options, storage: extension.storage, }; return { allowGapCursor: (_a = callOrReturn(getExtensionField(extension, "allowGapCursor", context))) != null ? _a : null, }; }, }); const s = new PluginKey("blocknote-show-selection"), b = a$1(({ editor: a }) => { const e = f$2( { enabledSet: /* @__PURE__ */ new Set() }, { onUpdate() { a.transact((t) => t.setMeta(s, {})); }, }, ); return { key: "showSelection", store: e, prosemirrorPlugins: [ new Plugin({ key: s, props: { decorations: (t) => { const { doc: o, selection: n } = t; if (e.state.enabledSet.size === 0) return DecorationSet.empty; const i = Decoration.inline(n.from, n.to, { "data-show-selection": "true", }); return DecorationSet.create(o, [i]); }, }, }), ], /** * Show or hide the selection decoration * * @param shouldShow - Whether to show the selection decoration * @param key - The key of the selection to show or hide, * this is necessary to prevent disabling ShowSelection from one place * will interfere with other parts of the code that need to show the selection decoration * (e.g.: CreateLinkButton and AIExtension) */ showSelection(t, o) { e.setState({ enabledSet: t ? /* @__PURE__ */ new Set([...e.state.enabledSet, o]) : new Set([...e.state.enabledSet].filter((n) => n !== o)), }); }, }; }); /** * @import { * InitialConstruct, * Initializer, * State, * TokenizeContext, * Token * } from 'micromark-util-types' */ /** @type {InitialConstruct} */ const content = { tokenize: initializeContent, }; /** * @this {TokenizeContext} * Context. * @type {Initializer} * Content. */ function initializeContent(effects) { const contentStart = effects.attempt(this.parser.constructs.contentInitial, afterContentStartConstruct, paragraphInitial); /** @type {Token} */ let previous; return contentStart; /** @type {State} */ function afterContentStartConstruct(code) { if (code === null) { effects.consume(code); return; } effects.enter("lineEnding"); effects.consume(code); effects.exit("lineEnding"); return factorySpace(effects, contentStart, "linePrefix"); } /** @type {State} */ function paragraphInitial(code) { effects.enter("paragraph"); return lineStart(code); } /** @type {State} */ function lineStart(code) { const token = effects.enter("chunkText", { contentType: "text", previous, }); if (previous) { previous.next = token; } previous = token; return data(code); } /** @type {State} */ function data(code) { if (code === null) { effects.exit("chunkText"); effects.exit("paragraph"); effects.consume(code); return; } if (markdownLineEnding(code)) { effects.consume(code); effects.exit("chunkText"); return lineStart; } // Data. effects.consume(code); return data; } } /** * @import { * Construct, * ContainerState, * InitialConstruct, * Initializer, * Point, * State, * TokenizeContext, * Tokenizer, * Token * } from 'micromark-util-types' */ /** @type {InitialConstruct} */ const document$2 = { tokenize: initializeDocument, }; /** @type {Construct} */ const containerConstruct = { tokenize: tokenizeContainer, }; /** * @this {TokenizeContext} * Self. * @type {Initializer} * Initializer. */ function initializeDocument(effects) { const self = this; /** @type {Array} */ const stack = []; let continued = 0; /** @type {TokenizeContext | undefined} */ let childFlow; /** @type {Token | undefined} */ let childToken; /** @type {number} */ let lineStartOffset; return start; /** @type {State} */ function start(code) { // First we iterate through the open blocks, starting with the root // document, and descending through last children down to the last open // block. // Each block imposes a condition that the line must satisfy if the block is // to remain open. // For example, a block quote requires a `>` character. // A paragraph requires a non-blank line. // In this phase we may match all or just some of the open blocks. // But we cannot close unmatched blocks yet, because we may have a lazy // continuation line. if (continued < stack.length) { const item = stack[continued]; self.containerState = item[1]; return effects.attempt(item[0].continuation, documentContinue, checkNewContainers)(code); } // Done. return checkNewContainers(code); } /** @type {State} */ function documentContinue(code) { continued++; // Note: this field is called `_closeFlow` but it also closes containers. // Perhaps a good idea to rename it but it’s already used in the wild by // extensions. if (self.containerState._closeFlow) { self.containerState._closeFlow = undefined; if (childFlow) { closeFlow(); } // Note: this algorithm for moving events around is similar to the // algorithm when dealing with lazy lines in `writeToChild`. const indexBeforeExits = self.events.length; let indexBeforeFlow = indexBeforeExits; /** @type {Point | undefined} */ let point; // Find the flow chunk. while (indexBeforeFlow--) { if (self.events[indexBeforeFlow][0] === "exit" && self.events[indexBeforeFlow][1].type === "chunkFlow") { point = self.events[indexBeforeFlow][1].end; break; } } exitContainers(continued); // Fix positions. let index = indexBeforeExits; while (index < self.events.length) { self.events[index][1].end = { ...point, }; index++; } // Inject the exits earlier (they’re still also at the end). splice(self.events, indexBeforeFlow + 1, 0, self.events.slice(indexBeforeExits)); // Discard the duplicate exits. self.events.length = index; return checkNewContainers(code); } return start(code); } /** @type {State} */ function checkNewContainers(code) { // Next, after consuming the continuation markers for existing blocks, we // look for new block starts (e.g. `>` for a block quote). // If we encounter a new block start, we close any blocks unmatched in // step 1 before creating the new block as a child of the last matched // block. if (continued === stack.length) { // No need to `check` whether there’s a container, of `exitContainers` // would be moot. // We can instead immediately `attempt` to parse one. if (!childFlow) { return documentContinued(code); } // If we have concrete content, such as block HTML or fenced code, // we can’t have containers “pierce” into them, so we can immediately // start. if (childFlow.currentConstruct && childFlow.currentConstruct.concrete) { return flowStart(code); } // If we do have flow, it could still be a blank line, // but we’d be interrupting it w/ a new container if there’s a current // construct. // To do: next major: remove `_gfmTableDynamicInterruptHack` (no longer // needed in micromark-extension-gfm-table@1.0.6). self.interrupt = Boolean(childFlow.currentConstruct && !childFlow._gfmTableDynamicInterruptHack); } // Check if there is a new container. self.containerState = {}; return effects.check(containerConstruct, thereIsANewContainer, thereIsNoNewContainer)(code); } /** @type {State} */ function thereIsANewContainer(code) { if (childFlow) closeFlow(); exitContainers(continued); return documentContinued(code); } /** @type {State} */ function thereIsNoNewContainer(code) { self.parser.lazy[self.now().line] = continued !== stack.length; lineStartOffset = self.now().offset; return flowStart(code); } /** @type {State} */ function documentContinued(code) { // Try new containers. self.containerState = {}; return effects.attempt(containerConstruct, containerContinue, flowStart)(code); } /** @type {State} */ function containerContinue(code) { continued++; stack.push([self.currentConstruct, self.containerState]); // Try another. return documentContinued(code); } /** @type {State} */ function flowStart(code) { if (code === null) { if (childFlow) closeFlow(); exitContainers(0); effects.consume(code); return; } childFlow = childFlow || self.parser.flow(self.now()); effects.enter("chunkFlow", { _tokenizer: childFlow, contentType: "flow", previous: childToken, }); return flowContinue(code); } /** @type {State} */ function flowContinue(code) { if (code === null) { writeToChild(effects.exit("chunkFlow"), true); exitContainers(0); effects.consume(code); return; } if (markdownLineEnding(code)) { effects.consume(code); writeToChild(effects.exit("chunkFlow")); // Get ready for the next line. continued = 0; self.interrupt = undefined; return start; } effects.consume(code); return flowContinue; } /** * @param {Token} token * Token. * @param {boolean | undefined} [endOfFile] * Whether the token is at the end of the file (default: `false`). * @returns {undefined} * Nothing. */ function writeToChild(token, endOfFile) { const stream = self.sliceStream(token); if (endOfFile) stream.push(null); token.previous = childToken; if (childToken) childToken.next = token; childToken = token; childFlow.defineSkip(token.start); childFlow.write(stream); // Alright, so we just added a lazy line: // // ```markdown // > a // b. // // Or: // // > ~~~c // d // // Or: // // > | e | // f // ``` // // The construct in the second example (fenced code) does not accept lazy // lines, so it marked itself as done at the end of its first line, and // then the content construct parses `d`. // Most constructs in markdown match on the first line: if the first line // forms a construct, a non-lazy line can’t “unmake” it. // // The construct in the third example is potentially a GFM table, and // those are *weird*. // It *could* be a table, from the first line, if the following line // matches a condition. // In this case, that second line is lazy, which “unmakes” the first line // and turns the whole into one content block. // // We’ve now parsed the non-lazy and the lazy line, and can figure out // whether the lazy line started a new flow block. // If it did, we exit the current containers between the two flow blocks. if (self.parser.lazy[token.start.line]) { let index = childFlow.events.length; while (index--) { if ( // The token starts before the line ending… childFlow.events[index][1].start.offset < lineStartOffset && // …and either is not ended yet… (!childFlow.events[index][1].end || // …or ends after it. childFlow.events[index][1].end.offset > lineStartOffset) ) { // Exit: there’s still something open, which means it’s a lazy line // part of something. return; } } // Note: this algorithm for moving events around is similar to the // algorithm when closing flow in `documentContinue`. const indexBeforeExits = self.events.length; let indexBeforeFlow = indexBeforeExits; /** @type {boolean | undefined} */ let seen; /** @type {Point | undefined} */ let point; // Find the previous chunk (the one before the lazy line). while (indexBeforeFlow--) { if (self.events[indexBeforeFlow][0] === "exit" && self.events[indexBeforeFlow][1].type === "chunkFlow") { if (seen) { point = self.events[indexBeforeFlow][1].end; break; } seen = true; } } exitContainers(continued); // Fix positions. index = indexBeforeExits; while (index < self.events.length) { self.events[index][1].end = { ...point, }; index++; } // Inject the exits earlier (they’re still also at the end). splice(self.events, indexBeforeFlow + 1, 0, self.events.slice(indexBeforeExits)); // Discard the duplicate exits. self.events.length = index; } } /** * @param {number} size * Size. * @returns {undefined} * Nothing. */ function exitContainers(size) { let index = stack.length; // Exit open containers. while (index-- > size) { const entry = stack[index]; self.containerState = entry[1]; entry[0].exit.call(self, effects); } stack.length = size; } function closeFlow() { childFlow.write([null]); childToken = undefined; childFlow = undefined; self.containerState._closeFlow = undefined; } } /** * @this {TokenizeContext} * Context. * @type {Tokenizer} * Tokenizer. */ function tokenizeContainer(effects, ok, nok) { // Always populated by defaults. return factorySpace(effects, effects.attempt(this.parser.constructs.document, ok, nok), "linePrefix", this.parser.constructs.disable.null.includes("codeIndented") ? undefined : 4); } /** * @import { * InitialConstruct, * Initializer, * State, * TokenizeContext * } from 'micromark-util-types' */ /** @type {InitialConstruct} */ const flow$1 = { tokenize: initializeFlow, }; /** * @this {TokenizeContext} * Self. * @type {Initializer} * Initializer. */ function initializeFlow(effects) { const self = this; const initial = effects.attempt( // Try to parse a blank line. blankLine, atBlankEnding, // Try to parse initial flow (essentially, only code). effects.attempt( this.parser.constructs.flowInitial, afterConstruct, factorySpace(effects, effects.attempt(this.parser.constructs.flow, afterConstruct, effects.attempt(content$1, afterConstruct)), "linePrefix"), ), ); return initial; /** @type {State} */ function atBlankEnding(code) { if (code === null) { effects.consume(code); return; } effects.enter("lineEndingBlank"); effects.consume(code); effects.exit("lineEndingBlank"); self.currentConstruct = undefined; return initial; } /** @type {State} */ function afterConstruct(code) { if (code === null) { effects.consume(code); return; } effects.enter("lineEnding"); effects.consume(code); effects.exit("lineEnding"); self.currentConstruct = undefined; return initial; } } /** * @import { * Code, * InitialConstruct, * Initializer, * Resolver, * State, * TokenizeContext * } from 'micromark-util-types' */ const resolver = { resolveAll: createResolver(), }; const string$1 = initializeFactory("string"); const text$3 = initializeFactory("text"); /** * @param {'string' | 'text'} field * Field. * @returns {InitialConstruct} * Construct. */ function initializeFactory(field) { return { resolveAll: createResolver(field === "text" ? resolveAllLineSuffixes : undefined), tokenize: initializeText, }; /** * @this {TokenizeContext} * Context. * @type {Initializer} */ function initializeText(effects) { const self = this; const constructs = this.parser.constructs[field]; const text = effects.attempt(constructs, start, notText); return start; /** @type {State} */ function start(code) { return atBreak(code) ? text(code) : notText(code); } /** @type {State} */ function notText(code) { if (code === null) { effects.consume(code); return; } effects.enter("data"); effects.consume(code); return data; } /** @type {State} */ function data(code) { if (atBreak(code)) { effects.exit("data"); return text(code); } // Data. effects.consume(code); return data; } /** * @param {Code} code * Code. * @returns {boolean} * Whether the code is a break. */ function atBreak(code) { if (code === null) { return true; } const list = constructs[code]; let index = -1; if (list) { // Always populated by defaults. while (++index < list.length) { const item = list[index]; if (!item.previous || item.previous.call(self, self.previous)) { return true; } } } return false; } } } /** * @param {Resolver | undefined} [extraResolver] * Resolver. * @returns {Resolver} * Resolver. */ function createResolver(extraResolver) { return resolveAllText; /** @type {Resolver} */ function resolveAllText(events, context) { let index = -1; /** @type {number | undefined} */ let enter; // A rather boring computation (to merge adjacent `data` events) which // improves mm performance by 29%. while (++index <= events.length) { if (enter === undefined) { if (events[index] && events[index][1].type === "data") { enter = index; index++; } } else if (!events[index] || events[index][1].type !== "data") { // Don’t do anything if there is one data token. if (index !== enter + 2) { events[enter][1].end = events[index - 1][1].end; events.splice(enter + 2, index - enter - 2); index = enter + 2; } enter = undefined; } } return extraResolver ? extraResolver(events, context) : events; } } /** * A rather ugly set of instructions which again looks at chunks in the input * stream. * The reason to do this here is that it is *much* faster to parse in reverse. * And that we can’t hook into `null` to split the line suffix before an EOF. * To do: figure out if we can make this into a clean utility, or even in core. * As it will be useful for GFMs literal autolink extension (and maybe even * tables?) * * @type {Resolver} */ function resolveAllLineSuffixes(events, context) { let eventIndex = 0; // Skip first. while (++eventIndex <= events.length) { if ((eventIndex === events.length || events[eventIndex][1].type === "lineEnding") && events[eventIndex - 1][1].type === "data") { const data = events[eventIndex - 1][1]; const chunks = context.sliceStream(data); let index = chunks.length; let bufferIndex = -1; let size = 0; /** @type {boolean | undefined} */ let tabs; while (index--) { const chunk = chunks[index]; if (typeof chunk === "string") { bufferIndex = chunk.length; while (chunk.charCodeAt(bufferIndex - 1) === 32) { size++; bufferIndex--; } if (bufferIndex) break; bufferIndex = -1; } // Number else if (chunk === -2) { tabs = true; size++; } else if (chunk === -1); else { // Replacement character, exit. index++; break; } } // Allow final trailing whitespace. if (context._contentTypeTextTrailing && eventIndex === events.length) { size = 0; } if (size) { const token = { type: eventIndex === events.length || tabs || size < 2 ? "lineSuffix" : "hardBreakTrailing", start: { _bufferIndex: index ? bufferIndex : data.start._bufferIndex + bufferIndex, _index: data.start._index + index, line: data.end.line, column: data.end.column - size, offset: data.end.offset - size, }, end: { ...data.end, }, }; data.end = { ...token.start, }; if (data.start.offset === data.end.offset) { Object.assign(data, token); } else { events.splice(eventIndex, 0, ["enter", token, context], ["exit", token, context]); eventIndex += 2; } } eventIndex++; } } return events; } /** * @import {Extension} from 'micromark-util-types' */ /** @satisfies {Extension['document']} */ const document$1 = { [42]: list$1, [43]: list$1, [45]: list$1, [48]: list$1, [49]: list$1, [50]: list$1, [51]: list$1, [52]: list$1, [53]: list$1, [54]: list$1, [55]: list$1, [56]: list$1, [57]: list$1, [62]: blockQuote, }; /** @satisfies {Extension['contentInitial']} */ const contentInitial = { [91]: definition, }; /** @satisfies {Extension['flowInitial']} */ const flowInitial = { [-2]: codeIndented, [-1]: codeIndented, [32]: codeIndented, }; /** @satisfies {Extension['flow']} */ const flow = { [35]: headingAtx, [42]: thematicBreak$1, [45]: [setextUnderline, thematicBreak$1], [60]: htmlFlow, [61]: setextUnderline, [95]: thematicBreak$1, [96]: codeFenced, [126]: codeFenced, }; /** @satisfies {Extension['string']} */ const string = { [38]: characterReference, [92]: characterEscape, }; /** @satisfies {Extension['text']} */ const text$2 = { [-5]: lineEnding, [-4]: lineEnding, [-3]: lineEnding, [33]: labelStartImage, [38]: characterReference, [42]: attention, [60]: [autolink, htmlText], [91]: labelStartLink, [92]: [hardBreakEscape, characterEscape], [93]: labelEnd, [95]: attention, [96]: codeText, }; /** @satisfies {Extension['insideSpan']} */ const insideSpan = { null: [attention, resolver], }; /** @satisfies {Extension['attentionMarkers']} */ const attentionMarkers = { null: [42, 95], }; /** @satisfies {Extension['disable']} */ const disable = { null: [], }; const defaultConstructs = /*#__PURE__*/ Object.freeze( /*#__PURE__*/ Object.defineProperty( { __proto__: null, attentionMarkers, contentInitial, disable, document: document$1, flow, flowInitial, insideSpan, string, text: text$2, }, Symbol.toStringTag, { value: "Module" }, ), ); /** * @import { * Chunk, * Code, * ConstructRecord, * Construct, * Effects, * InitialConstruct, * ParseContext, * Point, * State, * TokenizeContext, * Token * } from 'micromark-util-types' */ /** * Create a tokenizer. * Tokenizers deal with one type of data (e.g., containers, flow, text). * The parser is the object dealing with it all. * `initialize` works like other constructs, except that only its `tokenize` * function is used, in which case it doesn’t receive an `ok` or `nok`. * `from` can be given to set the point before the first character, although * when further lines are indented, they must be set with `defineSkip`. * * @param {ParseContext} parser * Parser. * @param {InitialConstruct} initialize * Construct. * @param {Omit | undefined} [from] * Point (optional). * @returns {TokenizeContext} * Context. */ function createTokenizer(parser, initialize, from) { /** @type {Point} */ let point = { _bufferIndex: -1, _index: 0, line: (from && from.line) || 1, column: (from && from.column) || 1, offset: (from && from.offset) || 0, }; /** @type {Record} */ const columnStart = {}; /** @type {Array} */ const resolveAllConstructs = []; /** @type {Array} */ let chunks = []; /** @type {Array} */ let stack = []; /** * Tools used for tokenizing. * * @type {Effects} */ const effects = { attempt: constructFactory(onsuccessfulconstruct), check: constructFactory(onsuccessfulcheck), consume, enter, exit, interrupt: constructFactory(onsuccessfulcheck, { interrupt: true, }), }; /** * State and tools for resolving and serializing. * * @type {TokenizeContext} */ const context = { code: null, containerState: {}, defineSkip, events: [], now, parser, previous: null, sliceSerialize, sliceStream, write, }; /** * The state function. * * @type {State | undefined} */ let state = initialize.tokenize.call(context, effects); if (initialize.resolveAll) { resolveAllConstructs.push(initialize); } return context; /** @type {TokenizeContext['write']} */ function write(slice) { chunks = push(chunks, slice); main(); // Exit if we’re not done, resolve might change stuff. if (chunks[chunks.length - 1] !== null) { return []; } addResult(initialize, 0); // Otherwise, resolve, and exit. context.events = resolveAll(resolveAllConstructs, context.events, context); return context.events; } // // Tools. // /** @type {TokenizeContext['sliceSerialize']} */ function sliceSerialize(token, expandTabs) { return serializeChunks(sliceStream(token), expandTabs); } /** @type {TokenizeContext['sliceStream']} */ function sliceStream(token) { return sliceChunks(chunks, token); } /** @type {TokenizeContext['now']} */ function now() { // This is a hot path, so we clone manually instead of `Object.assign({}, point)` const { _bufferIndex, _index, line, column, offset } = point; return { _bufferIndex, _index, line, column, offset, }; } /** @type {TokenizeContext['defineSkip']} */ function defineSkip(value) { columnStart[value.line] = value.column; accountForPotentialSkip(); } // // State management. // /** * Main loop (note that `_index` and `_bufferIndex` in `point` are modified by * `consume`). * Here is where we walk through the chunks, which either include strings of * several characters, or numerical character codes. * The reason to do this in a loop instead of a call is so the stack can * drain. * * @returns {undefined} * Nothing. */ function main() { /** @type {number} */ let chunkIndex; while (point._index < chunks.length) { const chunk = chunks[point._index]; // If we’re in a buffer chunk, loop through it. if (typeof chunk === "string") { chunkIndex = point._index; if (point._bufferIndex < 0) { point._bufferIndex = 0; } while (point._index === chunkIndex && point._bufferIndex < chunk.length) { go(chunk.charCodeAt(point._bufferIndex)); } } else { go(chunk); } } } /** * Deal with one code. * * @param {Code} code * Code. * @returns {undefined} * Nothing. */ function go(code) { state = state(code); } /** @type {Effects['consume']} */ function consume(code) { if (markdownLineEnding(code)) { point.line++; point.column = 1; point.offset += code === -3 ? 2 : 1; accountForPotentialSkip(); } else if (code !== -1) { point.column++; point.offset++; } // Not in a string chunk. if (point._bufferIndex < 0) { point._index++; } else { point._bufferIndex++; // At end of string chunk. if ( point._bufferIndex === // Points w/ non-negative `_bufferIndex` reference // strings. /** @type {string} */ chunks[point._index].length ) { point._bufferIndex = -1; point._index++; } } // Expose the previous character. context.previous = code; } /** @type {Effects['enter']} */ function enter(type, fields) { /** @type {Token} */ // @ts-expect-error Patch instead of assign required fields to help GC. const token = fields || {}; token.type = type; token.start = now(); context.events.push(["enter", token, context]); stack.push(token); return token; } /** @type {Effects['exit']} */ function exit(type) { const token = stack.pop(); token.end = now(); context.events.push(["exit", token, context]); return token; } /** * Use results. * * @type {ReturnHandle} */ function onsuccessfulconstruct(construct, info) { addResult(construct, info.from); } /** * Discard results. * * @type {ReturnHandle} */ function onsuccessfulcheck(_, info) { info.restore(); } /** * Factory to attempt/check/interrupt. * * @param {ReturnHandle} onreturn * Callback. * @param {{interrupt?: boolean | undefined} | undefined} [fields] * Fields. */ function constructFactory(onreturn, fields) { return hook; /** * Handle either an object mapping codes to constructs, a list of * constructs, or a single construct. * * @param {Array | ConstructRecord | Construct} constructs * Constructs. * @param {State} returnState * State. * @param {State | undefined} [bogusState] * State. * @returns {State} * State. */ function hook(constructs, returnState, bogusState) { /** @type {ReadonlyArray} */ let listOfConstructs; /** @type {number} */ let constructIndex; /** @type {Construct} */ let currentConstruct; /** @type {Info} */ let info; return ( Array.isArray(constructs) /* c8 ignore next 1 */ ? handleListOfConstructs(constructs) : "tokenize" in constructs ? // Looks like a construct. handleListOfConstructs([/** @type {Construct} */ constructs]) : handleMapOfConstructs(constructs) ); /** * Handle a list of construct. * * @param {ConstructRecord} map * Constructs. * @returns {State} * State. */ function handleMapOfConstructs(map) { return start; /** @type {State} */ function start(code) { const left = code !== null && map[code]; const all = code !== null && map.null; const list = [ // To do: add more extension tests. /* c8 ignore next 2 */ ...(Array.isArray(left) ? left : left ? [left] : []), ...(Array.isArray(all) ? all : all ? [all] : []), ]; return handleListOfConstructs(list)(code); } } /** * Handle a list of construct. * * @param {ReadonlyArray} list * Constructs. * @returns {State} * State. */ function handleListOfConstructs(list) { listOfConstructs = list; constructIndex = 0; if (list.length === 0) { return bogusState; } return handleConstruct(list[constructIndex]); } /** * Handle a single construct. * * @param {Construct} construct * Construct. * @returns {State} * State. */ function handleConstruct(construct) { return start; /** @type {State} */ function start(code) { // To do: not needed to store if there is no bogus state, probably? // Currently doesn’t work because `inspect` in document does a check // w/o a bogus, which doesn’t make sense. But it does seem to help perf // by not storing. info = store(); currentConstruct = construct; if (!construct.partial) { context.currentConstruct = construct; } // Always populated by defaults. if (construct.name && context.parser.constructs.disable.null.includes(construct.name)) { return nok(); } return construct.tokenize.call( // If we do have fields, create an object w/ `context` as its // prototype. // This allows a “live binding”, which is needed for `interrupt`. fields ? Object.assign(Object.create(context), fields) : context, effects, ok, nok, )(code); } } /** @type {State} */ function ok(code) { onreturn(currentConstruct, info); return returnState; } /** @type {State} */ function nok(code) { info.restore(); if (++constructIndex < listOfConstructs.length) { return handleConstruct(listOfConstructs[constructIndex]); } return bogusState; } } } /** * @param {Construct} construct * Construct. * @param {number} from * From. * @returns {undefined} * Nothing. */ function addResult(construct, from) { if (construct.resolveAll && !resolveAllConstructs.includes(construct)) { resolveAllConstructs.push(construct); } if (construct.resolve) { splice(context.events, from, context.events.length - from, construct.resolve(context.events.slice(from), context)); } if (construct.resolveTo) { context.events = construct.resolveTo(context.events, context); } } /** * Store state. * * @returns {Info} * Info. */ function store() { const startPoint = now(); const startPrevious = context.previous; const startCurrentConstruct = context.currentConstruct; const startEventsIndex = context.events.length; const startStack = Array.from(stack); return { from: startEventsIndex, restore, }; /** * Restore state. * * @returns {undefined} * Nothing. */ function restore() { point = startPoint; context.previous = startPrevious; context.currentConstruct = startCurrentConstruct; context.events.length = startEventsIndex; stack = startStack; accountForPotentialSkip(); } } /** * Move the current point a bit forward in the line when it’s on a column * skip. * * @returns {undefined} * Nothing. */ function accountForPotentialSkip() { if (point.line in columnStart && point.column < 2) { point.column = columnStart[point.line]; point.offset += columnStart[point.line] - 1; } } } /** * Get the chunks from a slice of chunks in the range of a token. * * @param {ReadonlyArray} chunks * Chunks. * @param {Pick} token * Token. * @returns {Array} * Chunks. */ function sliceChunks(chunks, token) { const startIndex = token.start._index; const startBufferIndex = token.start._bufferIndex; const endIndex = token.end._index; const endBufferIndex = token.end._bufferIndex; /** @type {Array} */ let view; if (startIndex === endIndex) { // @ts-expect-error `_bufferIndex` is used on string chunks. view = [chunks[startIndex].slice(startBufferIndex, endBufferIndex)]; } else { view = chunks.slice(startIndex, endIndex); if (startBufferIndex > -1) { const head = view[0]; if (typeof head === "string") { view[0] = head.slice(startBufferIndex); /* c8 ignore next 4 -- used to be used, no longer */ } else { view.shift(); } } if (endBufferIndex > 0) { // @ts-expect-error `_bufferIndex` is used on string chunks. view.push(chunks[endIndex].slice(0, endBufferIndex)); } } return view; } /** * Get the string value of a slice of chunks. * * @param {ReadonlyArray} chunks * Chunks. * @param {boolean | undefined} [expandTabs=false] * Whether to expand tabs (default: `false`). * @returns {string} * Result. */ function serializeChunks(chunks, expandTabs) { let index = -1; /** @type {Array} */ const result = []; /** @type {boolean | undefined} */ let atTab; while (++index < chunks.length) { const chunk = chunks[index]; /** @type {string} */ let value; if (typeof chunk === "string") { value = chunk; } else switch (chunk) { case -5: { value = "\r"; break; } case -4: { value = "\n"; break; } case -3: { value = "\r" + "\n"; break; } case -2: { value = expandTabs ? " " : "\t"; break; } case -1: { if (!expandTabs && atTab) continue; value = " "; break; } default: { // Currently only replacement character. value = String.fromCharCode(chunk); } } atTab = chunk === -2; result.push(value); } return result.join(""); } /** * @import { * Create, * FullNormalizedExtension, * InitialConstruct, * ParseContext, * ParseOptions * } from 'micromark-util-types' */ /** * @param {ParseOptions | null | undefined} [options] * Configuration (optional). * @returns {ParseContext} * Parser. */ function parse$1(options) { const settings = options || {}; const constructs = /** @type {FullNormalizedExtension} */ combineExtensions([defaultConstructs, ...(settings.extensions || [])]); /** @type {ParseContext} */ const parser = { constructs, content: create(content), defined: [], document: create(document$2), flow: create(flow$1), lazy: {}, string: create(string$1), text: create(text$3), }; return parser; /** * @param {InitialConstruct} initial * Construct to start with. * @returns {Create} * Create a tokenizer. */ function create(initial) { return creator; /** @type {Create} */ function creator(from) { return createTokenizer(parser, initial, from); } } } /** * @import {Event} from 'micromark-util-types' */ /** * @param {Array} events * Events. * @returns {Array} * Events. */ function postprocess(events) { while (!subtokenize(events)) { // Empty } return events; } /** * @import {Chunk, Code, Encoding, Value} from 'micromark-util-types' */ /** * @callback Preprocessor * Preprocess a value. * @param {Value} value * Value. * @param {Encoding | null | undefined} [encoding] * Encoding when `value` is a typed array (optional). * @param {boolean | null | undefined} [end=false] * Whether this is the last chunk (default: `false`). * @returns {Array} * Chunks. */ const search = /[\0\t\n\r]/g; /** * @returns {Preprocessor} * Preprocess a value. */ function preprocess() { let column = 1; let buffer = ""; /** @type {boolean | undefined} */ let start = true; /** @type {boolean | undefined} */ let atCarriageReturn; return preprocessor; /** @type {Preprocessor} */ // eslint-disable-next-line complexity function preprocessor(value, encoding, end) { /** @type {Array} */ const chunks = []; /** @type {RegExpMatchArray | null} */ let match; /** @type {number} */ let next; /** @type {number} */ let startPosition; /** @type {number} */ let endPosition; /** @type {Code} */ let code; value = buffer + (typeof value === "string" ? value.toString() : new TextDecoder(encoding || undefined).decode(value)); startPosition = 0; buffer = ""; if (start) { // To do: `markdown-rs` actually parses BOMs (byte order mark). if (value.charCodeAt(0) === 65279) { startPosition++; } start = undefined; } while (startPosition < value.length) { search.lastIndex = startPosition; match = search.exec(value); endPosition = match && match.index !== undefined ? match.index : value.length; code = value.charCodeAt(endPosition); if (!match) { buffer = value.slice(startPosition); break; } if (code === 10 && startPosition === endPosition && atCarriageReturn) { chunks.push(-3); atCarriageReturn = undefined; } else { if (atCarriageReturn) { chunks.push(-5); atCarriageReturn = undefined; } if (startPosition < endPosition) { chunks.push(value.slice(startPosition, endPosition)); column += endPosition - startPosition; } switch (code) { case 0: { chunks.push(65533); column++; break; } case 9: { next = Math.ceil(column / 4) * 4; chunks.push(-2); while (column++ < next) chunks.push(-1); break; } case 10: { chunks.push(-4); column = 1; break; } default: { atCarriageReturn = true; column = 1; } } } startPosition = endPosition + 1; } if (end) { if (atCarriageReturn) chunks.push(-5); if (buffer) chunks.push(buffer); chunks.push(null); } return chunks; } } /** * @import { * Break, * Blockquote, * Code, * Definition, * Emphasis, * Heading, * Html, * Image, * InlineCode, * Link, * ListItem, * List, * Nodes, * Paragraph, * PhrasingContent, * ReferenceType, * Root, * Strong, * Text, * ThematicBreak * } from 'mdast' * @import { * Encoding, * Event, * Token, * Value * } from 'micromark-util-types' * @import {Point} from 'unist' * @import { * CompileContext, * CompileData, * Config, * Extension, * Handle, * OnEnterError, * Options * } from './types.js' */ const own$3 = {}.hasOwnProperty; /** * Turn markdown into a syntax tree. * * @overload * @param {Value} value * @param {Encoding | null | undefined} [encoding] * @param {Options | null | undefined} [options] * @returns {Root} * * @overload * @param {Value} value * @param {Options | null | undefined} [options] * @returns {Root} * * @param {Value} value * Markdown to parse. * @param {Encoding | Options | null | undefined} [encoding] * Character encoding for when `value` is `Buffer`. * @param {Options | null | undefined} [options] * Configuration. * @returns {Root} * mdast tree. */ function fromMarkdown(value, encoding, options) { if (encoding && typeof encoding === "object") { options = encoding; encoding = undefined; } return compiler(options)( postprocess( parse$1(options) .document() .write(preprocess()(value, encoding, true)), ), ); } /** * Note this compiler only understand complete buffering, not streaming. * * @param {Options | null | undefined} [options] */ function compiler(options) { /** @type {Config} */ const config = { transforms: [], canContainEols: ["emphasis", "fragment", "heading", "paragraph", "strong"], enter: { autolink: opener(link), autolinkProtocol: onenterdata, autolinkEmail: onenterdata, atxHeading: opener(heading), blockQuote: opener(blockQuote), characterEscape: onenterdata, characterReference: onenterdata, codeFenced: opener(codeFlow), codeFencedFenceInfo: buffer, codeFencedFenceMeta: buffer, codeIndented: opener(codeFlow, buffer), codeText: opener(codeText, buffer), codeTextData: onenterdata, data: onenterdata, codeFlowValue: onenterdata, definition: opener(definition), definitionDestinationString: buffer, definitionLabelString: buffer, definitionTitleString: buffer, emphasis: opener(emphasis), hardBreakEscape: opener(hardBreak), hardBreakTrailing: opener(hardBreak), htmlFlow: opener(html, buffer), htmlFlowData: onenterdata, htmlText: opener(html, buffer), htmlTextData: onenterdata, image: opener(image), label: buffer, link: opener(link), listItem: opener(listItem), listItemValue: onenterlistitemvalue, listOrdered: opener(list, onenterlistordered), listUnordered: opener(list), paragraph: opener(paragraph), reference: onenterreference, referenceString: buffer, resourceDestinationString: buffer, resourceTitleString: buffer, setextHeading: opener(heading), strong: opener(strong), thematicBreak: opener(thematicBreak), }, exit: { atxHeading: closer(), atxHeadingSequence: onexitatxheadingsequence, autolink: closer(), autolinkEmail: onexitautolinkemail, autolinkProtocol: onexitautolinkprotocol, blockQuote: closer(), characterEscapeValue: onexitdata, characterReferenceMarkerHexadecimal: onexitcharacterreferencemarker, characterReferenceMarkerNumeric: onexitcharacterreferencemarker, characterReferenceValue: onexitcharacterreferencevalue, characterReference: onexitcharacterreference, codeFenced: closer(onexitcodefenced), codeFencedFence: onexitcodefencedfence, codeFencedFenceInfo: onexitcodefencedfenceinfo, codeFencedFenceMeta: onexitcodefencedfencemeta, codeFlowValue: onexitdata, codeIndented: closer(onexitcodeindented), codeText: closer(onexitcodetext), codeTextData: onexitdata, data: onexitdata, definition: closer(), definitionDestinationString: onexitdefinitiondestinationstring, definitionLabelString: onexitdefinitionlabelstring, definitionTitleString: onexitdefinitiontitlestring, emphasis: closer(), hardBreakEscape: closer(onexithardbreak), hardBreakTrailing: closer(onexithardbreak), htmlFlow: closer(onexithtmlflow), htmlFlowData: onexitdata, htmlText: closer(onexithtmltext), htmlTextData: onexitdata, image: closer(onexitimage), label: onexitlabel, labelText: onexitlabeltext, lineEnding: onexitlineending, link: closer(onexitlink), listItem: closer(), listOrdered: closer(), listUnordered: closer(), paragraph: closer(), referenceString: onexitreferencestring, resourceDestinationString: onexitresourcedestinationstring, resourceTitleString: onexitresourcetitlestring, resource: onexitresource, setextHeading: closer(onexitsetextheading), setextHeadingLineSequence: onexitsetextheadinglinesequence, setextHeadingText: onexitsetextheadingtext, strong: closer(), thematicBreak: closer(), }, }; configure(config, (options || {}).mdastExtensions || []); /** @type {CompileData} */ const data = {}; return compile; /** * Turn micromark events into an mdast tree. * * @param {Array} events * Events. * @returns {Root} * mdast tree. */ function compile(events) { /** @type {Root} */ let tree = { type: "root", children: [], }; /** @type {Omit} */ const context = { stack: [tree], tokenStack: [], config, enter, exit, buffer, resume, data, }; /** @type {Array} */ const listStack = []; let index = -1; while (++index < events.length) { // We preprocess lists to add `listItem` tokens, and to infer whether // items the list itself are spread out. if (events[index][1].type === "listOrdered" || events[index][1].type === "listUnordered") { if (events[index][0] === "enter") { listStack.push(index); } else { const tail = listStack.pop(); index = prepareList(events, tail, index); } } } index = -1; while (++index < events.length) { const handler = config[events[index][0]]; if (own$3.call(handler, events[index][1].type)) { handler[events[index][1].type].call( Object.assign( { sliceSerialize: events[index][2].sliceSerialize, }, context, ), events[index][1], ); } } // Handle tokens still being open. if (context.tokenStack.length > 0) { const tail = context.tokenStack[context.tokenStack.length - 1]; const handler = tail[1] || defaultOnError; handler.call(context, undefined, tail[0]); } // Figure out `root` position. tree.position = { start: point( events.length > 0 ? events[0][1].start : { line: 1, column: 1, offset: 0, }, ), end: point( events.length > 0 ? events[events.length - 2][1].end : { line: 1, column: 1, offset: 0, }, ), }; // Call transforms. index = -1; while (++index < config.transforms.length) { tree = config.transforms[index](tree) || tree; } return tree; } /** * @param {Array} events * @param {number} start * @param {number} length * @returns {number} */ function prepareList(events, start, length) { let index = start - 1; let containerBalance = -1; let listSpread = false; /** @type {Token | undefined} */ let listItem; /** @type {number | undefined} */ let lineIndex; /** @type {number | undefined} */ let firstBlankLineIndex; /** @type {boolean | undefined} */ let atMarker; while (++index <= length) { const event = events[index]; switch (event[1].type) { case "listUnordered": case "listOrdered": case "blockQuote": { if (event[0] === "enter") { containerBalance++; } else { containerBalance--; } atMarker = undefined; break; } case "lineEndingBlank": { if (event[0] === "enter") { if (listItem && !atMarker && !containerBalance && !firstBlankLineIndex) { firstBlankLineIndex = index; } atMarker = undefined; } break; } case "linePrefix": case "listItemValue": case "listItemMarker": case "listItemPrefix": case "listItemPrefixWhitespace": { // Empty. break; } default: { atMarker = undefined; } } if ( (!containerBalance && event[0] === "enter" && event[1].type === "listItemPrefix") || (containerBalance === -1 && event[0] === "exit" && (event[1].type === "listUnordered" || event[1].type === "listOrdered")) ) { if (listItem) { let tailIndex = index; lineIndex = undefined; while (tailIndex--) { const tailEvent = events[tailIndex]; if (tailEvent[1].type === "lineEnding" || tailEvent[1].type === "lineEndingBlank") { if (tailEvent[0] === "exit") continue; if (lineIndex) { events[lineIndex][1].type = "lineEndingBlank"; listSpread = true; } tailEvent[1].type = "lineEnding"; lineIndex = tailIndex; } else if ( tailEvent[1].type === "linePrefix" || tailEvent[1].type === "blockQuotePrefix" || tailEvent[1].type === "blockQuotePrefixWhitespace" || tailEvent[1].type === "blockQuoteMarker" || tailEvent[1].type === "listItemIndent" ); else { break; } } if (firstBlankLineIndex && (!lineIndex || firstBlankLineIndex < lineIndex)) { listItem._spread = true; } // Fix position. listItem.end = Object.assign({}, lineIndex ? events[lineIndex][1].start : event[1].end); events.splice(lineIndex || index, 0, ["exit", listItem, event[2]]); index++; length++; } // Create a new list item. if (event[1].type === "listItemPrefix") { /** @type {Token} */ const item = { type: "listItem", _spread: false, start: Object.assign({}, event[1].start), // @ts-expect-error: we’ll add `end` in a second. end: undefined, }; listItem = item; events.splice(index, 0, ["enter", item, event[2]]); index++; length++; firstBlankLineIndex = undefined; atMarker = true; } } } events[start][1]._spread = listSpread; return length; } /** * Create an opener handle. * * @param {(token: Token) => Nodes} create * Create a node. * @param {Handle | undefined} [and] * Optional function to also run. * @returns {Handle} * Handle. */ function opener(create, and) { return open; /** * @this {CompileContext} * @param {Token} token * @returns {undefined} */ function open(token) { enter.call(this, create(token), token); if (and) and.call(this, token); } } /** * @type {CompileContext['buffer']} */ function buffer() { this.stack.push({ type: "fragment", children: [], }); } /** * @type {CompileContext['enter']} */ function enter(node, token, errorHandler) { const parent = this.stack[this.stack.length - 1]; /** @type {Array} */ const siblings = parent.children; siblings.push(node); this.stack.push(node); this.tokenStack.push([token, errorHandler || undefined]); node.position = { start: point(token.start), // @ts-expect-error: `end` will be patched later. end: undefined, }; } /** * Create a closer handle. * * @param {Handle | undefined} [and] * Optional function to also run. * @returns {Handle} * Handle. */ function closer(and) { return close; /** * @this {CompileContext} * @param {Token} token * @returns {undefined} */ function close(token) { if (and) and.call(this, token); exit.call(this, token); } } /** * @type {CompileContext['exit']} */ function exit(token, onExitError) { const node = this.stack.pop(); const open = this.tokenStack.pop(); if (!open) { throw new Error( "Cannot close `" + token.type + "` (" + stringifyPosition({ start: token.start, end: token.end, }) + "): it’s not open", ); } else if (open[0].type !== token.type) { if (onExitError) { onExitError.call(this, token, open[0]); } else { const handler = open[1] || defaultOnError; handler.call(this, token, open[0]); } } node.position.end = point(token.end); } /** * @type {CompileContext['resume']} */ function resume() { return toString(this.stack.pop()); } // // Handlers. // /** * @this {CompileContext} * @type {Handle} */ function onenterlistordered() { this.data.expectingFirstListItemValue = true; } /** * @this {CompileContext} * @type {Handle} */ function onenterlistitemvalue(token) { if (this.data.expectingFirstListItemValue) { const ancestor = this.stack[this.stack.length - 2]; ancestor.start = Number.parseInt(this.sliceSerialize(token), 10); this.data.expectingFirstListItemValue = undefined; } } /** * @this {CompileContext} * @type {Handle} */ function onexitcodefencedfenceinfo() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.lang = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitcodefencedfencemeta() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.meta = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitcodefencedfence() { // Exit if this is the closing fence. if (this.data.flowCodeInside) return; this.buffer(); this.data.flowCodeInside = true; } /** * @this {CompileContext} * @type {Handle} */ function onexitcodefenced() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.value = data.replace(/^(\r?\n|\r)|(\r?\n|\r)$/g, ""); this.data.flowCodeInside = undefined; } /** * @this {CompileContext} * @type {Handle} */ function onexitcodeindented() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.value = data.replace(/(\r?\n|\r)$/g, ""); } /** * @this {CompileContext} * @type {Handle} */ function onexitdefinitionlabelstring(token) { const label = this.resume(); const node = this.stack[this.stack.length - 1]; node.label = label; node.identifier = normalizeIdentifier(this.sliceSerialize(token)).toLowerCase(); } /** * @this {CompileContext} * @type {Handle} */ function onexitdefinitiontitlestring() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.title = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitdefinitiondestinationstring() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.url = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitatxheadingsequence(token) { const node = this.stack[this.stack.length - 1]; if (!node.depth) { const depth = this.sliceSerialize(token).length; node.depth = depth; } } /** * @this {CompileContext} * @type {Handle} */ function onexitsetextheadingtext() { this.data.setextHeadingSlurpLineEnding = true; } /** * @this {CompileContext} * @type {Handle} */ function onexitsetextheadinglinesequence(token) { const node = this.stack[this.stack.length - 1]; node.depth = this.sliceSerialize(token).codePointAt(0) === 61 ? 1 : 2; } /** * @this {CompileContext} * @type {Handle} */ function onexitsetextheading() { this.data.setextHeadingSlurpLineEnding = undefined; } /** * @this {CompileContext} * @type {Handle} */ function onenterdata(token) { const node = this.stack[this.stack.length - 1]; /** @type {Array} */ const siblings = node.children; let tail = siblings[siblings.length - 1]; if (!tail || tail.type !== "text") { // Add a new text node. tail = text(); tail.position = { start: point(token.start), // @ts-expect-error: we’ll add `end` later. end: undefined, }; siblings.push(tail); } this.stack.push(tail); } /** * @this {CompileContext} * @type {Handle} */ function onexitdata(token) { const tail = this.stack.pop(); tail.value += this.sliceSerialize(token); tail.position.end = point(token.end); } /** * @this {CompileContext} * @type {Handle} */ function onexitlineending(token) { const context = this.stack[this.stack.length - 1]; // If we’re at a hard break, include the line ending in there. if (this.data.atHardBreak) { const tail = context.children[context.children.length - 1]; tail.position.end = point(token.end); this.data.atHardBreak = undefined; return; } if (!this.data.setextHeadingSlurpLineEnding && config.canContainEols.includes(context.type)) { onenterdata.call(this, token); onexitdata.call(this, token); } } /** * @this {CompileContext} * @type {Handle} */ function onexithardbreak() { this.data.atHardBreak = true; } /** * @this {CompileContext} * @type {Handle} */ function onexithtmlflow() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.value = data; } /** * @this {CompileContext} * @type {Handle} */ function onexithtmltext() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.value = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitcodetext() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.value = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitlink() { const node = this.stack[this.stack.length - 1]; // Note: there are also `identifier` and `label` fields on this link node! // These are used / cleaned here. // To do: clean. if (this.data.inReference) { /** @type {ReferenceType} */ const referenceType = this.data.referenceType || "shortcut"; node.type += "Reference"; // @ts-expect-error: mutate. node.referenceType = referenceType; // @ts-expect-error: mutate. delete node.url; delete node.title; } else { // @ts-expect-error: mutate. delete node.identifier; // @ts-expect-error: mutate. delete node.label; } this.data.referenceType = undefined; } /** * @this {CompileContext} * @type {Handle} */ function onexitimage() { const node = this.stack[this.stack.length - 1]; // Note: there are also `identifier` and `label` fields on this link node! // These are used / cleaned here. // To do: clean. if (this.data.inReference) { /** @type {ReferenceType} */ const referenceType = this.data.referenceType || "shortcut"; node.type += "Reference"; // @ts-expect-error: mutate. node.referenceType = referenceType; // @ts-expect-error: mutate. delete node.url; delete node.title; } else { // @ts-expect-error: mutate. delete node.identifier; // @ts-expect-error: mutate. delete node.label; } this.data.referenceType = undefined; } /** * @this {CompileContext} * @type {Handle} */ function onexitlabeltext(token) { const string = this.sliceSerialize(token); const ancestor = this.stack[this.stack.length - 2]; // @ts-expect-error: stash this on the node, as it might become a reference // later. ancestor.label = decodeString(string); // @ts-expect-error: same as above. ancestor.identifier = normalizeIdentifier(string).toLowerCase(); } /** * @this {CompileContext} * @type {Handle} */ function onexitlabel() { const fragment = this.stack[this.stack.length - 1]; const value = this.resume(); const node = this.stack[this.stack.length - 1]; // Assume a reference. this.data.inReference = true; if (node.type === "link") { /** @type {Array} */ const children = fragment.children; node.children = children; } else { node.alt = value; } } /** * @this {CompileContext} * @type {Handle} */ function onexitresourcedestinationstring() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.url = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitresourcetitlestring() { const data = this.resume(); const node = this.stack[this.stack.length - 1]; node.title = data; } /** * @this {CompileContext} * @type {Handle} */ function onexitresource() { this.data.inReference = undefined; } /** * @this {CompileContext} * @type {Handle} */ function onenterreference() { this.data.referenceType = "collapsed"; } /** * @this {CompileContext} * @type {Handle} */ function onexitreferencestring(token) { const label = this.resume(); const node = this.stack[this.stack.length - 1]; // @ts-expect-error: stash this on the node, as it might become a reference // later. node.label = label; // @ts-expect-error: same as above. node.identifier = normalizeIdentifier(this.sliceSerialize(token)).toLowerCase(); this.data.referenceType = "full"; } /** * @this {CompileContext} * @type {Handle} */ function onexitcharacterreferencemarker(token) { this.data.characterReferenceType = token.type; } /** * @this {CompileContext} * @type {Handle} */ function onexitcharacterreferencevalue(token) { const data = this.sliceSerialize(token); const type = this.data.characterReferenceType; /** @type {string} */ let value; if (type) { value = decodeNumericCharacterReference(data, type === "characterReferenceMarkerNumeric" ? 10 : 16); this.data.characterReferenceType = undefined; } else { const result = decodeNamedCharacterReference(data); value = result; } const tail = this.stack[this.stack.length - 1]; tail.value += value; } /** * @this {CompileContext} * @type {Handle} */ function onexitcharacterreference(token) { const tail = this.stack.pop(); tail.position.end = point(token.end); } /** * @this {CompileContext} * @type {Handle} */ function onexitautolinkprotocol(token) { onexitdata.call(this, token); const node = this.stack[this.stack.length - 1]; node.url = this.sliceSerialize(token); } /** * @this {CompileContext} * @type {Handle} */ function onexitautolinkemail(token) { onexitdata.call(this, token); const node = this.stack[this.stack.length - 1]; node.url = "mailto:" + this.sliceSerialize(token); } // // Creaters. // /** @returns {Blockquote} */ function blockQuote() { return { type: "blockquote", children: [], }; } /** @returns {Code} */ function codeFlow() { return { type: "code", lang: null, meta: null, value: "", }; } /** @returns {InlineCode} */ function codeText() { return { type: "inlineCode", value: "", }; } /** @returns {Definition} */ function definition() { return { type: "definition", identifier: "", label: null, title: null, url: "", }; } /** @returns {Emphasis} */ function emphasis() { return { type: "emphasis", children: [], }; } /** @returns {Heading} */ function heading() { return { type: "heading", // @ts-expect-error `depth` will be set later. depth: 0, children: [], }; } /** @returns {Break} */ function hardBreak() { return { type: "break", }; } /** @returns {Html} */ function html() { return { type: "html", value: "", }; } /** @returns {Image} */ function image() { return { type: "image", title: null, url: "", alt: null, }; } /** @returns {Link} */ function link() { return { type: "link", title: null, url: "", children: [], }; } /** * @param {Token} token * @returns {List} */ function list(token) { return { type: "list", ordered: token.type === "listOrdered", start: null, spread: token._spread, children: [], }; } /** * @param {Token} token * @returns {ListItem} */ function listItem(token) { return { type: "listItem", spread: token._spread, checked: null, children: [], }; } /** @returns {Paragraph} */ function paragraph() { return { type: "paragraph", children: [], }; } /** @returns {Strong} */ function strong() { return { type: "strong", children: [], }; } /** @returns {Text} */ function text() { return { type: "text", value: "", }; } /** @returns {ThematicBreak} */ function thematicBreak() { return { type: "thematicBreak", }; } } /** * Copy a point-like value. * * @param {Point} d * Point-like value. * @returns {Point} * unist point. */ function point(d) { return { line: d.line, column: d.column, offset: d.offset, }; } /** * @param {Config} combined * @param {Array | Extension>} extensions * @returns {undefined} */ function configure(combined, extensions) { let index = -1; while (++index < extensions.length) { const value = extensions[index]; if (Array.isArray(value)) { configure(combined, value); } else { extension(combined, value); } } } /** * @param {Config} combined * @param {Extension} extension * @returns {undefined} */ function extension(combined, extension) { /** @type {keyof Extension} */ let key; for (key in extension) { if (own$3.call(extension, key)) { switch (key) { case "canContainEols": { const right = extension[key]; if (right) { combined[key].push(...right); } break; } case "transforms": { const right = extension[key]; if (right) { combined[key].push(...right); } break; } case "enter": case "exit": { const right = extension[key]; if (right) { Object.assign(combined[key], right); } break; } // No default } } } } /** @type {OnEnterError} */ function defaultOnError(left, right) { if (left) { throw new Error( "Cannot close `" + left.type + "` (" + stringifyPosition({ start: left.start, end: left.end, }) + "): a different token (`" + right.type + "`, " + stringifyPosition({ start: right.start, end: right.end, }) + ") is open", ); } else { throw new Error( "Cannot close document, a token (`" + right.type + "`, " + stringifyPosition({ start: right.start, end: right.end, }) + ") is still open", ); } } /** * @typedef {import('mdast').Root} Root * @typedef {import('mdast-util-from-markdown').Options} FromMarkdownOptions * @typedef {import('unified').Parser} Parser * @typedef {import('unified').Processor} Processor */ /** * Aadd support for parsing from markdown. * * @param {Readonly | null | undefined} [options] * Configuration (optional). * @returns {undefined} * Nothing. */ function remarkParse(options) { /** @type {Processor} */ // @ts-expect-error: TS in JSDoc generates wrong types if `this` is typed regularly. const self = this; self.parser = parser; /** * @type {Parser} */ function parser(doc) { return fromMarkdown(doc, { ...self.data("settings"), ...options, // Note: these options are not in the readme. // The goal is for them to be set by plugins on `data` instead of being // passed by users. extensions: self.data("micromarkExtensions") || [], mdastExtensions: self.data("fromMarkdownExtensions") || [], }); } } /** * @import {Element} from 'hast' * @import {Blockquote} from 'mdast' * @import {State} from '../state.js' */ /** * Turn an mdast `blockquote` node into hast. * * @param {State} state * Info passed around. * @param {Blockquote} node * mdast node. * @returns {Element} * hast node. */ function blockquote(state, node) { /** @type {Element} */ const result = { type: "element", tagName: "blockquote", properties: {}, children: state.wrap(state.all(node), true), }; state.patch(node, result); return state.applyData(node, result); } /** * @import {Element, Text} from 'hast' * @import {Break} from 'mdast' * @import {State} from '../state.js' */ /** * Turn an mdast `break` node into hast. * * @param {State} state * Info passed around. * @param {Break} node * mdast node. * @returns {Array} * hast element content. */ function hardBreak(state, node) { /** @type {Element} */ const result = { type: "element", tagName: "br", properties: {}, children: [] }; state.patch(node, result); return [state.applyData(node, result), { type: "text", value: "\n" }]; } /** * @import {Element, Properties} from 'hast' * @import {Code} from 'mdast' * @import {State} from '../state.js' */ /** * Turn an mdast `code` node into hast. * * @param {State} state * Info passed around. * @param {Code} node * mdast node. * @returns {Element} * hast node. */ function code(state, node) { const value = node.value ? node.value + "\n" : ""; /** @type {Properties} */ const properties = {}; // Someone can write `js python ruby`. const language = node.lang ? node.lang.split(/\s+/) : []; // GH/CM still drop the non-first languages. if (language.length > 0) { properties.className = ["language-" + language[0]]; } // Create ``. /** @type {Element} */ let result = { type: "element", tagName: "code", properties, children: [{ type: "text", value }], }; if (node.meta) { result.data = { meta: node.meta }; } state.patch(node, result); result = state.applyData(node, result); // Create `
`.
	result = { type: "element", tagName: "pre", properties: {}, children: [result] };
	state.patch(node, result);
	return result;
}

/**
 * @import {Element} from 'hast'
 * @import {Delete} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `delete` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Delete} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function strikethrough(state, node) {
	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "del",
		properties: {},
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element} from 'hast'
 * @import {Emphasis} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `emphasis` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Emphasis} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function emphasis(state, node) {
	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "em",
		properties: {},
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element} from 'hast'
 * @import {FootnoteReference} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `footnoteReference` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {FootnoteReference} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function footnoteReference(state, node) {
	const clobberPrefix = typeof state.options.clobberPrefix === "string" ? state.options.clobberPrefix : "user-content-";
	const id = String(node.identifier).toUpperCase();
	const safeId = normalizeUri(id.toLowerCase());
	const index = state.footnoteOrder.indexOf(id);
	/** @type {number} */
	let counter;

	let reuseCounter = state.footnoteCounts.get(id);

	if (reuseCounter === undefined) {
		reuseCounter = 0;
		state.footnoteOrder.push(id);
		counter = state.footnoteOrder.length;
	} else {
		counter = index + 1;
	}

	reuseCounter += 1;
	state.footnoteCounts.set(id, reuseCounter);

	/** @type {Element} */
	const link = {
		type: "element",
		tagName: "a",
		properties: {
			href: "#" + clobberPrefix + "fn-" + safeId,
			id: clobberPrefix + "fnref-" + safeId + (reuseCounter > 1 ? "-" + reuseCounter : ""),
			dataFootnoteRef: true,
			ariaDescribedBy: ["footnote-label"],
		},
		children: [{ type: "text", value: String(counter) }],
	};
	state.patch(node, link);

	/** @type {Element} */
	const sup = {
		type: "element",
		tagName: "sup",
		properties: {},
		children: [link],
	};
	state.patch(node, sup);
	return state.applyData(node, sup);
}

/**
 * @import {Element} from 'hast'
 * @import {Heading} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `heading` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Heading} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function heading(state, node) {
	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "h" + node.depth,
		properties: {},
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element} from 'hast'
 * @import {Html} from 'mdast'
 * @import {State} from '../state.js'
 * @import {Raw} from '../../index.js'
 */

/**
 * Turn an mdast `html` node into hast (`raw` node in dangerous mode, otherwise
 * nothing).
 *
 * @param {State} state
 *   Info passed around.
 * @param {Html} node
 *   mdast node.
 * @returns {Element | Raw | undefined}
 *   hast node.
 */
function html$2(state, node) {
	if (state.options.allowDangerousHtml) {
		/** @type {Raw} */
		const result = { type: "raw", value: node.value };
		state.patch(node, result);
		return state.applyData(node, result);
	}

	return undefined;
}

/**
 * @import {ElementContent} from 'hast'
 * @import {Reference, Nodes} from 'mdast'
 * @import {State} from './state.js'
 */

/**
 * Return the content of a reference without definition as plain text.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Extract} node
 *   Reference node (image, link).
 * @returns {Array}
 *   hast content.
 */
function revert(state, node) {
	const subtype = node.referenceType;
	let suffix = "]";

	if (subtype === "collapsed") {
		suffix += "[]";
	} else if (subtype === "full") {
		suffix += "[" + (node.label || node.identifier) + "]";
	}

	if (node.type === "imageReference") {
		return [{ type: "text", value: "![" + node.alt + suffix }];
	}

	const contents = state.all(node);
	const head = contents[0];

	if (head && head.type === "text") {
		head.value = "[" + head.value;
	} else {
		contents.unshift({ type: "text", value: "[" });
	}

	const tail = contents[contents.length - 1];

	if (tail && tail.type === "text") {
		tail.value += suffix;
	} else {
		contents.push({ type: "text", value: suffix });
	}

	return contents;
}

/**
 * @import {ElementContent, Element, Properties} from 'hast'
 * @import {ImageReference} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `imageReference` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {ImageReference} node
 *   mdast node.
 * @returns {Array | ElementContent}
 *   hast node.
 */
function imageReference(state, node) {
	const id = String(node.identifier).toUpperCase();
	const definition = state.definitionById.get(id);

	if (!definition) {
		return revert(state, node);
	}

	/** @type {Properties} */
	const properties = { src: normalizeUri(definition.url || ""), alt: node.alt };

	if (definition.title !== null && definition.title !== undefined) {
		properties.title = definition.title;
	}

	/** @type {Element} */
	const result = { type: "element", tagName: "img", properties, children: [] };
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element, Properties} from 'hast'
 * @import {Image} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `image` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Image} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function image(state, node) {
	/** @type {Properties} */
	const properties = { src: normalizeUri(node.url) };

	if (node.alt !== null && node.alt !== undefined) {
		properties.alt = node.alt;
	}

	if (node.title !== null && node.title !== undefined) {
		properties.title = node.title;
	}

	/** @type {Element} */
	const result = { type: "element", tagName: "img", properties, children: [] };
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element, Text} from 'hast'
 * @import {InlineCode} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `inlineCode` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {InlineCode} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function inlineCode(state, node) {
	/** @type {Text} */
	const text = { type: "text", value: node.value.replace(/\r?\n|\r/g, " ") };
	state.patch(node, text);

	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "code",
		properties: {},
		children: [text],
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {ElementContent, Element, Properties} from 'hast'
 * @import {LinkReference} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `linkReference` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {LinkReference} node
 *   mdast node.
 * @returns {Array | ElementContent}
 *   hast node.
 */
function linkReference(state, node) {
	const id = String(node.identifier).toUpperCase();
	const definition = state.definitionById.get(id);

	if (!definition) {
		return revert(state, node);
	}

	/** @type {Properties} */
	const properties = { href: normalizeUri(definition.url || "") };

	if (definition.title !== null && definition.title !== undefined) {
		properties.title = definition.title;
	}

	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "a",
		properties,
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element, Properties} from 'hast'
 * @import {Link} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `link` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Link} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function link(state, node) {
	/** @type {Properties} */
	const properties = { href: normalizeUri(node.url) };

	if (node.title !== null && node.title !== undefined) {
		properties.title = node.title;
	}

	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "a",
		properties,
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {ElementContent, Element, Properties} from 'hast'
 * @import {ListItem, Parents} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `listItem` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {ListItem} node
 *   mdast node.
 * @param {Parents | undefined} parent
 *   Parent of `node`.
 * @returns {Element}
 *   hast node.
 */
function listItem(state, node, parent) {
	const results = state.all(node);
	const loose = parent ? listLoose(parent) : listItemLoose(node);
	/** @type {Properties} */
	const properties = {};
	/** @type {Array} */
	const children = [];

	if (typeof node.checked === "boolean") {
		const head = results[0];
		/** @type {Element} */
		let paragraph;

		if (head && head.type === "element" && head.tagName === "p") {
			paragraph = head;
		} else {
			paragraph = { type: "element", tagName: "p", properties: {}, children: [] };
			results.unshift(paragraph);
		}

		if (paragraph.children.length > 0) {
			paragraph.children.unshift({ type: "text", value: " " });
		}

		paragraph.children.unshift({
			type: "element",
			tagName: "input",
			properties: { type: "checkbox", checked: node.checked, disabled: true },
			children: [],
		});

		// According to github-markdown-css, this class hides bullet.
		// See: .
		properties.className = ["task-list-item"];
	}

	let index = -1;

	while (++index < results.length) {
		const child = results[index];

		// Add eols before nodes, except if this is a loose, first paragraph.
		if (loose || index !== 0 || child.type !== "element" || child.tagName !== "p") {
			children.push({ type: "text", value: "\n" });
		}

		if (child.type === "element" && child.tagName === "p" && !loose) {
			children.push(...child.children);
		} else {
			children.push(child);
		}
	}

	const tail = results[results.length - 1];

	// Add a final eol.
	if (tail && (loose || tail.type !== "element" || tail.tagName !== "p")) {
		children.push({ type: "text", value: "\n" });
	}

	/** @type {Element} */
	const result = { type: "element", tagName: "li", properties, children };
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @param {Parents} node
 * @return {Boolean}
 */
function listLoose(node) {
	let loose = false;
	if (node.type === "list") {
		loose = node.spread || false;
		const children = node.children;
		let index = -1;

		while (!loose && ++index < children.length) {
			loose = listItemLoose(children[index]);
		}
	}

	return loose;
}

/**
 * @param {ListItem} node
 * @return {Boolean}
 */
function listItemLoose(node) {
	const spread = node.spread;

	return spread === null || spread === undefined ? node.children.length > 1 : spread;
}

/**
 * @import {Element, Properties} from 'hast'
 * @import {List} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `list` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {List} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function list(state, node) {
	/** @type {Properties} */
	const properties = {};
	const results = state.all(node);
	let index = -1;

	if (typeof node.start === "number" && node.start !== 1) {
		properties.start = node.start;
	}

	// Like GitHub, add a class for custom styling.
	while (++index < results.length) {
		const child = results[index];

		if (child.type === "element" && child.tagName === "li" && child.properties && Array.isArray(child.properties.className) && child.properties.className.includes("task-list-item")) {
			properties.className = ["contains-task-list"];
			break;
		}
	}

	/** @type {Element} */
	const result = {
		type: "element",
		tagName: node.ordered ? "ol" : "ul",
		properties,
		children: state.wrap(results, true),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element} from 'hast'
 * @import {Paragraph} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `paragraph` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Paragraph} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function paragraph(state, node) {
	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "p",
		properties: {},
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Parents as HastParents, Root as HastRoot} from 'hast'
 * @import {Root as MdastRoot} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `root` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {MdastRoot} node
 *   mdast node.
 * @returns {HastParents}
 *   hast node.
 */
function root$1(state, node) {
	/** @type {HastRoot} */
	const result = { type: "root", children: state.wrap(state.all(node)) };
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element} from 'hast'
 * @import {Strong} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `strong` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Strong} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function strong(state, node) {
	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "strong",
		properties: {},
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Table} from 'mdast'
 * @import {Element} from 'hast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `table` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {Table} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function table(state, node) {
	const rows = state.all(node);
	const firstRow = rows.shift();
	/** @type {Array} */
	const tableContent = [];

	if (firstRow) {
		/** @type {Element} */
		const head = {
			type: "element",
			tagName: "thead",
			properties: {},
			children: state.wrap([firstRow], true),
		};
		state.patch(node.children[0], head);
		tableContent.push(head);
	}

	if (rows.length > 0) {
		/** @type {Element} */
		const body = {
			type: "element",
			tagName: "tbody",
			properties: {},
			children: state.wrap(rows, true),
		};

		const start = pointStart(node.children[1]);
		const end = pointEnd(node.children[node.children.length - 1]);
		if (start && end) body.position = { start, end };
		tableContent.push(body);
	}

	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "table",
		properties: {},
		children: state.wrap(tableContent, true),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element, ElementContent, Properties} from 'hast'
 * @import {Parents, TableRow} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `tableRow` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {TableRow} node
 *   mdast node.
 * @param {Parents | undefined} parent
 *   Parent of `node`.
 * @returns {Element}
 *   hast node.
 */
function tableRow(state, node, parent) {
	const siblings = parent ? parent.children : undefined;
	// Generate a body row when without parent.
	const rowIndex = siblings ? siblings.indexOf(node) : 1;
	const tagName = rowIndex === 0 ? "th" : "td";
	// To do: option to use `style`?
	const align = parent && parent.type === "table" ? parent.align : undefined;
	const length = align ? align.length : node.children.length;
	let cellIndex = -1;
	/** @type {Array} */
	const cells = [];

	while (++cellIndex < length) {
		// Note: can also be undefined.
		const cell = node.children[cellIndex];
		/** @type {Properties} */
		const properties = {};
		const alignValue = align ? align[cellIndex] : undefined;

		if (alignValue) {
			properties.align = alignValue;
		}

		/** @type {Element} */
		let result = { type: "element", tagName, properties, children: [] };

		if (cell) {
			result.children = state.all(cell);
			state.patch(cell, result);
			result = state.applyData(cell, result);
		}

		cells.push(result);
	}

	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "tr",
		properties: {},
		children: state.wrap(cells, true),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element} from 'hast'
 * @import {TableCell} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `tableCell` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {TableCell} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function tableCell(state, node) {
	// Note: this function is normally not called: see `table-row` for how rows
	// and their cells are compiled.
	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "td", // Assume body cell.
		properties: {},
		children: state.all(node),
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

const tab = 9; /* `\t` */
const space = 32; /* ` ` */

/**
 * Remove initial and final spaces and tabs at the line breaks in `value`.
 * Does not trim initial and final spaces and tabs of the value itself.
 *
 * @param {string} value
 *   Value to trim.
 * @returns {string}
 *   Trimmed value.
 */
function trimLines(value) {
	const source = String(value);
	const search = /\r?\n|\r/g;
	let match = search.exec(source);
	let last = 0;
	/** @type {Array} */
	const lines = [];

	while (match) {
		lines.push(trimLine(source.slice(last, match.index), last > 0, true), match[0]);

		last = match.index + match[0].length;
		match = search.exec(source);
	}

	lines.push(trimLine(source.slice(last), last > 0, false));

	return lines.join("");
}

/**
 * @param {string} value
 *   Line to trim.
 * @param {boolean} start
 *   Whether to trim the start of the line.
 * @param {boolean} end
 *   Whether to trim the end of the line.
 * @returns {string}
 *   Trimmed line.
 */
function trimLine(value, start, end) {
	let startIndex = 0;
	let endIndex = value.length;

	if (start) {
		let code = value.codePointAt(startIndex);

		while (code === tab || code === space) {
			startIndex++;
			code = value.codePointAt(startIndex);
		}
	}

	if (end) {
		let code = value.codePointAt(endIndex - 1);

		while (code === tab || code === space) {
			endIndex--;
			code = value.codePointAt(endIndex - 1);
		}
	}

	return endIndex > startIndex ? value.slice(startIndex, endIndex) : "";
}

/**
 * @import {Element as HastElement, Text as HastText} from 'hast'
 * @import {Text as MdastText} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `text` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {MdastText} node
 *   mdast node.
 * @returns {HastElement | HastText}
 *   hast node.
 */
function text$1(state, node) {
	/** @type {HastText} */
	const result = { type: "text", value: trimLines(String(node.value)) };
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Element} from 'hast'
 * @import {ThematicBreak} from 'mdast'
 * @import {State} from '../state.js'
 */

/**
 * Turn an mdast `thematicBreak` node into hast.
 *
 * @param {State} state
 *   Info passed around.
 * @param {ThematicBreak} node
 *   mdast node.
 * @returns {Element}
 *   hast node.
 */
function thematicBreak(state, node) {
	/** @type {Element} */
	const result = {
		type: "element",
		tagName: "hr",
		properties: {},
		children: [],
	};
	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * @import {Handlers} from '../state.js'
 */

/**
 * Default handlers for nodes.
 *
 * @satisfies {Handlers}
 */
const handlers = {
	blockquote,
	break: hardBreak,
	code,
	delete: strikethrough,
	emphasis,
	footnoteReference,
	heading,
	html: html$2,
	imageReference,
	image,
	inlineCode,
	linkReference,
	link,
	listItem,
	list,
	paragraph,
	// @ts-expect-error: root is different, but hard to type.
	root: root$1,
	strong,
	table,
	tableCell,
	tableRow,
	text: text$1,
	thematicBreak,
	toml: ignore,
	yaml: ignore,
	definition: ignore,
	footnoteDefinition: ignore,
};

// Return nothing for nodes that are ignored.
function ignore() {
	return undefined;
}

/**
 * @import {ElementContent, Element} from 'hast'
 * @import {State} from './state.js'
 */

/**
 * Generate the default content that GitHub uses on backreferences.
 *
 * @param {number} _
 *   Index of the definition in the order that they are first referenced,
 *   0-indexed.
 * @param {number} rereferenceIndex
 *   Index of calls to the same definition, 0-indexed.
 * @returns {Array}
 *   Content.
 */
function defaultFootnoteBackContent(_, rereferenceIndex) {
	/** @type {Array} */
	const result = [{ type: "text", value: "↩" }];

	if (rereferenceIndex > 1) {
		result.push({
			type: "element",
			tagName: "sup",
			properties: {},
			children: [{ type: "text", value: String(rereferenceIndex) }],
		});
	}

	return result;
}

/**
 * Generate the default label that GitHub uses on backreferences.
 *
 * @param {number} referenceIndex
 *   Index of the definition in the order that they are first referenced,
 *   0-indexed.
 * @param {number} rereferenceIndex
 *   Index of calls to the same definition, 0-indexed.
 * @returns {string}
 *   Label.
 */
function defaultFootnoteBackLabel(referenceIndex, rereferenceIndex) {
	return "Back to reference " + (referenceIndex + 1) + (rereferenceIndex > 1 ? "-" + rereferenceIndex : "");
}

/**
 * Generate a hast footer for called footnote definitions.
 *
 * @param {State} state
 *   Info passed around.
 * @returns {Element | undefined}
 *   `section` element or `undefined`.
 */
// eslint-disable-next-line complexity
function footer(state) {
	const clobberPrefix = typeof state.options.clobberPrefix === "string" ? state.options.clobberPrefix : "user-content-";
	const footnoteBackContent = state.options.footnoteBackContent || defaultFootnoteBackContent;
	const footnoteBackLabel = state.options.footnoteBackLabel || defaultFootnoteBackLabel;
	const footnoteLabel = state.options.footnoteLabel || "Footnotes";
	const footnoteLabelTagName = state.options.footnoteLabelTagName || "h2";
	const footnoteLabelProperties = state.options.footnoteLabelProperties || {
		className: ["sr-only"],
	};
	/** @type {Array} */
	const listItems = [];
	let referenceIndex = -1;

	while (++referenceIndex < state.footnoteOrder.length) {
		const definition = state.footnoteById.get(state.footnoteOrder[referenceIndex]);

		if (!definition) {
			continue;
		}

		const content = state.all(definition);
		const id = String(definition.identifier).toUpperCase();
		const safeId = normalizeUri(id.toLowerCase());
		let rereferenceIndex = 0;
		/** @type {Array} */
		const backReferences = [];
		const counts = state.footnoteCounts.get(id);

		// eslint-disable-next-line no-unmodified-loop-condition
		while (counts !== undefined && ++rereferenceIndex <= counts) {
			if (backReferences.length > 0) {
				backReferences.push({ type: "text", value: " " });
			}

			let children = typeof footnoteBackContent === "string" ? footnoteBackContent : footnoteBackContent(referenceIndex, rereferenceIndex);

			if (typeof children === "string") {
				children = { type: "text", value: children };
			}

			backReferences.push({
				type: "element",
				tagName: "a",
				properties: {
					href: "#" + clobberPrefix + "fnref-" + safeId + (rereferenceIndex > 1 ? "-" + rereferenceIndex : ""),
					dataFootnoteBackref: "",
					ariaLabel: typeof footnoteBackLabel === "string" ? footnoteBackLabel : footnoteBackLabel(referenceIndex, rereferenceIndex),
					className: ["data-footnote-backref"],
				},
				children: Array.isArray(children) ? children : [children],
			});
		}

		const tail = content[content.length - 1];

		if (tail && tail.type === "element" && tail.tagName === "p") {
			const tailTail = tail.children[tail.children.length - 1];
			if (tailTail && tailTail.type === "text") {
				tailTail.value += " ";
			} else {
				tail.children.push({ type: "text", value: " " });
			}

			tail.children.push(...backReferences);
		} else {
			content.push(...backReferences);
		}

		/** @type {Element} */
		const listItem = {
			type: "element",
			tagName: "li",
			properties: { id: clobberPrefix + "fn-" + safeId },
			children: state.wrap(content, true),
		};

		state.patch(definition, listItem);

		listItems.push(listItem);
	}

	if (listItems.length === 0) {
		return;
	}

	return {
		type: "element",
		tagName: "section",
		properties: { dataFootnotes: true, className: ["footnotes"] },
		children: [
			{
				type: "element",
				tagName: footnoteLabelTagName,
				properties: {
					...structuredClone$1(footnoteLabelProperties),
					id: "footnote-label",
				},
				children: [{ type: "text", value: footnoteLabel }],
			},
			{ type: "text", value: "\n" },
			{
				type: "element",
				tagName: "ol",
				properties: {},
				children: state.wrap(listItems, true),
			},
			{ type: "text", value: "\n" },
		],
	};
}

/**
 * @import {
 *   ElementContent as HastElementContent,
 *   Element as HastElement,
 *   Nodes as HastNodes,
 *   Properties as HastProperties,
 *   RootContent as HastRootContent,
 *   Text as HastText
 * } from 'hast'
 * @import {
 *   Definition as MdastDefinition,
 *   FootnoteDefinition as MdastFootnoteDefinition,
 *   Nodes as MdastNodes,
 *   Parents as MdastParents
 * } from 'mdast'
 * @import {VFile} from 'vfile'
 * @import {
 *   FootnoteBackContentTemplate,
 *   FootnoteBackLabelTemplate
 * } from './footer.js'
 */

const own$2 = {}.hasOwnProperty;

/** @type {Options} */
const emptyOptions$1 = {};

/**
 * Create `state` from an mdast tree.
 *
 * @param {MdastNodes} tree
 *   mdast node to transform.
 * @param {Options | null | undefined} [options]
 *   Configuration (optional).
 * @returns {State}
 *   `state` function.
 */
function createState(tree, options) {
	const settings = options || emptyOptions$1;
	/** @type {Map} */
	const definitionById = new Map();
	/** @type {Map} */
	const footnoteById = new Map();
	/** @type {Map} */
	const footnoteCounts = new Map();
	/** @type {Handlers} */
	// @ts-expect-error: the root handler returns a root.
	// Hard to type.
	const handlers$1 = { ...handlers, ...settings.handlers };

	/** @type {State} */
	const state = {
		all,
		applyData,
		definitionById,
		footnoteById,
		footnoteCounts,
		footnoteOrder: [],
		handlers: handlers$1,
		one,
		options: settings,
		patch,
		wrap,
	};

	visit(tree, function (node) {
		if (node.type === "definition" || node.type === "footnoteDefinition") {
			const map = node.type === "definition" ? definitionById : footnoteById;
			const id = String(node.identifier).toUpperCase();

			// Mimick CM behavior of link definitions.
			// See: .
			if (!map.has(id)) {
				// @ts-expect-error: node type matches map.
				map.set(id, node);
			}
		}
	});

	return state;

	/**
	 * Transform an mdast node into a hast node.
	 *
	 * @param {MdastNodes} node
	 *   mdast node.
	 * @param {MdastParents | undefined} [parent]
	 *   Parent of `node`.
	 * @returns {Array | HastElementContent | undefined}
	 *   Resulting hast node.
	 */
	function one(node, parent) {
		const type = node.type;
		const handle = state.handlers[type];

		if (own$2.call(state.handlers, type) && handle) {
			return handle(state, node, parent);
		}

		if (state.options.passThrough && state.options.passThrough.includes(type)) {
			if ("children" in node) {
				const { children, ...shallow } = node;
				const result = structuredClone$1(shallow);
				// @ts-expect-error: TS doesn’t understand…
				result.children = state.all(node);
				// @ts-expect-error: TS doesn’t understand…
				return result;
			}

			// @ts-expect-error: it’s custom.
			return structuredClone$1(node);
		}

		const unknown = state.options.unknownHandler || defaultUnknownHandler;

		return unknown(state, node, parent);
	}

	/**
	 * Transform the children of an mdast node into hast nodes.
	 *
	 * @param {MdastNodes} parent
	 *   mdast node to compile
	 * @returns {Array}
	 *   Resulting hast nodes.
	 */
	function all(parent) {
		/** @type {Array} */
		const values = [];

		if ("children" in parent) {
			const nodes = parent.children;
			let index = -1;
			while (++index < nodes.length) {
				const result = state.one(nodes[index], parent);

				// To do: see if we van clean this? Can we merge texts?
				if (result) {
					if (index && nodes[index - 1].type === "break") {
						if (!Array.isArray(result) && result.type === "text") {
							result.value = trimMarkdownSpaceStart(result.value);
						}

						if (!Array.isArray(result) && result.type === "element") {
							const head = result.children[0];

							if (head && head.type === "text") {
								head.value = trimMarkdownSpaceStart(head.value);
							}
						}
					}

					if (Array.isArray(result)) {
						values.push(...result);
					} else {
						values.push(result);
					}
				}
			}
		}

		return values;
	}
}

/**
 * Copy a node’s positional info.
 *
 * @param {MdastNodes} from
 *   mdast node to copy from.
 * @param {HastNodes} to
 *   hast node to copy into.
 * @returns {undefined}
 *   Nothing.
 */
function patch(from, to) {
	if (from.position) to.position = position(from);
}

/**
 * Honor the `data` of `from` and maybe generate an element instead of `to`.
 *
 * @template {HastNodes} Type
 *   Node type.
 * @param {MdastNodes} from
 *   mdast node to use data from.
 * @param {Type} to
 *   hast node to change.
 * @returns {HastElement | Type}
 *   Nothing.
 */
function applyData(from, to) {
	/** @type {HastElement | Type} */
	let result = to;

	// Handle `data.hName`, `data.hProperties, `data.hChildren`.
	if (from && from.data) {
		const hName = from.data.hName;
		const hChildren = from.data.hChildren;
		const hProperties = from.data.hProperties;

		if (typeof hName === "string") {
			// Transforming the node resulted in an element with a different name
			// than wanted:
			if (result.type === "element") {
				result.tagName = hName;
			}
			// Transforming the node resulted in a non-element, which happens for
			// raw, text, and root nodes (unless custom handlers are passed).
			// The intent of `hName` is to create an element, but likely also to keep
			// the content around (otherwise: pass `hChildren`).
			else {
				/** @type {Array} */
				// @ts-expect-error: assume no doctypes in `root`.
				const children = "children" in result ? result.children : [result];
				result = { type: "element", tagName: hName, properties: {}, children };
			}
		}

		if (result.type === "element" && hProperties) {
			Object.assign(result.properties, structuredClone$1(hProperties));
		}

		if ("children" in result && result.children && hChildren !== null && hChildren !== undefined) {
			result.children = hChildren;
		}
	}

	return result;
}

/**
 * Transform an unknown node.
 *
 * @param {State} state
 *   Info passed around.
 * @param {MdastNodes} node
 *   Unknown mdast node.
 * @returns {HastElement | HastText}
 *   Resulting hast node.
 */
function defaultUnknownHandler(state, node) {
	const data = node.data || {};
	/** @type {HastElement | HastText} */
	const result =
		"value" in node && !(own$2.call(data, "hProperties") || own$2.call(data, "hChildren")) ?
			{ type: "text", value: node.value }
		:	{
				type: "element",
				tagName: "div",
				properties: {},
				children: state.all(node),
			};

	state.patch(node, result);
	return state.applyData(node, result);
}

/**
 * Wrap `nodes` with line endings between each node.
 *
 * @template {HastRootContent} Type
 *   Node type.
 * @param {Array} nodes
 *   List of nodes to wrap.
 * @param {boolean | undefined} [loose=false]
 *   Whether to add line endings at start and end (default: `false`).
 * @returns {Array}
 *   Wrapped nodes.
 */
function wrap(nodes, loose) {
	/** @type {Array} */
	const result = [];
	let index = -1;

	if (loose) {
		result.push({ type: "text", value: "\n" });
	}

	while (++index < nodes.length) {
		if (index) result.push({ type: "text", value: "\n" });
		result.push(nodes[index]);
	}

	if (loose && nodes.length > 0) {
		result.push({ type: "text", value: "\n" });
	}

	return result;
}

/**
 * Trim spaces and tabs at the start of `value`.
 *
 * @param {string} value
 *   Value to trim.
 * @returns {string}
 *   Result.
 */
function trimMarkdownSpaceStart(value) {
	let index = 0;
	let code = value.charCodeAt(index);

	while (code === 9 || code === 32) {
		index++;
		code = value.charCodeAt(index);
	}

	return value.slice(index);
}

/**
 * @import {Nodes as HastNodes} from 'hast'
 * @import {Nodes as MdastNodes} from 'mdast'
 * @import {Options} from './state.js'
 */

/**
 * Transform mdast to hast.
 *
 * ##### Notes
 *
 * ###### HTML
 *
 * Raw HTML is available in mdast as `html` nodes and can be embedded in hast
 * as semistandard `raw` nodes.
 * Most utilities ignore `raw` nodes but two notable ones don’t:
 *
 * *   `hast-util-to-html` also has an option `allowDangerousHtml` which will
 *     output the raw HTML.
 *     This is typically discouraged as noted by the option name but is useful
 *     if you completely trust authors
 * *   `hast-util-raw` can handle the raw embedded HTML strings by parsing them
 *     into standard hast nodes (`element`, `text`, etc).
 *     This is a heavy task as it needs a full HTML parser, but it is the only
 *     way to support untrusted content
 *
 * ###### Footnotes
 *
 * Many options supported here relate to footnotes.
 * Footnotes are not specified by CommonMark, which we follow by default.
 * They are supported by GitHub, so footnotes can be enabled in markdown with
 * `mdast-util-gfm`.
 *
 * The options `footnoteBackLabel` and `footnoteLabel` define natural language
 * that explains footnotes, which is hidden for sighted users but shown to
 * assistive technology.
 * When your page is not in English, you must define translated values.
 *
 * Back references use ARIA attributes, but the section label itself uses a
 * heading that is hidden with an `sr-only` class.
 * To show it to sighted users, define different attributes in
 * `footnoteLabelProperties`.
 *
 * ###### Clobbering
 *
 * Footnotes introduces a problem, as it links footnote calls to footnote
 * definitions on the page through `id` attributes generated from user content,
 * which results in DOM clobbering.
 *
 * DOM clobbering is this:
 *
 * ```html
 * 

* * ``` * * Elements by their ID are made available by browsers on the `window` object, * which is a security risk. * Using a prefix solves this problem. * * More information on how to handle clobbering and the prefix is explained in * Example: headings (DOM clobbering) in `rehype-sanitize`. * * ###### Unknown nodes * * Unknown nodes are nodes with a type that isn’t in `handlers` or `passThrough`. * The default behavior for unknown nodes is: * * * when the node has a `value` (and doesn’t have `data.hName`, * `data.hProperties`, or `data.hChildren`, see later), create a hast `text` * node * * otherwise, create a `
` element (which could be changed with * `data.hName`), with its children mapped from mdast to hast as well * * This behavior can be changed by passing an `unknownHandler`. * * @param {MdastNodes} tree * mdast tree. * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {HastNodes} * hast tree. */ function toHast(tree, options) { const state = createState(tree, options); const node = state.one(tree, undefined); const foot = footer(state); /** @type {HastNodes} */ const result = Array.isArray(node) ? { type: "root", children: node } : node || { type: "root", children: [] }; if (foot) { result.children.push({ type: "text", value: "\n" }, foot); } return result; } /** * @import {Root as HastRoot} from 'hast' * @import {Root as MdastRoot} from 'mdast' * @import {Options as ToHastOptions} from 'mdast-util-to-hast' * @import {Processor} from 'unified' * @import {VFile} from 'vfile' */ /** * Turn markdown into HTML. * * ##### Notes * * ###### Signature * * * if a processor is given, * runs the (rehype) plugins used on it with a hast tree, * then discards the result (*bridge mode*) * * otherwise, * returns a hast tree, * the plugins used after `remarkRehype` are rehype plugins (*mutate mode*) * * > 👉 **Note**: * > It’s highly unlikely that you want to pass a `processor`. * * ###### HTML * * Raw HTML is available in mdast as `html` nodes and can be embedded in hast * as semistandard `raw` nodes. * Most plugins ignore `raw` nodes but two notable ones don’t: * * * `rehype-stringify` also has an option `allowDangerousHtml` which will * output the raw HTML. * This is typically discouraged as noted by the option name but is useful if * you completely trust authors * * `rehype-raw` can handle the raw embedded HTML strings by parsing them * into standard hast nodes (`element`, `text`, etc); * this is a heavy task as it needs a full HTML parser, * but it is the only way to support untrusted content * * ###### Footnotes * * Many options supported here relate to footnotes. * Footnotes are not specified by CommonMark, * which we follow by default. * They are supported by GitHub, * so footnotes can be enabled in markdown with `remark-gfm`. * * The options `footnoteBackLabel` and `footnoteLabel` define natural language * that explains footnotes, * which is hidden for sighted users but shown to assistive technology. * When your page is not in English, * you must define translated values. * * Back references use ARIA attributes, * but the section label itself uses a heading that is hidden with an * `sr-only` class. * To show it to sighted users, * define different attributes in `footnoteLabelProperties`. * * ###### Clobbering * * Footnotes introduces a problem, * as it links footnote calls to footnote definitions on the page through `id` * attributes generated from user content, * which results in DOM clobbering. * * DOM clobbering is this: * * ```html *

* * ``` * * Elements by their ID are made available by browsers on the `window` object, * which is a security risk. * Using a prefix solves this problem. * * More information on how to handle clobbering and the prefix is explained in * *Example: headings (DOM clobbering)* in `rehype-sanitize`. * * ###### Unknown nodes * * Unknown nodes are nodes with a type that isn’t in `handlers` or `passThrough`. * The default behavior for unknown nodes is: * * * when the node has a `value` * (and doesn’t have `data.hName`, `data.hProperties`, or `data.hChildren`, * see later), * create a hast `text` node * * otherwise, * create a `
` element (which could be changed with `data.hName`), * with its children mapped from mdast to hast as well * * This behavior can be changed by passing an `unknownHandler`. * * @overload * @param {Processor} processor * @param {Readonly | null | undefined} [options] * @returns {TransformBridge} * * @overload * @param {Readonly | null | undefined} [options] * @returns {TransformMutate} * * @overload * @param {Readonly | Processor | null | undefined} [destination] * @param {Readonly | null | undefined} [options] * @returns {TransformBridge | TransformMutate} * * @param {Readonly | Processor | null | undefined} [destination] * Processor or configuration (optional). * @param {Readonly | null | undefined} [options] * When a processor was given, * configuration (optional). * @returns {TransformBridge | TransformMutate} * Transform. */ function remarkRehype(destination, options) { if (destination && "run" in destination) { /** * @type {TransformBridge} */ return async function (tree, file) { // Cast because root in -> root out. const hastTree = /** @type {HastRoot} */ (toHast(tree, { file, ...options })); await destination.run(hastTree, file); }; } /** * @type {TransformMutate} */ return function (tree, file) { // Cast because root in -> root out. // To do: in the future, disallow ` || options` fallback. // With `unified-engine`, `destination` can be `undefined` but // `options` will be the file set. // We should not pass that as `options`. return /** @type {HastRoot} */ (toHast(tree, { file, ...(destination || options) })); }; } /** * List of HTML void tag names. * * @type {Array} */ const htmlVoidElements = ["area", "base", "basefont", "bgsound", "br", "col", "command", "embed", "frame", "hr", "image", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]; /** * @typedef CoreOptions * @property {ReadonlyArray} [subset=[]] * Whether to only escape the given subset of characters. * @property {boolean} [escapeOnly=false] * Whether to only escape possibly dangerous characters. * Those characters are `"`, `&`, `'`, `<`, `>`, and `` ` ``. * * @typedef FormatOptions * @property {(code: number, next: number, options: CoreWithFormatOptions) => string} format * Format strategy. * * @typedef {CoreOptions & FormatOptions & import('./util/format-smart.js').FormatSmartOptions} CoreWithFormatOptions */ const defaultSubsetRegex = /["&'<>`]/g; const surrogatePairsRegex = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g; const controlCharactersRegex = // eslint-disable-next-line no-control-regex, unicorn/no-hex-escape /[\x01-\t\v\f\x0E-\x1F\x7F\x81\x8D\x8F\x90\x9D\xA0-\uFFFF]/g; const regexEscapeRegex = /[|\\{}()[\]^$+*?.]/g; /** @type {WeakMap, RegExp>} */ const subsetToRegexCache = new WeakMap(); /** * Encode certain characters in `value`. * * @param {string} value * @param {CoreWithFormatOptions} options * @returns {string} */ function core(value, options) { value = value.replace(options.subset ? charactersToExpressionCached(options.subset) : defaultSubsetRegex, basic); if (options.subset || options.escapeOnly) { return value; } return ( value // Surrogate pairs. .replace(surrogatePairsRegex, surrogate) // BMP control characters (C0 except for LF, CR, SP; DEL; and some more // non-ASCII ones). .replace(controlCharactersRegex, basic) ); /** * @param {string} pair * @param {number} index * @param {string} all */ function surrogate(pair, index, all) { return options.format((pair.charCodeAt(0) - 0xd800) * 0x400 + pair.charCodeAt(1) - 0xdc00 + 0x10000, all.charCodeAt(index + 2), options); } /** * @param {string} character * @param {number} index * @param {string} all */ function basic(character, index, all) { return options.format(character.charCodeAt(0), all.charCodeAt(index + 1), options); } } /** * A wrapper function that caches the result of `charactersToExpression` with a WeakMap. * This can improve performance when tooling calls `charactersToExpression` repeatedly * with the same subset. * * @param {ReadonlyArray} subset * @returns {RegExp} */ function charactersToExpressionCached(subset) { let cached = subsetToRegexCache.get(subset); if (!cached) { cached = charactersToExpression(subset); subsetToRegexCache.set(subset, cached); } return cached; } /** * @param {ReadonlyArray} subset * @returns {RegExp} */ function charactersToExpression(subset) { /** @type {Array} */ const groups = []; let index = -1; while (++index < subset.length) { groups.push(subset[index].replace(regexEscapeRegex, "\\$&")); } return new RegExp("(?:" + groups.join("|") + ")", "g"); } const hexadecimalRegex = /[\dA-Fa-f]/; /** * Configurable ways to encode characters as hexadecimal references. * * @param {number} code * @param {number} next * @param {boolean|undefined} omit * @returns {string} */ function toHexadecimal(code, next, omit) { const value = "&#x" + code.toString(16).toUpperCase(); return omit && next && !hexadecimalRegex.test(String.fromCharCode(next)) ? value : value + ";"; } const decimalRegex = /\d/; /** * Configurable ways to encode characters as decimal references. * * @param {number} code * @param {number} next * @param {boolean|undefined} omit * @returns {string} */ function toDecimal(code, next, omit) { const value = "&#" + String(code); return omit && next && !decimalRegex.test(String.fromCharCode(next)) ? value : value + ";"; } /** * List of legacy HTML named character references that don’t need a trailing semicolon. * * @type {Array} */ const characterEntitiesLegacy = [ "AElig", "AMP", "Aacute", "Acirc", "Agrave", "Aring", "Atilde", "Auml", "COPY", "Ccedil", "ETH", "Eacute", "Ecirc", "Egrave", "Euml", "GT", "Iacute", "Icirc", "Igrave", "Iuml", "LT", "Ntilde", "Oacute", "Ocirc", "Ograve", "Oslash", "Otilde", "Ouml", "QUOT", "REG", "THORN", "Uacute", "Ucirc", "Ugrave", "Uuml", "Yacute", "aacute", "acirc", "acute", "aelig", "agrave", "amp", "aring", "atilde", "auml", "brvbar", "ccedil", "cedil", "cent", "copy", "curren", "deg", "divide", "eacute", "ecirc", "egrave", "eth", "euml", "frac12", "frac14", "frac34", "gt", "iacute", "icirc", "iexcl", "igrave", "iquest", "iuml", "laquo", "lt", "macr", "micro", "middot", "nbsp", "not", "ntilde", "oacute", "ocirc", "ograve", "ordf", "ordm", "oslash", "otilde", "ouml", "para", "plusmn", "pound", "quot", "raquo", "reg", "sect", "shy", "sup1", "sup2", "sup3", "szlig", "thorn", "times", "uacute", "ucirc", "ugrave", "uml", "uuml", "yacute", "yen", "yuml", ]; /** * Map of named character references from HTML 4. * * @type {Record} */ const characterEntitiesHtml4 = { nbsp: " ", iexcl: "¡", cent: "¢", pound: "£", curren: "¤", yen: "¥", brvbar: "¦", sect: "§", uml: "¨", copy: "©", ordf: "ª", laquo: "«", not: "¬", shy: "­", reg: "®", macr: "¯", deg: "°", plusmn: "±", sup2: "²", sup3: "³", acute: "´", micro: "µ", para: "¶", middot: "·", cedil: "¸", sup1: "¹", ordm: "º", raquo: "»", frac14: "¼", frac12: "½", frac34: "¾", iquest: "¿", Agrave: "À", Aacute: "Á", Acirc: "Â", Atilde: "Ã", Auml: "Ä", Aring: "Å", AElig: "Æ", Ccedil: "Ç", Egrave: "È", Eacute: "É", Ecirc: "Ê", Euml: "Ë", Igrave: "Ì", Iacute: "Í", Icirc: "Î", Iuml: "Ï", ETH: "Ð", Ntilde: "Ñ", Ograve: "Ò", Oacute: "Ó", Ocirc: "Ô", Otilde: "Õ", Ouml: "Ö", times: "×", Oslash: "Ø", Ugrave: "Ù", Uacute: "Ú", Ucirc: "Û", Uuml: "Ü", Yacute: "Ý", THORN: "Þ", szlig: "ß", agrave: "à", aacute: "á", acirc: "â", atilde: "ã", auml: "ä", aring: "å", aelig: "æ", ccedil: "ç", egrave: "è", eacute: "é", ecirc: "ê", euml: "ë", igrave: "ì", iacute: "í", icirc: "î", iuml: "ï", eth: "ð", ntilde: "ñ", ograve: "ò", oacute: "ó", ocirc: "ô", otilde: "õ", ouml: "ö", divide: "÷", oslash: "ø", ugrave: "ù", uacute: "ú", ucirc: "û", uuml: "ü", yacute: "ý", thorn: "þ", yuml: "ÿ", fnof: "ƒ", Alpha: "Α", Beta: "Β", Gamma: "Γ", Delta: "Δ", Epsilon: "Ε", Zeta: "Ζ", Eta: "Η", Theta: "Θ", Iota: "Ι", Kappa: "Κ", Lambda: "Λ", Mu: "Μ", Nu: "Ν", Xi: "Ξ", Omicron: "Ο", Pi: "Π", Rho: "Ρ", Sigma: "Σ", Tau: "Τ", Upsilon: "Υ", Phi: "Φ", Chi: "Χ", Psi: "Ψ", Omega: "Ω", alpha: "α", beta: "β", gamma: "γ", delta: "δ", epsilon: "ε", zeta: "ζ", eta: "η", theta: "θ", iota: "ι", kappa: "κ", lambda: "λ", mu: "μ", nu: "ν", xi: "ξ", omicron: "ο", pi: "π", rho: "ρ", sigmaf: "ς", sigma: "σ", tau: "τ", upsilon: "υ", phi: "φ", chi: "χ", psi: "ψ", omega: "ω", thetasym: "ϑ", upsih: "ϒ", piv: "ϖ", bull: "•", hellip: "…", prime: "′", Prime: "″", oline: "‾", frasl: "⁄", weierp: "℘", image: "ℑ", real: "ℜ", trade: "™", alefsym: "ℵ", larr: "←", uarr: "↑", rarr: "→", darr: "↓", harr: "↔", crarr: "↵", lArr: "⇐", uArr: "⇑", rArr: "⇒", dArr: "⇓", hArr: "⇔", forall: "∀", part: "∂", exist: "∃", empty: "∅", nabla: "∇", isin: "∈", notin: "∉", ni: "∋", prod: "∏", sum: "∑", minus: "−", lowast: "∗", radic: "√", prop: "∝", infin: "∞", ang: "∠", and: "∧", or: "∨", cap: "∩", cup: "∪", int: "∫", there4: "∴", sim: "∼", cong: "≅", asymp: "≈", ne: "≠", equiv: "≡", le: "≤", ge: "≥", sub: "⊂", sup: "⊃", nsub: "⊄", sube: "⊆", supe: "⊇", oplus: "⊕", otimes: "⊗", perp: "⊥", sdot: "⋅", lceil: "⌈", rceil: "⌉", lfloor: "⌊", rfloor: "⌋", lang: "〈", rang: "〉", loz: "◊", spades: "♠", clubs: "♣", hearts: "♥", diams: "♦", quot: '"', amp: "&", lt: "<", gt: ">", OElig: "Œ", oelig: "œ", Scaron: "Š", scaron: "š", Yuml: "Ÿ", circ: "ˆ", tilde: "˜", ensp: " ", emsp: " ", thinsp: " ", zwnj: "‌", zwj: "‍", lrm: "‎", rlm: "‏", ndash: "–", mdash: "—", lsquo: "‘", rsquo: "’", sbquo: "‚", ldquo: "“", rdquo: "”", bdquo: "„", dagger: "†", Dagger: "‡", permil: "‰", lsaquo: "‹", rsaquo: "›", euro: "€", }; /** * List of legacy (that don’t need a trailing `;`) named references which could, * depending on what follows them, turn into a different meaning * * @type {Array} */ const dangerous = ["cent", "copy", "divide", "gt", "lt", "not", "para", "times"]; const own$1 = {}.hasOwnProperty; /** * `characterEntitiesHtml4` but inverted. * * @type {Record} */ const characters = {}; /** @type {string} */ let key; for (key in characterEntitiesHtml4) { if (own$1.call(characterEntitiesHtml4, key)) { characters[characterEntitiesHtml4[key]] = key; } } const notAlphanumericRegex = /[^\dA-Za-z]/; /** * Configurable ways to encode characters as named references. * * @param {number} code * @param {number} next * @param {boolean|undefined} omit * @param {boolean|undefined} attribute * @returns {string} */ function toNamed(code, next, omit, attribute) { const character = String.fromCharCode(code); if (own$1.call(characters, character)) { const name = characters[character]; const value = "&" + name; if (omit && characterEntitiesLegacy.includes(name) && !dangerous.includes(name) && (!attribute || (next && next !== 61 /* `=` */ && notAlphanumericRegex.test(String.fromCharCode(next))))) { return value; } return value + ";"; } return ""; } /** * @typedef FormatSmartOptions * @property {boolean} [useNamedReferences=false] * Prefer named character references (`&`) where possible. * @property {boolean} [useShortestReferences=false] * Prefer the shortest possible reference, if that results in less bytes. * **Note**: `useNamedReferences` can be omitted when using `useShortestReferences`. * @property {boolean} [omitOptionalSemicolons=false] * Whether to omit semicolons when possible. * **Note**: This creates what HTML calls “parse errors” but is otherwise still valid HTML — don’t use this except when building a minifier. * Omitting semicolons is possible for certain named and numeric references in some cases. * @property {boolean} [attribute=false] * Create character references which don’t fail in attributes. * **Note**: `attribute` only applies when operating dangerously with * `omitOptionalSemicolons: true`. */ /** * Configurable ways to encode a character yielding pretty or small results. * * @param {number} code * @param {number} next * @param {FormatSmartOptions} options * @returns {string} */ function formatSmart(code, next, options) { let numeric = toHexadecimal(code, next, options.omitOptionalSemicolons); /** @type {string|undefined} */ let named; if (options.useNamedReferences || options.useShortestReferences) { named = toNamed(code, next, options.omitOptionalSemicolons, options.attribute); } // Use the shortest numeric reference when requested. // A simple algorithm would use decimal for all code points under 100, as // those are shorter than hexadecimal: // // * `c` vs `c` (decimal shorter) // * `d` vs `d` (equal) // // However, because we take `next` into consideration when `omit` is used, // And it would be possible that decimals are shorter on bigger values as // well if `next` is hexadecimal but not decimal, we instead compare both. if ((options.useShortestReferences || !named) && options.useShortestReferences) { const decimal = toDecimal(code, next, options.omitOptionalSemicolons); if (decimal.length < numeric.length) { numeric = decimal; } } return named && (!options.useShortestReferences || named.length < numeric.length) ? named : numeric; } /** * @typedef {import('./core.js').CoreOptions & import('./util/format-smart.js').FormatSmartOptions} Options * @typedef {import('./core.js').CoreOptions} LightOptions */ /** * Encode special characters in `value`. * * @param {string} value * Value to encode. * @param {Options} [options] * Configuration. * @returns {string} * Encoded value. */ function stringifyEntities(value, options) { return core(value, Object.assign({ format: formatSmart }, options)); } /** * @import {Comment, Parents} from 'hast' * @import {State} from '../index.js' */ const htmlCommentRegex = /^>|^->||--!>|"]; const commentEntitySubset = ["<", ">"]; /** * Serialize a comment. * * @param {Comment} node * Node to handle. * @param {number | undefined} _1 * Index of `node` in `parent. * @param {Parents | undefined} _2 * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function comment(node, _1, _2, state) { // See: return state.settings.bogusComments ? "" : ""; /** * @param {string} $0 */ function encode($0) { return stringifyEntities( $0, Object.assign({}, state.settings.characterReferences, { subset: commentEntitySubset, }), ); } } /** * @import {Doctype, Parents} from 'hast' * @import {State} from '../index.js' */ /** * Serialize a doctype. * * @param {Doctype} _1 * Node to handle. * @param {number | undefined} _2 * Index of `node` in `parent. * @param {Parents | undefined} _3 * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function doctype(_1, _2, _3, state) { return ""; } /** * @import {Parents, RootContent} from 'hast' */ const siblingAfter = siblings(1); const siblingBefore = siblings(-1); /** @type {Array} */ const emptyChildren$1 = []; /** * Factory to check siblings in a direction. * * @param {number} increment */ function siblings(increment) { return sibling; /** * Find applicable siblings in a direction. * * @template {Parents} Parent * Parent type. * @param {Parent | undefined} parent * Parent. * @param {number | undefined} index * Index of child in `parent`. * @param {boolean | undefined} [includeWhitespace=false] * Whether to include whitespace (default: `false`). * @returns {Parent extends {children: Array} ? Child | undefined : never} * Child of parent. */ function sibling(parent, index, includeWhitespace) { const siblings = parent ? parent.children : emptyChildren$1; let offset = (index || 0) + increment; let next = siblings[offset]; if (!includeWhitespace) { while (next && whitespace(next)) { offset += increment; next = siblings[offset]; } } // @ts-expect-error: it’s a correct child. return next; } } /** * @import {Element, Parents} from 'hast' */ /** * @callback OmitHandle * Check if a tag can be omitted. * @param {Element} element * Element to check. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether to omit a tag. * */ const own = {}.hasOwnProperty; /** * Factory to check if a given node can have a tag omitted. * * @param {Record} handlers * Omission handlers, where each key is a tag name, and each value is the * corresponding handler. * @returns {OmitHandle} * Whether to omit a tag of an element. */ function omission(handlers) { return omit; /** * Check if a given node can have a tag omitted. * * @type {OmitHandle} */ function omit(node, index, parent) { return own.call(handlers, node.tagName) && handlers[node.tagName](node, index, parent); } } /** * @import {Element, Parents} from 'hast' */ const closing = omission({ body: body$1, caption: headOrColgroupOrCaption, colgroup: headOrColgroupOrCaption, dd, dt, head: headOrColgroupOrCaption, html: html$1, li, optgroup, option, p, rp: rubyElement, rt: rubyElement, tbody: tbody$1, td: cells, tfoot, th: cells, thead, tr, }); /** * Macro for ``, ``, and ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function headOrColgroupOrCaption(_, index, parent) { const next = siblingAfter(parent, index, true); return !next || (next.type !== "comment" && !(next.type === "text" && whitespace(next.value.charAt(0)))); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function html$1(_, index, parent) { const next = siblingAfter(parent, index); return !next || next.type !== "comment"; } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function body$1(_, index, parent) { const next = siblingAfter(parent, index); return !next || next.type !== "comment"; } /** * Whether to omit `

`. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function p(_, index, parent) { const next = siblingAfter(parent, index); return next ? next.type === "element" && (next.tagName === "address" || next.tagName === "article" || next.tagName === "aside" || next.tagName === "blockquote" || next.tagName === "details" || next.tagName === "div" || next.tagName === "dl" || next.tagName === "fieldset" || next.tagName === "figcaption" || next.tagName === "figure" || next.tagName === "footer" || next.tagName === "form" || next.tagName === "h1" || next.tagName === "h2" || next.tagName === "h3" || next.tagName === "h4" || next.tagName === "h5" || next.tagName === "h6" || next.tagName === "header" || next.tagName === "hgroup" || next.tagName === "hr" || next.tagName === "main" || next.tagName === "menu" || next.tagName === "nav" || next.tagName === "ol" || next.tagName === "p" || next.tagName === "pre" || next.tagName === "section" || next.tagName === "table" || next.tagName === "ul") : !parent || // Confusing parent. !( parent.type === "element" && (parent.tagName === "a" || parent.tagName === "audio" || parent.tagName === "del" || parent.tagName === "ins" || parent.tagName === "map" || parent.tagName === "noscript" || parent.tagName === "video") ); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function li(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && next.tagName === "li"); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function dt(_, index, parent) { const next = siblingAfter(parent, index); return Boolean(next && next.type === "element" && (next.tagName === "dt" || next.tagName === "dd")); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function dd(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && (next.tagName === "dt" || next.tagName === "dd")); } /** * Whether to omit `` or ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function rubyElement(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && (next.tagName === "rp" || next.tagName === "rt")); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function optgroup(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && next.tagName === "optgroup"); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function option(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && (next.tagName === "option" || next.tagName === "optgroup")); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function thead(_, index, parent) { const next = siblingAfter(parent, index); return Boolean(next && next.type === "element" && (next.tagName === "tbody" || next.tagName === "tfoot")); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function tbody$1(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && (next.tagName === "tbody" || next.tagName === "tfoot")); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function tfoot(_, index, parent) { return !siblingAfter(parent, index); } /** * Whether to omit ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function tr(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && next.tagName === "tr"); } /** * Whether to omit `` or ``. * * @param {Element} _ * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the closing tag can be omitted. */ function cells(_, index, parent) { const next = siblingAfter(parent, index); return !next || (next.type === "element" && (next.tagName === "td" || next.tagName === "th")); } /** * @import {Element, Parents} from 'hast' */ const opening = omission({ body, colgroup, head, html, tbody, }); /** * Whether to omit ``. * * @param {Element} node * Element. * @returns {boolean} * Whether the opening tag can be omitted. */ function html(node) { const head = siblingAfter(node, -1); return !head || head.type !== "comment"; } /** * Whether to omit ``. * * @param {Element} node * Element. * @returns {boolean} * Whether the opening tag can be omitted. */ function head(node) { /** @type {Set} */ const seen = new Set(); // Whether `srcdoc` or not, // make sure the content model at least doesn’t have too many `base`s/`title`s. for (const child of node.children) { if (child.type === "element" && (child.tagName === "base" || child.tagName === "title")) { if (seen.has(child.tagName)) return false; seen.add(child.tagName); } } // “May be omitted if the element is empty, // or if the first thing inside the head element is an element.” const child = node.children[0]; return !child || child.type === "element"; } /** * Whether to omit ``. * * @param {Element} node * Element. * @returns {boolean} * Whether the opening tag can be omitted. */ function body(node) { const head = siblingAfter(node, -1, true); return ( !head || (head.type !== "comment" && !(head.type === "text" && whitespace(head.value.charAt(0))) && !(head.type === "element" && (head.tagName === "meta" || head.tagName === "link" || head.tagName === "script" || head.tagName === "style" || head.tagName === "template"))) ); } /** * Whether to omit `
`. * The spec describes some logic for the opening tag, but it’s easier to * implement in the closing tag, to the same effect, so we handle it there * instead. * * @param {Element} node * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the opening tag can be omitted. */ function colgroup(node, index, parent) { const previous = siblingBefore(parent, index); const head = siblingAfter(node, -1, true); // Previous colgroup was already omitted. if (parent && previous && previous.type === "element" && previous.tagName === "colgroup" && closing(previous, parent.children.indexOf(previous), parent)) { return false; } return Boolean(head && head.type === "element" && head.tagName === "col"); } /** * Whether to omit ``. * * @param {Element} node * Element. * @param {number | undefined} index * Index of element in parent. * @param {Parents | undefined} parent * Parent of element. * @returns {boolean} * Whether the opening tag can be omitted. */ function tbody(node, index, parent) { const previous = siblingBefore(parent, index); const head = siblingAfter(node, -1); // Previous table section was already omitted. if (parent && previous && previous.type === "element" && (previous.tagName === "thead" || previous.tagName === "tbody") && closing(previous, parent.children.indexOf(previous), parent)) { return false; } return Boolean(head && head.type === "element" && head.tagName === "tr"); } /** * @import {Element, Parents, Properties} from 'hast' * @import {State} from '../index.js' */ /** * Maps of subsets. * * Each value is a matrix of tuples. * The value at `0` causes parse errors, the value at `1` is valid. * Of both, the value at `0` is unsafe, and the value at `1` is safe. * * @type {Record<'double' | 'name' | 'single' | 'unquoted', Array<[Array, Array]>>} */ const constants = { // See: . name: [ ["\t\n\f\r &/=>".split(""), "\t\n\f\r \"&'/=>`".split("")], ["\0\t\n\f\r \"&'/<=>".split(""), "\0\t\n\f\r \"&'/<=>`".split("")], ], // See: . unquoted: [ ["\t\n\f\r &>".split(""), "\0\t\n\f\r \"&'<=>`".split("")], ["\0\t\n\f\r \"&'<=>`".split(""), "\0\t\n\f\r \"&'<=>`".split("")], ], // See: . single: [ ["&'".split(""), "\"&'`".split("")], ["\0&'".split(""), "\0\"&'`".split("")], ], // See: . double: [ ['"&'.split(""), "\"&'`".split("")], ['\0"&'.split(""), "\0\"&'`".split("")], ], }; /** * Serialize an element node. * * @param {Element} node * Node to handle. * @param {number | undefined} index * Index of `node` in `parent. * @param {Parents | undefined} parent * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function element(node, index, parent, state) { const schema = state.schema; const omit = schema.space === "svg" ? false : state.settings.omitOptionalTags; let selfClosing = schema.space === "svg" ? state.settings.closeEmptyElements : state.settings.voids.includes(node.tagName.toLowerCase()); /** @type {Array} */ const parts = []; /** @type {string} */ let last; if (schema.space === "html" && node.tagName === "svg") { state.schema = svg; } const attributes = serializeAttributes(state, node.properties); const content = state.all(schema.space === "html" && node.tagName === "template" ? node.content : node); state.schema = schema; // If the node is categorised as void, but it has children, remove the // categorisation. // This enables for example `menuitem`s, which are void in W3C HTML but not // void in WHATWG HTML, to be stringified properly. // Note: `menuitem` has since been removed from the HTML spec, and so is no // longer void. if (content) selfClosing = false; if (attributes || !omit || !opening(node, index, parent)) { parts.push("<", node.tagName, attributes ? " " + attributes : ""); if (selfClosing && (schema.space === "svg" || state.settings.closeSelfClosing)) { last = attributes.charAt(attributes.length - 1); if (!state.settings.tightSelfClosing || last === "/" || (last && last !== '"' && last !== "'")) { parts.push(" "); } parts.push("/"); } parts.push(">"); } parts.push(content); if (!selfClosing && (!omit || !closing(node, index, parent))) { parts.push(""); } return parts.join(""); } /** * @param {State} state * @param {Properties | null | undefined} properties * @returns {string} */ function serializeAttributes(state, properties) { /** @type {Array} */ const values = []; let index = -1; /** @type {string} */ let key; if (properties) { for (key in properties) { if (properties[key] !== null && properties[key] !== undefined) { const value = serializeAttribute(state, key, properties[key]); if (value) values.push(value); } } } while (++index < values.length) { const last = state.settings.tightAttributes ? values[index].charAt(values[index].length - 1) : undefined; // In tight mode, don’t add a space after quoted attributes. if (index !== values.length - 1 && last !== '"' && last !== "'") { values[index] += " "; } } return values.join(""); } /** * @param {State} state * @param {string} key * @param {Properties[keyof Properties]} value * @returns {string} */ function serializeAttribute(state, key, value) { const info = find(state.schema, key); const x = state.settings.allowParseErrors && state.schema.space === "html" ? 0 : 1; const y = state.settings.allowDangerousCharacters ? 0 : 1; let quote = state.quote; /** @type {string | undefined} */ let result; if (info.overloadedBoolean && (value === info.attribute || value === "")) { value = true; } else if ((info.boolean || info.overloadedBoolean) && (typeof value !== "string" || value === info.attribute || value === "")) { value = Boolean(value); } if (value === null || value === undefined || value === false || (typeof value === "number" && Number.isNaN(value))) { return ""; } const name = stringifyEntities( info.attribute, Object.assign({}, state.settings.characterReferences, { // Always encode without parse errors in non-HTML. subset: constants.name[x][y], }), ); // No value. // There is currently only one boolean property in SVG: `[download]` on // ``. // This property does not seem to work in browsers (Firefox, Safari, Chrome), // so I can’t test if dropping the value works. // But I assume that it should: // // ```html // // // // // // // ``` // // See: if (value === true) return name; // `spaces` doesn’t accept a second argument, but it’s given here just to // keep the code cleaner. value = Array.isArray(value) ? (info.commaSeparated ? stringify$1 : stringify)(value, { padLeft: !state.settings.tightCommaSeparatedLists, }) : String(value); if (state.settings.collapseEmptyAttributes && !value) return name; // Check unquoted value. if (state.settings.preferUnquoted) { result = stringifyEntities( value, Object.assign({}, state.settings.characterReferences, { attribute: true, subset: constants.unquoted[x][y], }), ); } // If we don’t want unquoted, or if `value` contains character references when // unquoted… if (result !== value) { // If the alternative is less common than `quote`, switch. if (state.settings.quoteSmart && ccount(value, quote) > ccount(value, state.alternative)) { quote = state.alternative; } result = quote + stringifyEntities( value, Object.assign({}, state.settings.characterReferences, { // Always encode without parse errors in non-HTML. subset: (quote === "'" ? constants.single : constants.double)[x][y], attribute: true, }), ) + quote; } // Don’t add a `=` for unquoted empties. return name + (result ? "=" + result : result); } /** * @import {Parents, Text} from 'hast' * @import {Raw} from 'mdast-util-to-hast' * @import {State} from '../index.js' */ // Declare array as variable so it can be cached by `stringifyEntities` const textEntitySubset = ["<", "&"]; /** * Serialize a text node. * * @param {Raw | Text} node * Node to handle. * @param {number | undefined} _ * Index of `node` in `parent. * @param {Parents | undefined} parent * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function text(node, _, parent, state) { // Check if content of `node` should be escaped. return parent && parent.type === "element" && (parent.tagName === "script" || parent.tagName === "style") ? node.value : stringifyEntities( node.value, Object.assign({}, state.settings.characterReferences, { subset: textEntitySubset, }), ); } /** * @import {Parents} from 'hast' * @import {Raw} from 'mdast-util-to-hast' * @import {State} from '../index.js' */ /** * Serialize a raw node. * * @param {Raw} node * Node to handle. * @param {number | undefined} index * Index of `node` in `parent. * @param {Parents | undefined} parent * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function raw(node, index, parent, state) { return state.settings.allowDangerousHtml ? node.value : text(node, index, parent, state); } /** * @import {Parents, Root} from 'hast' * @import {State} from '../index.js' */ /** * Serialize a root. * * @param {Root} node * Node to handle. * @param {number | undefined} _1 * Index of `node` in `parent. * @param {Parents | undefined} _2 * Parent of `node`. * @param {State} state * Info passed around about the current state. * @returns {string} * Serialized node. */ function root(node, _1, _2, state) { return state.all(node); } /** * @import {Nodes, Parents} from 'hast' * @import {State} from '../index.js' */ /** * @type {(node: Nodes, index: number | undefined, parent: Parents | undefined, state: State) => string} */ const handle = zwitch("type", { invalid, unknown, handlers: { comment, doctype, element, raw, root, text }, }); /** * Fail when a non-node is found in the tree. * * @param {unknown} node * Unknown value. * @returns {never} * Never. */ function invalid(node) { throw new Error("Expected node, not `" + node + "`"); } /** * Fail when a node with an unknown type is found in the tree. * * @param {unknown} node_ * Unknown node. * @returns {never} * Never. */ function unknown(node_) { // `type` is guaranteed by runtime JS. const node = /** @type {Nodes} */ (node_); throw new Error("Cannot compile unknown node `" + node.type + "`"); } /** * @import {Nodes, Parents, RootContent} from 'hast' * @import {Schema} from 'property-information' * @import {Options as StringifyEntitiesOptions} from 'stringify-entities' */ /** @type {Options} */ const emptyOptions = {}; /** @type {CharacterReferences} */ const emptyCharacterReferences = {}; /** @type {Array} */ const emptyChildren = []; /** * Serialize hast as HTML. * * @param {Array | Nodes} tree * Tree to serialize. * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {string} * Serialized HTML. */ function toHtml(tree, options) { const options_ = options || emptyOptions; const quote = options_.quote || '"'; const alternative = quote === '"' ? "'" : '"'; if (quote !== '"' && quote !== "'") { throw new Error("Invalid quote `" + quote + "`, expected `'` or `\"`"); } /** @type {State} */ const state = { one, all, settings: { omitOptionalTags: options_.omitOptionalTags || false, allowParseErrors: options_.allowParseErrors || false, allowDangerousCharacters: options_.allowDangerousCharacters || false, quoteSmart: options_.quoteSmart || false, preferUnquoted: options_.preferUnquoted || false, tightAttributes: options_.tightAttributes || false, upperDoctype: options_.upperDoctype || false, tightDoctype: options_.tightDoctype || false, bogusComments: options_.bogusComments || false, tightCommaSeparatedLists: options_.tightCommaSeparatedLists || false, tightSelfClosing: options_.tightSelfClosing || false, collapseEmptyAttributes: options_.collapseEmptyAttributes || false, allowDangerousHtml: options_.allowDangerousHtml || false, voids: options_.voids || htmlVoidElements, characterReferences: options_.characterReferences || emptyCharacterReferences, closeSelfClosing: options_.closeSelfClosing || false, closeEmptyElements: options_.closeEmptyElements || false, }, schema: options_.space === "svg" ? svg : html$4, quote, alternative, }; return state.one(Array.isArray(tree) ? { type: "root", children: tree } : tree, undefined, undefined); } /** * Serialize a node. * * @this {State} * Info passed around about the current state. * @param {Nodes} node * Node to handle. * @param {number | undefined} index * Index of `node` in `parent. * @param {Parents | undefined} parent * Parent of `node`. * @returns {string} * Serialized node. */ function one(node, index, parent) { return handle(node, index, parent, this); } /** * Serialize all children of `parent`. * * @this {State} * Info passed around about the current state. * @param {Parents | undefined} parent * Parent whose children to serialize. * @returns {string} */ function all(parent) { /** @type {Array} */ const results = []; const children = (parent && parent.children) || emptyChildren; let index = -1; while (++index < children.length) { results[index] = this.one(children[index], index, parent); } return results.join(""); } /** * @import {Root} from 'hast' * @import {Options} from 'hast-util-to-html' * @import {Compiler, Processor} from 'unified' */ /** * Plugin to add support for serializing as HTML. * * @param {Options | null | undefined} [options] * Configuration (optional). * @returns {undefined} * Nothing. */ function rehypeStringify(options) { /** @type {Processor} */ // @ts-expect-error: TS in JSDoc generates wrong types if `this` is typed regularly. const self = this; const settings = { ...self.data("settings"), ...options }; self.compiler = compiler; /** * @type {Compiler} */ function compiler(tree) { return toHtml(tree, settings); } } var Fe = Object.defineProperty; var Ve = (o, e, t) => (e in o ? Fe(o, e, { enumerable: true, configurable: true, writable: true, value: t }) : (o[e] = t)); var k = (o, e, t) => Ve(o, typeof e != "symbol" ? e + "" : e, t); function Zt(o, e, t, n = "before") { const s = typeof t == "string" ? t : t.id, r = ht(o), i = e.map((d) => bt$1(d, r)), l = Bt$1(s, o.doc); if (!l) throw new Error(`Block with ID ${s} not found`); let a = l.posBeforeNode; return (n === "after" && (a += l.node.nodeSize), o.step(new ReplaceStep(a, a, new Slice(Fragment.from(i), 0, 0))), i.map((d) => L$2(d, r))); } function Q(o) { if (!o || o.type.name !== "column") throw new Error("Invalid columnPos: does not point to column node."); const e = o.firstChild; if (!e) throw new Error("Invalid column: does not have child node."); const t = e.firstChild; if (!t) throw new Error("Invalid blockContainer: does not have child node."); return o.childCount === 1 && e.childCount === 1 && t.type.name === "paragraph" && t.content.content.length === 0; } function eo(o, e) { const t = o.doc.resolve(e), n = t.nodeAfter; if (!n || n.type.name !== "columnList") throw new Error("Invalid columnListPos: does not point to columnList node."); for (let s = n.childCount - 1; s >= 0; s--) { const r = o.doc.resolve(t.pos + 1).posAtIndex(s), l = o.doc.resolve(r).nodeAfter; if (!l || l.type.name !== "column") throw new Error("Invalid columnPos: does not point to column node."); Q(l) && o.delete(r, r + l.nodeSize); } } function H(o, e) { eo(o, e); const n = o.doc.resolve(e).nodeAfter; if (!n || n.type.name !== "columnList") throw new Error("Invalid columnListPos: does not point to columnList node."); if (n.childCount > 2) return; if (n.childCount < 2) throw new Error("Invalid columnList: contains fewer than two children."); const s = e + 1, i = o.doc.resolve(s).nodeAfter, l = e + n.nodeSize - 1, c = o.doc.resolve(l).nodeBefore; if (!i || !c) throw new Error("Invalid columnList: does not contain children."); const d = Q(i), u = Q(c); if (d && u) { o.delete(e, e + n.nodeSize); return; } if (d) { o.step( new ReplaceAroundStep( // Replaces `columnList`. e, e + n.nodeSize, // Replaces with content of last `column`. l - c.nodeSize + 1, l - 1, // Doesn't append anything. Slice.empty, 0, false, ), ); return; } if (u) { o.step( new ReplaceAroundStep( // Replaces `columnList`. e, e + n.nodeSize, // Replaces with content of first `column`. s + 1, s + i.nodeSize - 1, // Doesn't append anything. Slice.empty, 0, false, ), ); return; } } function ae(o, e, t) { const n = ht(o), s = t.map((u) => bt$1(u, n)), r = new Set(e.map((u) => (typeof u == "string" ? u : u.id))), i = [], l = /* @__PURE__ */ new Set(), a = typeof e[0] == "string" ? e[0] : e[0].id; let c = 0; if ( (o.doc.descendants((u, p) => { if (r.size === 0) return false; if (!u.type.isInGroup("bnBlock") || !r.has(u.attrs.id)) return true; if ((i.push(L$2(u, n)), r.delete(u.attrs.id), t.length > 0 && u.attrs.id === a)) { const b = o.doc.nodeSize; o.insert(p, s); const g = o.doc.nodeSize; c += b - g; } const h = o.doc.nodeSize, f = o.doc.resolve(p - c); (f.node().type.name === "column" ? l.add(f.before(-1)) : f.node().type.name === "columnList" && l.add(f.before()), f.node().type.name === "blockGroup" && f.node(f.depth - 1).type.name !== "doc" && f.node().childCount === 1 ? o.delete(f.before(), f.after()) : o.delete(p - c, p - c + u.nodeSize)); const m = o.doc.nodeSize; return ((c += h - m), false); }), r.size > 0) ) { const u = [...r].join(` `); throw Error("Blocks with the following IDs could not be found in the editor: " + u); } return (l.forEach((u) => H(o, u)), { insertedBlocks: s.map((u) => L$2(u, n)), removedBlocks: i }); } function to(o, e, t, n, s) { let r; if (e) if (typeof e == "string") r = T$1([e], o.pmSchema, n); else if (Array.isArray(e)) r = T$1(e, o.pmSchema, n); else if (e.type === "tableContent") r = kt$1(e, o.pmSchema); else throw new O(e.type); else throw new Error("blockContent is required"); const l = ((s == null ? void 0 : s.document) ?? document).createDocumentFragment(); for (const a of r) if (a.type.name !== "text" && o.schema.inlineContentSchema[a.type.name]) { const c = o.schema.inlineContentSpecs[a.type.name].implementation; if (c) { const d = wt$1(a, o.schema.inlineContentSchema, o.schema.styleSchema), u = c.render.call( { renderType: "dom", props: void 0, }, d, () => {}, o, ); if (u) { if ((l.appendChild(u.dom), u.contentDOM)) { const p = t.serializeFragment(a.content, s); ((u.contentDOM.dataset.editable = ""), u.contentDOM.appendChild(p)); } continue; } } } else if (a.type.name === "text") { let c = document.createTextNode(a.textContent); for (const d of a.marks.toReversed()) if (d.type.name in o.schema.styleSpecs) { const u = o.schema.styleSpecs[d.type.name].implementation.render(d.attrs.stringValue, o); (u.contentDOM.appendChild(c), (c = u.dom)); } else { const u = d.type.spec.toDOM(d, true), p = DOMSerializer.renderSpec(document, u); (p.contentDOM.appendChild(c), (c = p.dom)); } l.appendChild(c); } else { const c = t.serializeFragment(Fragment.from([a]), s); l.appendChild(c); } return l; } function oo(o, e, t, n) { var u, p, h, f, m; const s = o.pmSchema.nodes.blockContainer, r = e.props || {}; for (const [b, g] of Object.entries(o.schema.blockSchema[e.type].propSchema)) !(b in r) && g.default !== void 0 && (r[b] = g.default); const i = e.children || [], a = o.blockImplementations[e.type].implementation.render.call( { renderType: "dom", props: void 0, }, { ...e, props: r, children: i }, o, ); if (a.contentDOM && e.content) { const b = to( o, e.content, // TODO t, e.type, n, ); a.contentDOM.appendChild(b); } if (o.pmSchema.nodes[e.type].isInGroup("bnBlock")) { if (e.children && e.children.length > 0) { const b = Se(o, e.children, t, n); (u = a.contentDOM) == null || u.append(b); } return a.dom; } const d = (h = (p = s.spec) == null ? void 0 : p.toDOM) == null ? void 0 : h.call( p, s.create({ id: e.id, ...r, }), ); return ((f = d.contentDOM) == null || f.appendChild(a.dom), e.children && e.children.length > 0 && ((m = d.contentDOM) == null || m.appendChild(xe(o, e.children, t, n))), d.dom); } function Se(o, e, t, n) { const r = ((n == null ? void 0 : n.document) ?? document).createDocumentFragment(); for (const i of e) { const l = oo(o, i, t, n); r.appendChild(l); } return r; } const xe = (o, e, t, n) => { var l; const s = o.pmSchema.nodes.blockGroup, r = s.spec.toDOM(s.create({})), i = Se(o, e, t, n); return ((l = r.contentDOM) == null || l.appendChild(i), r.dom); }, no = (o) => ( o.querySelectorAll('[data-content-type="numberedListItem"]').forEach((t) => { var s, r; const n = (r = (s = t.closest(".bn-block-outer")) == null ? void 0 : s.previousElementSibling) == null ? void 0 : r.querySelector('[data-content-type="numberedListItem"]'); if (!n) t.setAttribute("data-index", t.getAttribute("data-start") || "1"); else { const i = n.getAttribute("data-index"); t.setAttribute("data-index", (parseInt(i || "0") + 1).toString()); } }), o ), so = (o) => ( o.querySelectorAll('[data-content-type="checkListItem"] input').forEach((t) => { t.disabled = true; }), o ), ro = (o) => ( o.querySelectorAll('.bn-toggle-wrapper[data-show-children="false"]').forEach((t) => { t.setAttribute("data-show-children", "true"); }), o ), io = (o) => ( o.querySelectorAll('[data-content-type="table"] table').forEach((t) => { (t.setAttribute("style", `--default-cell-min-width: ${Oe$2}px;`), t.setAttribute("data-show-children", "true")); }), o ), ao = (o) => ( o.querySelectorAll('[data-content-type="table"] table').forEach((t) => { var r; const n = document.createElement("div"); n.className = "tableWrapper"; const s = document.createElement("div"); ((s.className = "tableWrapper-inner"), n.appendChild(s), (r = t.parentElement) == null || r.appendChild(n), n.appendChild(t)); }), o ), co = (o) => ( o.querySelectorAll(".bn-inline-content:empty").forEach((t) => { const n = document.createElement("span"); ((n.className = "ProseMirror-trailingBreak"), n.setAttribute("style", "display: inline-block;"), t.appendChild(n)); }), o ), lo = (o, e) => { const t = DOMSerializer.fromSchema(o), n = [no, so, ro, io, ao, co]; return { serializeBlocks: (s, r) => { let i = xe(e, s, t, r); for (const l of n) i = l(i); return i.outerHTML; }, }; }; function uo(o) { return o.transact((e) => { const t = Y$1(e.doc, e.selection.anchor); if (e.selection instanceof CellSelection) return { type: "cell", anchorBlockId: t.node.attrs.id, anchorCellOffset: e.selection.$anchorCell.pos - t.posBeforeNode, headCellOffset: e.selection.$headCell.pos - t.posBeforeNode, }; if (e.selection instanceof NodeSelection) return { type: "node", anchorBlockId: t.node.attrs.id, }; { const n = Y$1(e.doc, e.selection.head); return { type: "text", anchorBlockId: t.node.attrs.id, headBlockId: n.node.attrs.id, anchorOffset: e.selection.anchor - t.posBeforeNode, headOffset: e.selection.head - n.posBeforeNode, }; } }); } function po(o, e) { var s, r; const t = (s = Bt$1(e.anchorBlockId, o.doc)) == null ? void 0 : s.posBeforeNode; if (t === void 0) throw new Error(`Could not find block with ID ${e.anchorBlockId} to update selection`); let n; if (e.type === "cell") n = CellSelection.create(o.doc, t + e.anchorCellOffset, t + e.headCellOffset); else if (e.type === "node") n = NodeSelection.create(o.doc, t + 1); else { const i = (r = Bt$1(e.headBlockId, o.doc)) == null ? void 0 : r.posBeforeNode; if (i === void 0) throw new Error(`Could not find block with ID ${e.headBlockId} to update selection`); n = TextSelection.create(o.doc, t + e.anchorOffset, i + e.headOffset); } o.setSelection(n); } function X(o) { return o .map((e) => e.type === "columnList" ? e.children.map((t) => X(t.children)).flat() : { ...e, children: X(e.children), }, ) .flat(); } function Ee(o, e, t) { o.transact((n) => { var i; const s = ((i = o.getSelection()) == null ? void 0 : i.blocks) || [o.getTextCursorPosition().block], r = uo(o); (o.removeBlocks(s), o.insertBlocks(X(s), e, t), po(n, r)); }); } function Pe(o) { return !o || o.type !== "columnList"; } function Te(o, e, t) { let n, s; if ( (e ? e.children.length > 0 ? ((n = e.children[e.children.length - 1]), (s = "after")) : ((n = e), (s = "before")) : t && ((n = t), (s = "before")), !n || !s) ) return; const r = o.getParentBlock(n); return Pe(r) ? { referenceBlock: n, placement: s } : Te(o, s === "after" ? n : o.getPrevBlock(n), r); } function we(o, e, t) { let n, s; if ( (e ? e.children.length > 0 ? ((n = e.children[0]), (s = "before")) : ((n = e), (s = "after")) : t && ((n = t), (s = "after")), !n || !s) ) return; const r = o.getParentBlock(n); return Pe(r) ? { referenceBlock: n, placement: s } : we(o, s === "before" ? n : o.getNextBlock(n), r); } function fo(o) { o.transact(() => { const e = o.getSelection(), t = (e == null ? void 0 : e.blocks[0]) || o.getTextCursorPosition().block, n = Te(o, o.getPrevBlock(t), o.getParentBlock(t)); n && Ee(o, n.referenceBlock, n.placement); }); } function ho(o) { o.transact(() => { const e = o.getSelection(), t = (e == null ? void 0 : e.blocks[(e == null ? void 0 : e.blocks.length) - 1]) || o.getTextCursorPosition().block, n = we(o, o.getNextBlock(t), o.getParentBlock(t)); n && Ee(o, n.referenceBlock, n.placement); }); } function mo(o, e, t) { const { $from: n, $to: s } = o.selection, r = n.blockRange( s, (f) => f.childCount > 0 && (f.type.name === "blockGroup" || f.type.name === "column"), // change necessary to not look at first item child type ); if (!r) return false; const i = r.startIndex; if (i === 0) return false; const a = r.parent.child(i - 1); if (a.type !== e) return false; const c = a.lastChild && a.lastChild.type === t, d = Fragment.from(c ? e.create() : null), u = new Slice( Fragment.from( e.create(null, Fragment.from(t.create(null, d))), // change necessary to create "groupType" instead of parent.type ), c ? 3 : 1, 0, ), p = r.start, h = r.end; return (o.step(new ReplaceAroundStep(p - (c ? 3 : 1), h, p, h, u, 1, true)).scrollIntoView(), true); } function Me(o) { return o.transact((e) => mo(e, o.pmSchema.nodes.blockContainer, o.pmSchema.nodes.blockGroup)); } function ko(o) { o._tiptapEditor.commands.liftListItem("blockContainer"); } function bo(o) { return o.transact((e) => { const { bnBlock: t } = Ot$2(e); return e.doc.resolve(t.beforePos).nodeBefore !== null; }); } function go(o) { return o.transact((e) => { const { bnBlock: t } = Ot$2(e); return e.doc.resolve(t.beforePos).depth > 1; }); } function Bo(o, e) { const t = typeof e == "string" ? e : e.id, n = ht(o), s = Bt$1(t, o); if (s) return L$2(s.node, n); } function yo(o, e) { const t = typeof e == "string" ? e : e.id, n = Bt$1(t, o), s = ht(o); if (!n) return; const i = o.resolve(n.posBeforeNode).nodeBefore; if (i) return L$2(i, s); } function Co(o, e) { const t = typeof e == "string" ? e : e.id, n = Bt$1(t, o), s = ht(o); if (!n) return; const i = o.resolve(n.posBeforeNode + n.node.nodeSize).nodeAfter; if (i) return L$2(i, s); } function So(o, e) { const t = typeof e == "string" ? e : e.id, n = ht(o), s = Bt$1(t, o); if (!s) return; const r = o.resolve(s.posBeforeNode), i = r.node(), l = r.node(-1), a = l.type.name !== "doc" ? i.type.name === "blockGroup" ? l : i : void 0; if (a) return L$2(a, n); } class xo { constructor(e) { this.editor = e; } /** * Gets a snapshot of all top-level (non-nested) blocks in the editor. * @returns A snapshot of all top-level (non-nested) blocks in the editor. */ get document() { return this.editor.transact((e) => St$1(e.doc, this.editor.pmSchema)); } /** * Gets a snapshot of an existing block from the editor. * @param blockIdentifier The identifier of an existing block that should be * retrieved. * @returns The block that matches the identifier, or `undefined` if no * matching block was found. */ getBlock(e) { return this.editor.transact((t) => Bo(t.doc, e)); } /** * Gets a snapshot of the previous sibling of an existing block from the * editor. * @param blockIdentifier The identifier of an existing block for which the * previous sibling should be retrieved. * @returns The previous sibling of the block that matches the identifier. * `undefined` if no matching block was found, or it's the first child/block * in the document. */ getPrevBlock(e) { return this.editor.transact((t) => yo(t.doc, e)); } /** * Gets a snapshot of the next sibling of an existing block from the editor. * @param blockIdentifier The identifier of an existing block for which the * next sibling should be retrieved. * @returns The next sibling of the block that matches the identifier. * `undefined` if no matching block was found, or it's the last child/block in * the document. */ getNextBlock(e) { return this.editor.transact((t) => Co(t.doc, e)); } /** * Gets a snapshot of the parent of an existing block from the editor. * @param blockIdentifier The identifier of an existing block for which the * parent should be retrieved. * @returns The parent of the block that matches the identifier. `undefined` * if no matching block was found, or the block isn't nested. */ getParentBlock(e) { return this.editor.transact((t) => So(t.doc, e)); } /** * Traverses all blocks in the editor depth-first, and executes a callback for each. * @param callback The callback to execute for each block. Returning `false` stops the traversal. * @param reverse Whether the blocks should be traversed in reverse order. */ forEachBlock(e, t = false) { const n = this.document.slice(); t && n.reverse(); function s(r) { for (const i of r) { if (e(i) === false) return false; const l = t ? i.children.slice().reverse() : i.children; if (!s(l)) return false; } return true; } s(n); } /** * Inserts new blocks into the editor. If a block's `id` is undefined, BlockNote generates one automatically. Throws an * error if the reference block could not be found. * @param blocksToInsert An array of partial blocks that should be inserted. * @param referenceBlock An identifier for an existing block, at which the new blocks should be inserted. * @param placement Whether the blocks should be inserted just before, just after, or nested inside the * `referenceBlock`. */ insertBlocks(e, t, n = "before") { return this.editor.transact((s) => Zt(s, e, t, n)); } /** * Updates an existing block in the editor. Since updatedBlock is a PartialBlock object, some fields might not be * defined. These undefined fields are kept as-is from the existing block. Throws an error if the block to update could * not be found. * @param blockToUpdate The block that should be updated. * @param update A partial block which defines how the existing block should be changed. */ updateBlock(e, t) { return this.editor.transact((n) => vo$1(n, e, t)); } /** * Removes existing blocks from the editor. Throws an error if any of the blocks could not be found. * @param blocksToRemove An array of identifiers for existing blocks that should be removed. */ removeBlocks(e) { return this.editor.transact((t) => ae(t, e, []).removedBlocks); } /** * Replaces existing blocks in the editor with new blocks. If the blocks that should be removed are not adjacent or * are at different nesting levels, `blocksToInsert` will be inserted at the position of the first block in * `blocksToRemove`. Throws an error if any of the blocks to remove could not be found. * @param blocksToRemove An array of blocks that should be replaced. * @param blocksToInsert An array of partial blocks to replace the old ones with. */ replaceBlocks(e, t) { return this.editor.transact((n) => ae(n, e, t)); } /** * Checks if the block containing the text cursor can be nested. */ canNestBlock() { return bo(this.editor); } /** * Nests the block containing the text cursor into the block above it. */ nestBlock() { Me(this.editor); } /** * Checks if the block containing the text cursor is nested. */ canUnnestBlock() { return go(this.editor); } /** * Lifts the block containing the text cursor out of its parent. */ unnestBlock() { ko(this.editor); } /** * Moves the selected blocks up. If the previous block has children, moves * them to the end of its children. If there is no previous block, but the * current blocks share a common parent, moves them out of & before it. */ moveBlocksUp() { return fo(this.editor); } /** * Moves the selected blocks down. If the next block has children, moves * them to the start of its children. If there is no next block, but the * current blocks share a common parent, moves them out of & after it. */ moveBlocksDown() { return ho(this.editor); } } class Eo extends f { constructor(e) { (super(), (this.editor = e), e.on("create", () => { (e._tiptapEditor.on("update", ({ transaction: t, appendedTransactions: n }) => { this.emit("onChange", { editor: e, transaction: t, appendedTransactions: n }); }), e._tiptapEditor.on("selectionUpdate", ({ transaction: t }) => { this.emit("onSelectionChange", { editor: e, transaction: t }); }), e._tiptapEditor.on("mount", () => { this.emit("onMount", { editor: e }); }), e._tiptapEditor.on("unmount", () => { this.emit("onUnmount", { editor: e }); })); })); } /** * Register a callback that will be called when the editor changes. */ onChange(e, t = true) { const n = ({ transaction: s, appendedTransactions: r }) => { (!t && ce(s)) || e(this.editor, { getChanges() { return Mt(s, r); }, }); }; return ( this.on("onChange", n), () => { this.off("onChange", n); } ); } /** * Register a callback that will be called when the selection changes. */ onSelectionChange(e, t = false) { const n = (s) => { (!t && ce(s.transaction)) || e(this.editor); }; return ( this.on("onSelectionChange", n), () => { this.off("onSelectionChange", n); } ); } /** * Register a callback that will be called when the editor is mounted. */ onMount(e) { return ( this.on("onMount", e), () => { this.off("onMount", e); } ); } /** * Register a callback that will be called when the editor is unmounted. */ onUnmount(e) { return ( this.on("onUnmount", e), () => { this.off("onUnmount", e); } ); } } function ce(o) { return !!o.getMeta("y-sync$"); } function Po(o) { return Array.prototype.indexOf.call(o.parentElement.childNodes, o); } function To(o) { return o.nodeType === 3 && !/\S/.test(o.nodeValue || ""); } function wo(o) { o.querySelectorAll("li > ul, li > ol").forEach((e) => { const t = Po(e), n = e.parentElement, s = Array.from(n.childNodes).slice(t + 1); (e.remove(), s.forEach((r) => { r.remove(); }), n.insertAdjacentElement("afterend", e), s.reverse().forEach((r) => { if (To(r)) return; const i = document.createElement("li"); (i.append(r), e.insertAdjacentElement("afterend", i)); }), n.childNodes.length === 0 && n.remove()); }); } function Mo(o) { o.querySelectorAll("li + ul, li + ol").forEach((e) => { var r, i; const t = e.previousElementSibling, n = document.createElement("div"); (t.insertAdjacentElement("afterend", n), n.append(t)); const s = document.createElement("div"); for ( s.setAttribute("data-node-type", "blockGroup"), n.append(s); ((r = n.nextElementSibling) == null ? void 0 : r.nodeName) === "UL" || ((i = n.nextElementSibling) == null ? void 0 : i.nodeName) === "OL"; ) s.append(n.nextElementSibling); }); } let le = null; function vo() { return le || (le = document.implementation.createHTMLDocument("title")); } function Io(o) { if (typeof o == "string") { const e = vo().createElement("div"); ((e.innerHTML = o), (o = e)); } return (wo(o), Mo(o), o); } function Ao(o) { const e = o.ownerDocument.createTreeWalker( o, // NodeFilter.SHOW_COMMENT 128, ); let t; for (; (t = e.nextNode()); ) if (/^\s*notionvc:/.test(t.nodeValue || "")) return true; return false; } function No(o) { const e = /* @__PURE__ */ new Set(["PRE", "CODE"]), t = o.ownerDocument.createTreeWalker( o, // NodeFilter.SHOW_TEXT 4, { acceptNode(r) { let i = r.parentElement; for (; i && i !== o; ) { if (e.has(i.tagName)) return 2; i = i.parentElement; } return 1; }, }, ), n = []; let s; for (; (s = t.nextNode()); ) n.push(s); for (const r of n) r.nodeValue && /[\r\n]/.test(r.nodeValue) && (r.nodeValue = r.nodeValue.replace(/[ \t\r\n\f]+/g, " ")); } function Lo(o) { Ao(o) || No(o); } function ve(o, e) { const t = Io(o); Lo(t); const s = DOMParser$1.fromSchema(e).parse(t, { topNode: e.nodes.blockGroup.create(), }), r = []; for (let i = 0; i < s.childCount; i++) r.push(L$2(s.child(i), e)); return r; } function _o(o, e) { const t = e.value ? e.value : "", n = {}; e.lang && (n["data-language"] = e.lang); let s = { type: "element", tagName: "code", properties: n, children: [{ type: "text", value: t }], }; return ( e.meta && (s.data = { meta: e.meta }), o.patch(e, s), (s = o.applyData(e, s)), (s = { type: "element", tagName: "pre", properties: {}, children: [s], }), o.patch(e, s), s ); } function Do(o, e) { var r; const t = String((e == null ? void 0 : e.url) || ""), n = e != null && e.title ? String(e.title) : void 0; let s = { type: "element", tagName: "video", properties: { "src": t, "data-name": n, "data-url": t, "controls": true, }, children: [], }; return ((r = o.patch) == null || r.call(o, e, s), (s = o.applyData ? o.applyData(e, s) : s), s); } function Ie(o) { return unified() .use(remarkParse) .use(remarkGfm) .use(remarkRehype, { handlers: { ...handlers, image: (t, n) => { const s = String((n == null ? void 0 : n.url) || ""); return mo$1(s) ? Do(t, n) : handlers.image(t, n); }, code: _o, blockquote: (t, n) => { const s = { type: "element", tagName: "blockquote", properties: {}, // The only difference from the original is that we don't wrap the children with line endings children: t.wrap(t.all(n), false), }; return (t.patch(n, s), t.applyData(n, s)); }, }, }) .use(rehypeStringify) .processSync(o).value; } function Oo(o, e) { const t = Ie(o); return ve(t, e); } class $o { constructor(e) { this.editor = e; } /** * Exports blocks into a simplified HTML string. To better conform to HTML standards, children of blocks which aren't list * items are un-nested in the output HTML. * * @param blocks An array of blocks that should be serialized into HTML. * @returns The blocks, serialized as an HTML string. */ blocksToHTMLLossy(e = this.editor.document) { return Be(this.editor.pmSchema, this.editor).exportBlocks(e, {}); } /** * Serializes blocks into an HTML string in the format that would normally be rendered by the editor. * * Use this method if you want to server-side render HTML (for example, a blog post that has been edited in BlockNote) * and serve it to users without loading the editor on the client (i.e.: displaying the blog post) * * @param blocks An array of blocks that should be serialized into HTML. * @returns The blocks, serialized as an HTML string. */ blocksToFullHTML(e = this.editor.document) { return lo(this.editor.pmSchema, this.editor).serializeBlocks(e, {}); } /** * Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and * `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote * doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text. * @param html The HTML string to parse blocks from. * @returns The blocks parsed from the HTML string. */ tryParseHTMLToBlocks(e) { return ve(e, this.editor.pmSchema); } /** * Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of * BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed. * @param blocks An array of blocks that should be serialized into Markdown. * @returns The blocks, serialized as a Markdown string. */ blocksToMarkdownLossy(e = this.editor.document) { return Uo$1(e, this.editor.pmSchema, this.editor, {}); } /** * Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on * Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it * as text. * @param markdown The Markdown string to parse blocks from. * @returns The blocks parsed from the Markdown string. */ tryParseMarkdownToBlocks(e) { return Oo(e, this.editor.pmSchema); } /** * Paste HTML into the editor. Defaults to converting HTML to BlockNote HTML. * @param html The HTML to paste. * @param raw Whether to paste the HTML as is, or to convert it to BlockNote HTML. */ pasteHTML(e, t = false) { var s; let n = e; if (!t) { const r = this.tryParseHTMLToBlocks(e); n = this.blocksToFullHTML(r); } n && ((s = this.editor.prosemirrorView) == null || s.pasteHTML(n)); } /** * Paste text into the editor. Defaults to interpreting text as markdown. * @param text The text to paste. */ pasteText(e) { var t; return (t = this.editor.prosemirrorView) == null ? void 0 : t.pasteText(e); } /** * Paste markdown into the editor. * @param markdown The markdown to paste. */ pasteMarkdown(e) { const t = Ie(e); return this.pasteHTML(t); } } const se = ["vscode-editor-data", "blocknote/html", "text/markdown", "text/html", "text/plain", "Files"]; function Ho(o, e) { if (!o.startsWith(".") || !e.startsWith(".")) throw new Error("The strings provided are not valid file extensions."); return o === e; } function Fo(o, e) { const t = o.split("/"), n = e.split("/"); if (t.length !== 2) throw new Error(`The string ${o} is not a valid MIME type.`); if (n.length !== 2) throw new Error(`The string ${e} is not a valid MIME type.`); return t[1] === "*" || n[1] === "*" ? t[0] === n[0] : (t[0] === "*" || n[0] === "*" || t[0] === n[0]) && t[1] === n[1]; } function de(o, e, t, n = "after") { let s; return (Array.isArray(e.content) && e.content.length === 0 ? (s = o.updateBlock(e, t).id) : (s = o.insertBlocks([t], e, n)[0].id), s); } async function Ae(o, e) { var r; if (!e.uploadFile) { console.warn("Attempted ot insert file, but uploadFile is not set in the BlockNote editor options"); return; } const t = "dataTransfer" in o ? o.dataTransfer : o.clipboardData; if (t === null) return; let n = null; for (const i of se) if (t.types.includes(i)) { n = i; break; } if (n !== "Files") return; const s = t.items; if (s) { o.preventDefault(); for (let i = 0; i < s.length; i++) { let l = "file"; for (const c of Object.values(e.schema.blockSpecs)) for (const d of ((r = c.implementation.meta) == null ? void 0 : r.fileBlockAccept) || []) { const u = d.startsWith("."), p = s[i].getAsFile(); if (p && ((!u && p.type && Fo(s[i].type, d)) || (u && Ho("." + p.name.split(".").pop(), d)))) { l = c.config.type; break; } } const a = s[i].getAsFile(); if (a) { const c = { type: l, props: { name: a.name, }, }; let d; if (o.type === "paste") { const h = e.getTextCursorPosition().block; d = de(e, h, c); } else if (o.type === "drop") { const h = { left: o.clientX, top: o.clientY, }, f = e.prosemirrorView.posAtCoords(h); if (!f) return; d = e.transact((m) => { var y; const b = Y$1(m.doc, f.pos), g = (y = e.domElement) == null ? void 0 : y.querySelector(`[data-id="${b.node.attrs.id}"]`), x = g == null ? void 0 : g.getBoundingClientRect(); return de(e, e.getBlock(b.node.attrs.id), c, x && (x.top + x.bottom) / 2 > h.top ? "before" : "after"); }); } else return; const u = await e.uploadFile(a, d), p = typeof u == "string" ? { props: { url: u, }, } : { ...u }; e.updateBlock(d, p); } } } } const Vo = (o) => Extension.create({ name: "dropFile", addProseMirrorPlugins() { return [ new Plugin({ props: { handleDOMEvents: { drop(e, t) { if (!o.isEditable) return; let n = null; for (const s of se) if (t.dataTransfer.types.includes(s)) { n = s; break; } return ( n === null ? true : n === "Files" ? (Ae(t, o), true) : false ); }, }, }, }), ]; }, }), Uo = /(^|\n) {0,3}#{1,6} {1,8}[^\n]{1,64}\r?\n\r?\n\s{0,32}\S/, zo = /(_|__|\*|\*\*|~~|==|\+\+)(?!\s)(?:[^\s](?:.{0,62}[^\s])?|\S)(?=\1)/, Ro = /\[[^\]]{1,128}\]\(https?:\/\/\S{1,999}\)/, Go = /(?:\s|^)`(?!\s)(?:[^\s`](?:[^`]{0,46}[^\s`])?|[^\s`])`([^\w]|$)/, Wo = /(?:^|\n)\s{0,5}-\s{1}[^\n]+\n\s{0,15}-\s/, jo = /(?:^|\n)\s{0,5}\d+\.\s{1}[^\n]+\n\s{0,15}\d+\.\s/, qo = /\n{2} {0,3}-{2,48}\n{2}/, Ko = /(?:\n|^)(```|~~~|\$\$)(?!`|~)[^\s]{0,64} {0,64}[^\n]{0,64}\n[\s\S]{0,9999}?\s*\1 {0,64}(?:\n+|$)/, Jo = /(?:\n|^)(?!\s)\w[^\n]{0,64}\r?\n(-|=)\1{0,64}\n\n\s{0,64}(\w|$)/, Yo = /(?:^|(\r?\n\r?\n))( {0,3}>[^\n]{1,333}\n){1,999}($|(\r?\n))/, Qo = /^\s*\|(.+\|)+\s*$/m, Xo = /^\s*\|(\s*[-:]+[-:]\s*\|)+\s*$/m, Zo = /^\s*\|(.+\|)+\s*$/m, en = (o) => Uo.test(o) || zo.test(o) || Ro.test(o) || Go.test(o) || Wo.test(o) || jo.test(o) || qo.test(o) || Ko.test(o) || Jo.test(o) || Yo.test(o) || Qo.test(o) || Xo.test(o) || Zo.test(o); async function tn(o, e) { const { schema: t } = e.state; if (!o.clipboardData) return false; const n = o.clipboardData.getData("text/plain"); if (!n) return false; if (!t.nodes.codeBlock) return (e.pasteText(n), true); const s = o.clipboardData.getData("vscode-editor-data"), r = s ? JSON.parse(s) : void 0, i = r == null ? void 0 : r.mode; return i ? (e.pasteHTML( `
${n.replace(
					/\r\n?/g,
					`
`,
				)}
`, ), true) : false; } function on({ event: o, editor: e, prioritizeMarkdownOverHTML: t, plainTextAsMarkdown: n }) { var l; if (e.transact((a) => a.selection.$from.parent.type.spec.code && a.selection.$to.parent.type.spec.code)) { const a = (l = o.clipboardData) == null ? void 0 : l.getData("text/plain"); if (a) return (e.pasteText(a), true); } let r; for (const a of se) if (o.clipboardData.types.includes(a)) { r = a; break; } if (!r) return true; if (r === "vscode-editor-data") return (tn(o, e.prosemirrorView), true); if (r === "Files") return (Ae(o, e), true); const i = o.clipboardData.getData(r); if (r === "blocknote/html") return (e.pasteHTML(i, true), true); if (r === "text/markdown") return (e.pasteMarkdown(i), true); if (t) { const a = o.clipboardData.getData("text/plain"); if (en(a)) return (e.pasteMarkdown(a), true); } return ( r === "text/html" ? (e.pasteHTML(i), true) : n ? (e.pasteMarkdown(i), true) : (e.pasteText(i), true) ); } const nn = (o, e) => Extension.create({ name: "pasteFromClipboard", addProseMirrorPlugins() { return [ new Plugin({ props: { handleDOMEvents: { paste(t, n) { if ((n.preventDefault(), !!o.isEditable)) return e({ event: n, editor: o, defaultPasteHandler: ({ prioritizeMarkdownOverHTML: s = true, plainTextAsMarkdown: r = true } = {}) => on({ event: n, editor: o, prioritizeMarkdownOverHTML: s, plainTextAsMarkdown: r, }), }); }, }, }, }), ]; }, }); function sn(o, e, t) { var l; let n = false; const s = o.state.selection instanceof CellSelection; if (!s) { const a = o.state.doc.slice(o.state.selection.from, o.state.selection.to, false).content, c = []; for (let d = 0; d < a.childCount; d++) c.push(a.child(d)); ((n = c.find((d) => d.type.isInGroup("bnBlock") || d.type.name === "blockGroup" || d.type.spec.group === "blockContent") === void 0), n && (e = a)); } let r; const i = Be(o.state.schema, t); if (s) { ((l = e.firstChild) == null ? void 0 : l.type.name) === "table" && (e = e.firstChild.content); const a = gt(e, t.schema.inlineContentSchema, t.schema.styleSchema); r = `
${i.exportInlineContent(a, {})}
`; } else if (n) { const a = F$2(e, t.schema.inlineContentSchema, t.schema.styleSchema); r = i.exportInlineContent(a, {}); } else { const a = Wt(e); r = i.exportBlocks(a, {}); } return r; } function Ne(o, e) { "node" in o.state.selection && o.state.selection.node.type.spec.group === "blockContent" && e.transact((i) => i.setSelection(new NodeSelection(i.doc.resolve(o.state.selection.from - 1)))); const t = o.serializeForClipboard(o.state.selection.content()).dom.innerHTML, n = o.state.selection.content().content, s = sn(o, n, e), r = Oe$1(s); return { clipboardHTML: t, externalHTML: s, markdown: r }; } const ue = () => { const o = window.getSelection(); if (!o || o.isCollapsed) return true; let e = o.focusNode; for (; e; ) { if (e instanceof HTMLElement && e.getAttribute("contenteditable") === "false") return true; e = e.parentElement; } return false; }, pe = (o, e, t) => { (t.preventDefault(), t.clipboardData.clearData()); const { clipboardHTML: n, externalHTML: s, markdown: r } = Ne(e, o); (t.clipboardData.setData("blocknote/html", n), t.clipboardData.setData("text/html", s), t.clipboardData.setData("text/plain", r)); }, rn = (o) => Extension.create({ name: "copyToClipboard", addProseMirrorPlugins() { return [ new Plugin({ props: { handleDOMEvents: { copy(e, t) { return (ue() || pe(o, e, t), true); }, cut(e, t) { return (ue() || (pe(o, e, t), e.editable && e.dispatch(e.state.tr.deleteSelection())), true); }, // This is for the use-case in which only a block without content // is selected, e.g. an image block, and dragged (not using the // drag handle). dragstart(e, t) { if (!("node" in e.state.selection) || e.state.selection.node.type.spec.group !== "blockContent") return; (o.transact((i) => i.setSelection(new NodeSelection(i.doc.resolve(e.state.selection.from - 1)))), t.preventDefault(), t.dataTransfer.clearData()); const { clipboardHTML: n, externalHTML: s, markdown: r } = Ne(e, o); return (t.dataTransfer.setData("blocknote/html", n), t.dataTransfer.setData("text/html", s), t.dataTransfer.setData("text/plain", r), true); }, }, }, }), ]; }, }), an = Extension.create({ name: "blockBackgroundColor", addGlobalAttributes() { return [ { types: ["tableCell", "tableHeader"], attributes: { backgroundColor: So$1(), }, }, ]; }, }), cn = Node3.create({ name: "hardBreak", inline: true, group: "inline", selectable: false, linebreakReplacement: true, priority: 10, parseHTML() { return [{ tag: "br" }]; }, renderHTML({ HTMLAttributes: o }) { return ["br", mergeAttributes(this.options.HTMLAttributes, o)]; }, renderText() { return ` `; }, }), Le = (o, e) => { var l; const t = o.resolve(e), n = t.depth - 1, s = t.before(n), r = o.resolve(s).nodeAfter; return ( r ? (l = r.type.spec.group) != null && l.includes("bnBlock") ? It$1(o.resolve(s)) : Le(o, s) : void 0 ); }, _ = (o, e) => { const t = o.resolve(e), n = t.index(); if (n === 0) return; const s = t.posAtIndex(n - 1); return It$1(o.resolve(s)); }, L = (o, e) => { const t = o.resolve(e), n = t.index(); if (n === t.node().childCount - 1) return; const s = t.posAtIndex(n + 1); return It$1(o.resolve(s)); }, _e = (o, e) => { for (; e.childContainer; ) { const t = e.childContainer.node, n = o.resolve(e.childContainer.beforePos + 1).posAtIndex(t.childCount - 1); e = It$1(o.resolve(n)); } return e; }, ln = (o, e) => o.isBlockContainer && o.blockContent.node.type.spec.content === "inline*" && o.blockContent.node.childCount > 0 && e.isBlockContainer && e.blockContent.node.type.spec.content === "inline*", dn = (o, e, t, n) => { if (!n.isBlockContainer) throw new Error(`Attempted to merge block at position ${n.bnBlock.beforePos} into previous block at position ${t.bnBlock.beforePos}, but next block is not a block container`); if (n.childContainer) { const s = o.doc.resolve(n.childContainer.beforePos + 1), r = o.doc.resolve(n.childContainer.afterPos - 1), i = s.blockRange(r); if (e) { const l = o.doc.resolve(n.bnBlock.beforePos); o.tr.lift(i, l.depth); } } if (e) { if (!t.isBlockContainer) throw new Error(`Attempted to merge block at position ${n.bnBlock.beforePos} into previous block at position ${t.bnBlock.beforePos}, but previous block is not a block container`); e(o.tr.delete(t.blockContent.afterPos - 1, n.blockContent.beforePos + 1)); } return true; }, fe = (o) => ({ state: e, dispatch: t }) => { const n = e.doc.resolve(o), s = It$1(n), r = _(e.doc, s.bnBlock.beforePos); if (!r) return false; const i = _e(e.doc, r); return ln(i, s) ? dn(e, t, i, s) : false; }, un = Extension.create({ priority: 50, // TODO: The shortcuts need a refactor. Do we want to use a command priority // design as there is now, or clump the logic into a single function? addKeyboardShortcuts() { const o = () => this.editor.commands.first(({ chain: n, commands: s }) => [ // Deletes the selection if it's not empty. () => s.deleteSelection(), // Undoes an input rule if one was triggered in the last editor state change. () => s.undoInputRule(), // Reverts block content type to a paragraph if the selection is at the start of the block. () => s.command(({ state: r }) => { const i = Tt$2(r); if (!i.isBlockContainer) return false; const l = r.selection.from === i.blockContent.beforePos + 1, a = i.blockContent.node.type.name === "paragraph"; return l && !a ? s.command( yo$1(i.bnBlock.beforePos, { type: "paragraph", props: {}, }), ) : false; }), // Removes a level of nesting if the block is indented if the selection is at the start of the block. () => s.command(({ state: r }) => { const i = Tt$2(r); if (!i.isBlockContainer) return false; const { blockContent: l } = i; return r.selection.from === l.beforePos + 1 ? s.liftListItem("blockContainer") : false; }), // Merges block with the previous one if it isn't indented, and the selection is at the start of the // block. The target block for merging must contain inline content. () => s.command(({ state: r }) => { const i = Tt$2(r); if (!i.isBlockContainer) return false; const { bnBlock: l, blockContent: a } = i, c = _(r.doc, i.bnBlock.beforePos); if (!c || !c.isBlockContainer || c.blockContent.node.type.spec.content !== "inline*") return false; const d = r.selection.from === a.beforePos + 1, u = r.selection.empty, p = l.beforePos; return d && u ? n().command(fe(p)).scrollIntoView().run() : false; }), // If the previous block is a columnList, moves the current block to // the end of the last column in it. () => s.command(({ state: r, tr: i, dispatch: l }) => { const a = Tt$2(r); if (!a.isBlockContainer) return false; const c = _(r.doc, a.bnBlock.beforePos); if (!c || c.isBlockContainer) return false; if (l) { const d = c.bnBlock.afterPos - 1, u = i.doc.resolve(d - 1); return (i.delete(a.bnBlock.beforePos, a.bnBlock.afterPos), i.insert(u.pos, a.bnBlock.node), i.setSelection(TextSelection.near(i.doc.resolve(u.pos + 1))), true); } return false; }), // If the block is the first in a column, moves it to the end of the // previous column. If there is no previous column, moves it above the // columnList. () => s.command(({ state: r, tr: i, dispatch: l }) => { const a = Tt$2(r); if (!a.isBlockContainer || !(i.selection.from === a.blockContent.beforePos + 1)) return false; const d = i.doc.resolve(a.bnBlock.beforePos); if (d.nodeBefore || d.node().type.name !== "column") return false; const h = i.doc.resolve(a.bnBlock.beforePos), f = i.doc.resolve(h.before()), m = f.before(); return ( l && (i.delete(a.bnBlock.beforePos, a.bnBlock.afterPos), H(i, m), f.pos === m + 1 ? (i.insert(m, a.bnBlock.node), i.setSelection(TextSelection.near(i.doc.resolve(m)))) : (i.insert(f.pos - 1, a.bnBlock.node), i.setSelection(TextSelection.near(i.doc.resolve(f.pos))))), true ); }), // Deletes the current block if it's an empty block with inline content, // and moves the selection to the previous block. () => s.command(({ state: r }) => { var a; const i = Tt$2(r); if (!i.isBlockContainer) return false; if (i.blockContent.node.childCount === 0 && i.blockContent.node.type.spec.content === "inline*") { const c = _(r.doc, i.bnBlock.beforePos); if (!c || !c.isBlockContainer) return false; let d = n(); if ( (i.childContainer && d.insertContentAt(i.bnBlock.afterPos, (a = i.childContainer) == null ? void 0 : a.node.content), c.blockContent.node.type.spec.content === "tableRow+") ) { const m = i.bnBlock.beforePos - 1 - 1 - 1 - 1 - 1; d = d.setTextSelection(m); } else if (c.blockContent.node.type.spec.content === "") d = d.setNodeSelection(c.blockContent.beforePos); else { const u = c.blockContent.afterPos - 1; d = d.setTextSelection(u); } return d .deleteRange({ from: i.bnBlock.beforePos, to: i.bnBlock.afterPos, }) .scrollIntoView() .run(); } return false; }), // Deletes previous block if it contains no content and isn't a table, // when the selection is empty and at the start of the block. Moves the // current block into the deleted block's place. () => s.command(({ state: r }) => { const i = Tt$2(r); if (!i.isBlockContainer) return false; const l = r.selection.from === i.blockContent.beforePos + 1, a = r.selection.empty, c = _(r.doc, i.bnBlock.beforePos); if (c && l && a) { const d = _e(r.doc, c); if (!d.isBlockContainer) return false; if (d.blockContent.node.type.spec.content === "" || (d.blockContent.node.type.spec.content === "inline*" && d.blockContent.node.childCount === 0)) return n() .cut( { from: i.bnBlock.beforePos, to: i.bnBlock.afterPos, }, d.bnBlock.afterPos, ) .deleteRange({ from: d.bnBlock.beforePos, to: d.bnBlock.afterPos, }) .run(); } return false; }), ]), e = () => this.editor.commands.first(({ chain: n, commands: s }) => [ // Deletes the selection if it's not empty. () => s.deleteSelection(), // Deletes the first child block and un-nests its children, if the // selection is empty and at the end of the current block. If both the // parent and child blocks have inline content, the child block's // content is appended to the parent's. The child block's own children // are unindented before it's deleted. () => s.command(({ state: r }) => { var p; const i = Tt$2(r); if (!i.isBlockContainer || !i.childContainer) return false; const { blockContent: l, childContainer: a } = i, c = r.selection.from === l.afterPos - 1, d = r.selection.empty, u = It$1(r.doc.resolve(a.beforePos + 1)); if (!u.isBlockContainer) return false; if (c && d) { const h = u.blockContent.node, f = h.type.spec.content === "inline*", m = l.node.type.spec.content === "inline*"; return n() .insertContentAt(u.bnBlock.afterPos, ((p = u.childContainer) == null ? void 0 : p.node.content) || Fragment.empty) .deleteRange( // Deletes whole child container if there's only one child. a.node.childCount === 1 ? { from: a.beforePos, to: a.afterPos, } : { from: u.bnBlock.beforePos, to: u.bnBlock.afterPos, }, ) .insertContentAt(r.selection.from, f && m ? h.content : null) .setTextSelection(r.selection.from) .scrollIntoView() .run(); } return false; }), // Merges block with the next one (at the same nesting level or lower), // if one exists, the block has no children, and the selection is at the // end of the block. () => s.command(({ state: r }) => { const i = Tt$2(r); if (!i.isBlockContainer) return false; const { bnBlock: l, blockContent: a } = i, c = L(r.doc, i.bnBlock.beforePos); if (!c || !c.isBlockContainer) return false; const d = r.selection.from === a.afterPos - 1, u = r.selection.empty, p = l.afterPos; return d && u ? n().command(fe(p)).scrollIntoView().run() : false; }), // If the previous block is a columnList, moves the current block to // the end of the last column in it. () => s.command(({ state: r, tr: i, dispatch: l }) => { const a = Tt$2(r); if (!a.isBlockContainer) return false; const c = L(r.doc, a.bnBlock.beforePos); if (!c || c.isBlockContainer) return false; if (l) { const d = c.bnBlock.beforePos + 1, u = i.doc.resolve(d + 1); return ( i.delete(u.pos, u.pos + u.nodeAfter.nodeSize), H(i, c.bnBlock.beforePos), i.insert(a.bnBlock.afterPos, u.nodeAfter), i.setSelection(TextSelection.near(i.doc.resolve(u.pos))), true ); } return false; }), // If the block is the last in a column, moves it to the start of the // next column. If there is no next column, moves it below the // columnList. () => s.command(({ state: r, tr: i, dispatch: l }) => { const a = Tt$2(r); if (!a.isBlockContainer || !(i.selection.from === a.blockContent.afterPos - 1)) return false; const d = i.doc.resolve(a.bnBlock.afterPos); if (d.nodeAfter || d.node().type.name !== "column") return false; const h = i.doc.resolve(a.bnBlock.afterPos), f = i.doc.resolve(h.after()), m = f.after(); if (l) { const b = f.pos === m - 1 ? m : f.pos + 1, g = It$1(i.doc.resolve(b)); (i.delete(g.bnBlock.beforePos, g.bnBlock.afterPos), H(i, m - f.node().nodeSize), i.insert(h.pos, g.bnBlock.node), i.setSelection(TextSelection.near(i.doc.resolve(b)))); } return true; }), // Deletes the next block at either the same or lower nesting level, if // the selection is empty and at the end of the block. If both the // current and next blocks have inline content, the next block's // content is appended to the current block's. The next block's own // children are unindented before it's deleted. () => s.command(({ state: r }) => { var d; const i = Tt$2(r); if (!i.isBlockContainer) return false; const { blockContent: l } = i, a = r.selection.from === l.afterPos - 1, c = r.selection.empty; if (a && c) { const u = (b, g) => { const x = L(b, g); if (x) return x; const y = Le(b, g); if (y) return u(b, y.bnBlock.beforePos); }, p = u(r.doc, i.bnBlock.beforePos); if (!p || !p.isBlockContainer) return false; const h = p.blockContent.node, f = h.type.spec.content === "inline*", m = l.node.type.spec.content === "inline*"; return n() .insertContentAt(p.bnBlock.afterPos, ((d = p.childContainer) == null ? void 0 : d.node.content) || Fragment.empty) .deleteRange({ from: p.bnBlock.beforePos, to: p.bnBlock.afterPos, }) .insertContentAt(r.selection.from, f && m ? h.content : null) .setTextSelection(r.selection.from) .scrollIntoView() .run(); } return false; }), // Deletes the current block if it's an empty block with inline content, // and moves the selection to the next block. () => s.command(({ state: r }) => { const i = Tt$2(r); if (!i.isBlockContainer) return false; if (i.blockContent.node.childCount === 0 && i.blockContent.node.type.spec.content === "inline*") { const a = L(r.doc, i.bnBlock.beforePos); if (!a || !a.isBlockContainer) return false; let c = n(); if (a.blockContent.node.type.spec.content === "tableRow+") { const f = i.bnBlock.afterPos + 1 + 1 + 1 + 1 + 1; c = c.setTextSelection(f); } else a.blockContent.node.type.spec.content === "" ? (c = c.setNodeSelection(a.blockContent.beforePos)) : (c = c.setTextSelection(a.blockContent.beforePos + 1)); return c .deleteRange({ from: i.bnBlock.beforePos, to: i.bnBlock.afterPos, }) .scrollIntoView() .run(); } return false; }), // Deletes next block if it contains no content and isn't a table, // when the selection is empty and at the end of the block. Moves the // current block into the deleted block's place. () => s.command(({ state: r }) => { const i = Tt$2(r); if (!i.isBlockContainer) return false; const l = r.selection.from === i.blockContent.afterPos - 1, a = r.selection.empty, c = L(r.doc, i.bnBlock.beforePos); if (!c || !c.isBlockContainer) return false; if (c && l && a && (c.blockContent.node.type.spec.content === "" || (c.blockContent.node.type.spec.content === "inline*" && c.blockContent.node.childCount === 0))) { const u = c.bnBlock.node.lastChild.content; return n() .deleteRange({ from: c.bnBlock.beforePos, to: c.bnBlock.afterPos, }) .insertContentAt(i.bnBlock.afterPos, c.bnBlock.node.childCount === 2 ? u : null) .run(); } return false; }), ]), t = (n = false) => this.editor.commands.first(({ commands: s, tr: r }) => [ // Removes a level of nesting if the block is empty & indented, while the selection is also empty & at the start // of the block. () => s.command(({ state: i }) => { const l = Tt$2(i); if (!l.isBlockContainer) return false; const { bnBlock: a, blockContent: c } = l, { depth: d } = i.doc.resolve(a.beforePos), u = i.selection.$anchor.parentOffset === 0, p = i.selection.anchor === i.selection.head, h = c.node.childCount === 0, f = d > 1; return u && p && h && f ? s.liftListItem("blockContainer") : false; }), // Creates a hard break if block is configured to do so. () => s.command(({ state: i }) => { var c; const l = Tt$2(i), a = ((c = this.options.editor.schema.blockSchema[l.blockNoteType].meta) == null ? void 0 : c.hardBreakShortcut) ?? "shift+enter"; if (a === "none") return false; if ( // If shortcut is not configured, or is configured as "shift+enter", // create a hard break for shift+enter, but not for enter. (a === "shift+enter" && n) || // If shortcut is configured as "enter", create a hard break for // both enter and shift+enter. a === "enter" ) { const d = r.storedMarks || r.selection.$head.marks().filter((u) => this.editor.extensionManager.splittableMarks.includes(u.type.name)); return (r.insert(r.selection.head, r.doc.type.schema.nodes.hardBreak.create()).ensureMarks(d), true); } return false; }), // Creates a new block and moves the selection to it if the current one is empty, while the selection is also // empty & at the start of the block. () => s.command(({ state: i, dispatch: l, tr: a }) => { var m; const c = Tt$2(i); if (!c.isBlockContainer) return false; const { bnBlock: d, blockContent: u } = c, p = i.selection.$anchor.parentOffset === 0, h = i.selection.anchor === i.selection.head, f = u.node.childCount === 0; if (p && h && f) { const b = d.afterPos, g = b + 2; if (l) { const x = i.schema.nodes.blockContainer.createAndFill( void 0, [i.schema.nodes.paragraph.createAndFill() || void 0, (m = c.childContainer) == null ? void 0 : m.node].filter((y) => y !== void 0), ); (a .insert(b, x) .setSelection(new TextSelection(a.doc.resolve(g))) .scrollIntoView(), c.childContainer && a.delete(c.childContainer.beforePos, c.childContainer.afterPos)); } return true; } return false; }), // Splits the current block, moving content inside that's after the cursor to a new text block below. Also // deletes the selection beforehand, if it's not empty. () => s.command(({ state: i, chain: l }) => { const a = Tt$2(i); if (!a.isBlockContainer) return false; const { blockContent: c } = a, d = i.selection.$anchor.parentOffset === 0; return c.node.childCount === 0 ? false : (l() .deleteSelection() .command(wo$1(i.selection.from, d, d)) .run(), true); }), ]); return { "Backspace": o, "Delete": e, "Enter": () => t(), "Shift-Enter": () => t(true), // Always returning true for tab key presses ensures they're not captured by the browser. Otherwise, they blur the // editor since the browser will try to use tab for keyboard navigation. "Tab": () => { var n, s; return ( this.options.tabBehavior !== "prefer-indent" && (((n = this.options.editor.getExtension(Ao$1)) != null && n.store.state) || ((s = this.options.editor.getExtension(H$1)) == null ? void 0 : s.store.state) !== void 0) ) ? false : Me(this.options.editor); }, "Shift-Tab": () => { var n, s; return ( this.options.tabBehavior !== "prefer-indent" && (((n = this.options.editor.getExtension(Ao$1)) != null && n.store.state) || ((s = this.options.editor.getExtension(H$1)) == null ? void 0 : s.store.state) !== void 0) ) ? false : this.editor.commands.liftListItem("blockContainer"); }, "Shift-Mod-ArrowUp": () => (this.options.editor.moveBlocksUp(), true), "Shift-Mod-ArrowDown": () => (this.options.editor.moveBlocksDown(), true), "Mod-z": () => this.options.editor.undo(), "Mod-y": () => this.options.editor.redo(), "Shift-Mod-z": () => this.options.editor.redo(), }; }, }), pn = Mark.create({ name: "insertion", inclusive: false, excludes: "deletion modification insertion", addAttributes() { return { id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap, so this doesn't actually work (considered not critical) }; }, extendMarkSchema(o) { return o.name !== "insertion" ? {} : { blocknoteIgnore: true, inclusive: false, toDOM(e, t) { return [ "ins", { "data-id": String(e.attrs.id), "data-inline": String(t), ...(!t && { style: "display: contents" }), // changed to "contents" to make this work for table rows }, 0, ]; }, parseDOM: [ { tag: "ins", getAttrs(e) { return e.dataset.id ? { id: parseInt(e.dataset.id, 10), } : false; }, }, ], }; }, }), fn = Mark.create({ name: "deletion", inclusive: false, excludes: "insertion modification deletion", addAttributes() { return { id: { default: null, validate: "number" }, // note: validate is supported in prosemirror but not in tiptap }; }, extendMarkSchema(o) { return o.name !== "deletion" ? {} : { blocknoteIgnore: true, inclusive: false, // attrs: { // id: { validate: "number" }, // }, toDOM(e, t) { return [ "del", { "data-id": String(e.attrs.id), "data-inline": String(t), ...(!t && { style: "display: contents" }), // changed to "contents" to make this work for table rows }, 0, ]; }, parseDOM: [ { tag: "del", getAttrs(e) { return e.dataset.id ? { id: parseInt(e.dataset.id, 10), } : false; }, }, ], }; }, }), hn = Mark.create({ name: "modification", inclusive: false, excludes: "deletion insertion", addAttributes() { return { id: { default: null, validate: "number" }, type: { validate: "string" }, attrName: { default: null, validate: "string|null" }, previousValue: { default: null }, newValue: { default: null }, }; }, extendMarkSchema(o) { return o.name !== "modification" ? {} : { blocknoteIgnore: true, inclusive: false, // attrs: { // id: { validate: "number" }, // type: { validate: "string" }, // attrName: { default: null, validate: "string|null" }, // previousValue: { default: null }, // newValue: { default: null }, // }, toDOM(e, t) { return [ t ? "span" : "div", { "data-type": "modification", "data-id": String(e.attrs.id), "data-mod-type": e.attrs.type, "data-mod-prev-val": JSON.stringify(e.attrs.previousValue), // TODO: Try to serialize marks with toJSON? "data-mod-new-val": JSON.stringify(e.attrs.newValue), }, 0, ]; }, parseDOM: [ { tag: "span[data-type='modification']", getAttrs(e) { return e.dataset.id ? { id: parseInt(e.dataset.id, 10), type: e.dataset.modType, previousValue: e.dataset.modPrevVal, newValue: e.dataset.modNewVal, } : false; }, }, { tag: "div[data-type='modification']", getAttrs(e) { return e.dataset.id ? { id: parseInt(e.dataset.id, 10), type: e.dataset.modType, previousValue: e.dataset.modPrevVal, } : false; }, }, ], }; }, }), mn = Extension.create({ name: "textAlignment", addGlobalAttributes() { return [ { // Generally text alignment is handled through props using the custom // blocks API. Tables are the only blocks that are created as TipTap // nodes and ported to blocks, so we need to add text alignment in a // separate extension. types: ["tableCell", "tableHeader"], attributes: { textAlignment: { default: "left", parseHTML: (o) => o.getAttribute("data-text-alignment"), renderHTML: (o) => o.textAlignment === "left" ? {} : { "data-text-alignment": o.textAlignment, }, }, }, }, ]; }, }), kn = Extension.create({ name: "blockTextColor", addGlobalAttributes() { return [ { types: ["table", "tableCell", "tableHeader"], attributes: { textColor: xo$1(), }, }, ]; }, }), bn = { blockColor: "data-block-color", blockStyle: "data-block-style", id: "data-id", depth: "data-depth", depthChange: "data-depth-change", }, gn = Node3.create({ name: "blockContainer", group: "blockGroupChild bnBlock", // A block always contains content, and optionally a blockGroup which contains nested blocks content: "blockContent blockGroup?", // Ensures content-specific keyboard handlers trigger first. priority: 50, defining: true, marks: "insertion modification deletion", parseHTML() { return [ { tag: "div[data-node-type=" + this.name + "]", getAttrs: (o) => { if (typeof o == "string") return false; const e = {}; for (const [t, n] of Object.entries(bn)) o.getAttribute(n) && (e[t] = o.getAttribute(n)); return e; }, }, // Ignore `blockOuter` divs, but parse the `blockContainer` divs inside them. { tag: 'div[data-node-type="blockOuter"]', skip: true, }, ]; }, renderHTML({ HTMLAttributes: o }) { var s; const e = document.createElement("div"); ((e.className = "bn-block-outer"), e.setAttribute("data-node-type", "blockOuter")); for (const [r, i] of Object.entries(o)) r !== "class" && e.setAttribute(r, i); const t = { ...(((s = this.options.domAttributes) == null ? void 0 : s.block) || {}), ...o, }, n = document.createElement("div"); ((n.className = D$2("bn-block", t.class)), n.setAttribute("data-node-type", this.name)); for (const [r, i] of Object.entries(t)) r !== "class" && n.setAttribute(r, i); return ( e.appendChild(n), { dom: e, contentDOM: n, } ); }, }), Bn = Node3.create({ name: "blockGroup", group: "childContainer", content: "blockGroupChild+", marks: "deletion insertion modification", parseHTML() { return [ { tag: "div", getAttrs: (o) => typeof o == "string" ? false : o.getAttribute("data-node-type") === "blockGroup" ? null : false, }, ]; }, renderHTML({ HTMLAttributes: o }) { var n; const e = { ...(((n = this.options.domAttributes) == null ? void 0 : n.blockGroup) || {}), ...o, }, t = document.createElement("div"); ((t.className = D$2("bn-block-group", e.class)), t.setAttribute("data-node-type", "blockGroup")); for (const [s, r] of Object.entries(e)) s !== "class" && t.setAttribute(s, r); return { dom: t, contentDOM: t, }; }, }), yn = Node3.create({ name: "doc", topNode: true, content: "blockGroup", marks: "insertion modification deletion", }), Cn = a$1(({ options: o }) => ({ key: "collaboration", blockNoteExtensions: [Oo$1(o), ae$1(o), K(o), X$1(), Po$1(o)], })); let he = false; function Sn(o, e) { const t = [ extensions_exports.ClipboardTextSerializer, extensions_exports.Commands, extensions_exports.Editable, extensions_exports.FocusEvents, extensions_exports.Tabindex, Gapcursor, Q$2.configure({ // everything from bnBlock group (nodes that represent a BlockNote block should have an id) types: ["blockContainer", "columnList", "column"], setIdAttribute: e.setIdAttribute, }), cn, Text, // marks: pn, fn, hn, Link.extend({ inclusive: false, }).configure({ defaultProtocol: Vo$1, // only call this once if we have multiple editors installed. Or fix https://github.com/ueberdosis/tiptap/issues/5450 protocols: he ? [] : Ro$1, }), ...Object.values(o.schema.styleSpecs).map((n) => n.implementation.mark.configure({ editor: o, }), ), kn, an, mn, // make sure escape blurs editor, so that we can tab to other elements in the host page (accessibility) Extension.create({ name: "OverrideEscape", addKeyboardShortcuts: () => ({ Escape: () => { var n; return (n = o.getExtension(Vn)) != null && n.shown() ? false : (o.blur(), true); }, }), }), // nodes yn, gn.configure({ editor: o, domAttributes: e.domAttributes, }), un.configure({ editor: o, tabBehavior: e.tabBehavior, }), Bn.configure({ domAttributes: e.domAttributes, }), ...Object.values(o.schema.inlineContentSpecs) .filter((n) => n.config !== "link" && n.config !== "text") .map((n) => n.implementation.node.configure({ editor: o, }), ), ...Object.values(o.schema.blockSpecs).flatMap((n) => [ // the node extension implementations ...("node" in n.implementation ? [ n.implementation.node.configure({ editor: o, domAttributes: e.domAttributes, }), ] : []), ]), rn(o), nn(o, e.pasteHandler || ((n) => n.defaultPasteHandler())), Vo(o), ]; return ((he = true), t); } function xn(o, e) { const t = [Do$1(), Mo$1(e), H$1(e), Ao$1(e), No$1(e), Ho$1(), $o$1(e), b(e), _o$1(e), Vn(e), ...(e.trailingBlock !== false ? [jo$1()] : [])]; return (e.collaboration ? t.push(Cn(e.collaboration)) : t.push(Lo$1()), "table" in o.schema.blockSpecs && t.push(Yo$1(e)), e.animations !== false && t.push(Fo$1()), t); } class En { constructor(e, t) { /** * A set of extension keys which are disabled by the options */ k(this, "disabledExtensions", /* @__PURE__ */ new Set()); /** * A list of all the extensions that are registered to the editor */ k(this, "extensions", []); /** * A map of all the abort controllers for each extension that has an init method defined */ k(this, "abortMap", /* @__PURE__ */ new Map()); /** * A map of all the extension factories that are registered to the editor */ k(this, "extensionFactories", /* @__PURE__ */ new Map()); /** * Because a single blocknote extension can both have it's own prosemirror plugins & additional generated ones (e.g. keymap & input rules plugins) * We need to keep track of all the plugins for each extension, so that we can remove them when the extension is unregistered */ k(this, "extensionPlugins", /* @__PURE__ */ new Map()); ((this.editor = e), (this.options = t), e.onMount(() => { for (const n of this.extensions) if (n.mount) { const s = new window.AbortController(), r = n.mount({ dom: e.prosemirrorView.dom, root: e.prosemirrorView.root, signal: s.signal, }); (r && s.signal.addEventListener("abort", () => { r(); }), this.abortMap.set(n, s)); } }), e.onUnmount(() => { for (const [n, s] of this.abortMap.entries()) (this.abortMap.delete(n), s.abort()); }), (this.disabledExtensions = new Set(t.disableExtensions || []))); for (const n of xn(this.editor, this.options)) this.addExtension(n); for (const n of this.options.extensions ?? []) this.addExtension(n); for (const n of Object.values(this.editor.schema.blockSpecs)) for (const s of n.extensions ?? []) this.addExtension(s); } /** * Register one or more extensions to the editor after the editor is initialized. * * This allows users to switch on & off extensions "at runtime". */ registerExtension(e) { var r; const t = [].concat(e).filter(Boolean); if (!t.length) { console.warn("No extensions found to register", e); return; } const n = t.map((i) => this.addExtension(i)).filter(Boolean), s = /* @__PURE__ */ new Set(); for (const i of n) (i != null && i.tiptapExtensions && console.warn( `Extension ${i.key} has tiptap extensions, but these cannot be changed after initializing the editor. Please separate the extension into multiple extensions if you want to add them, or re-initialize the editor.`, i, ), (r = i == null ? void 0 : i.inputRules) != null && r.length && console.warn( `Extension ${i.key} has input rules, but these cannot be changed after initializing the editor. Please separate the extension into multiple extensions if you want to add them, or re-initialize the editor.`, i, ), this.getProsemirrorPluginsFromExtension(i).plugins.forEach((l) => { s.add(l); })); this.updatePlugins((i) => [...i, ...s]); } /** * Register an extension to the editor * @param extension - The extension to register * @returns The extension instance */ addExtension(e) { let t; if ((typeof e == "function" ? (t = e({ editor: this.editor })) : (t = e), !(!t || this.disabledExtensions.has(t.key)))) { if (typeof e == "function") { const n = t[r$1]; typeof n == "function" && this.extensionFactories.set(n, t); } if ((this.extensions.push(t), t.blockNoteExtensions)) for (const n of t.blockNoteExtensions) this.addExtension(n); return t; } } /** * Resolve an extension or a list of extensions into a list of extension instances * @param toResolve - The extension or list of extensions to resolve * @returns A list of extension instances */ resolveExtensions(e) { const t = []; if (typeof e == "function") { const n = this.extensionFactories.get(e); n && t.push(n); } else if (Array.isArray(e)) for (const n of e) t.push(...this.resolveExtensions(n)); else if (typeof e == "object" && "key" in e) t.push(e); else if (typeof e == "string") { const n = this.extensions.find((s) => s.key === e); n && t.push(n); } return t; } /** * Unregister an extension from the editor * @param toUnregister - The extension to unregister * @returns void */ unregisterExtension(e) { var r; const t = this.resolveExtensions(e); if (!t.length) { console.warn("No extensions found to unregister", e); return; } let n = false; const s = /* @__PURE__ */ new Set(); for (const i of t) { ((this.extensions = this.extensions.filter((a) => a !== i)), this.extensionFactories.forEach((a, c) => { a === i && this.extensionFactories.delete(c); }), (r = this.abortMap.get(i)) == null || r.abort(), this.abortMap.delete(i)); const l = this.extensionPlugins.get(i); (l == null || l.forEach((a) => { s.add(a); }), this.extensionPlugins.delete(i), i.tiptapExtensions && !n && ((n = true), console.warn( `Extension ${i.key} has tiptap extensions, but they will not be removed. Please separate the extension into multiple extensions if you want to remove them, or re-initialize the editor.`, e, ))); } this.updatePlugins((i) => i.filter((l) => !s.has(l))); } /** * Allows resetting the current prosemirror state's plugins * @param update - A function that takes the current plugins and returns the new plugins * @returns void */ updatePlugins(e) { const t = this.editor.prosemirrorState, n = t.reconfigure({ plugins: e(t.plugins.slice()), }); this.editor.prosemirrorView.updateState(n); } /** * Get all the extensions that are registered to the editor */ getTiptapExtensions() { var s; const e = Sn(this.editor, this.options).filter((r) => !this.disabledExtensions.has(r.name)), t = P(this.extensions), n = /* @__PURE__ */ new Map(); for (const r of this.extensions) { r.tiptapExtensions && e.push(...r.tiptapExtensions); const i = t(r.key), { plugins: l, inputRules: a } = this.getProsemirrorPluginsFromExtension(r); (l.length && e.push( Extension.create({ name: r.key, priority: i, addProseMirrorPlugins: () => l, }), ), a.length && (n.has(i) || n.set(i, []), n.get(i).push(...a))); } e.push( Extension.create({ name: "blocknote-input-rules", addProseMirrorPlugins() { const r = []; return ( Array.from(n.keys()) .sort() .reverse() .forEach((i) => { r.push(...n.get(i)); }), [inputRules({ rules: r })] ); }, }), ); for (const r of ((s = this.options._tiptapOptions) == null ? void 0 : s.extensions) ?? []) e.push(r); return e; } /** * This maps a blocknote extension into an array of Prosemirror plugins if it has any of the following: * - plugins * - keyboard shortcuts * - input rules */ getProsemirrorPluginsFromExtension(e) { var s, r, i; const t = [...(e.prosemirrorPlugins ?? [])], n = []; return !((s = e.prosemirrorPlugins) != null && s.length) && !Object.keys(e.keyboardShortcuts || {}).length && !((r = e.inputRules) != null && r.length) ? { plugins: t, inputRules: n } : (this.extensionPlugins.set(e, t), (i = e.inputRules) != null && i.length && n.push( ...e.inputRules.map( (l) => new InputRule(l.find, (a, c, d, u) => { const p = l.replace({ match: c, range: { from: d, to: u }, editor: this.editor, }); if (p) { const h = this.editor.getTextCursorPosition(); if (this.editor.schema.blockSchema[h.block.type].content !== "inline") return null; const f = Ot$2(a.tr), m = a.tr.deleteRange(d, u); return (Q$1(m, f.bnBlock.beforePos, p), m); } return null; }), ), ), Object.keys(e.keyboardShortcuts || {}).length && t.push(keymap(Object.fromEntries(Object.entries(e.keyboardShortcuts).map(([l, a]) => [l, () => a({ editor: this.editor })])))), { plugins: t, inputRules: n }); } /** * Get all extensions */ getExtensions() { return new Map(this.extensions.map((e) => [e.key, e])); } getExtension(e) { if (typeof e == "string") { const t = this.extensions.find((n) => n.key === e); return t || void 0; } else if (typeof e == "function") { const t = this.extensionFactories.get(e); return t || void 0; } throw new Error(`Invalid extension type: ${typeof e}`); } /** * Check if an extension exists */ hasExtension(e) { return ( typeof e == "string" ? this.extensions.some((t) => t.key === e) : typeof e == "object" && "key" in e ? this.extensions.some((t) => t.key === e.key) : typeof e == "function" ? this.extensionFactories.has(e) : false ); } } function Pn(o, e) { let { $from: t, $to: n } = e; if (t.pos > t.start() && t.pos < o.content.size) { const s = o.textBetween(t.pos, t.pos + 1); if (/^[\w\p{P}]$/u.test(s)) { const i = o.textBetween(t.start(), t.pos).match(/[\w\p{P}]+$/u); i && (t = o.resolve(t.pos - i[0].length)); } } if (n.pos < n.end() && n.pos > 0) { const s = o.textBetween(n.pos - 1, n.pos); if (/^[\w\p{P}]$/u.test(s)) { const i = o.textBetween(n.pos, n.end()).match(/^[\w\p{P}]+/u); i && (n = o.resolve(n.pos + i[0].length)); } } return { $from: t, $to: n, from: t.pos, to: n.pos }; } function Tn(o) { const e = ht(o); if (o.selection.empty || "node" in o.selection) return; const t = o.doc.resolve(Y$1(o.doc, o.selection.from).posBeforeNode), n = o.doc.resolve(Y$1(o.doc, o.selection.to).posBeforeNode), s = (c, d) => { const u = t.posAtIndex(c, d), p = o.doc.resolve(u).nodeAfter; if (!p) throw new Error(`Error getting selection - node not found at position ${u}`); return L$2(p, e); }, r = [], i = t.sharedDepth(n.pos), l = t.index(i), a = n.index(i); if (t.depth > i) { r.push(L$2(t.nodeAfter, e)); for (let c = t.depth; c > i; c--) if (t.node(c).type.isInGroup("childContainer")) { const u = t.index(c) + 1, p = t.node(c).childCount; for (let h = u; h < p; h++) r.push(s(h, c)); } } else r.push(s(l, i)); for (let c = l + 1; c <= a; c++) r.push(s(c, i)); if (r.length === 0) throw new Error(`Error getting selection - selection doesn't span any blocks (${o.selection})`); return { blocks: r, }; } function wn(o, e, t) { const n = typeof e == "string" ? e : e.id, s = typeof t == "string" ? t : t.id, r = ht(o), i = J$1(r); if (n === s) throw new Error(`Attempting to set selection with the same anchor and head blocks (id ${n})`); const l = Bt$1(n, o.doc); if (!l) throw new Error(`Block with ID ${n} not found`); const a = Bt$1(s, o.doc); if (!a) throw new Error(`Block with ID ${s} not found`); const c = Z(l), d = Z(a), u = i.blockSchema[c.blockNoteType], p = i.blockSchema[d.blockNoteType]; if (!c.isBlockContainer || u.content === "none") throw new Error(`Attempting to set selection anchor in block without content (id ${n})`); if (!d.isBlockContainer || p.content === "none") throw new Error(`Attempting to set selection anchor in block without content (id ${s})`); let h, f; if (u.content === "table") { const m = TableMap.get(c.blockContent.node); h = c.blockContent.beforePos + m.positionAt(0, 0, c.blockContent.node) + 1 + 2; } else h = c.blockContent.beforePos + 1; if (p.content === "table") { const m = TableMap.get(d.blockContent.node), b = d.blockContent.beforePos + m.positionAt(m.height - 1, m.width - 1, d.blockContent.node) + 1, g = o.doc.resolve(b).nodeAfter.nodeSize; f = b + g - 2; } else f = d.blockContent.afterPos - 1; o.setSelection(TextSelection.create(o.doc, h, f)); } function Mn(o, e = false) { const t = ht(o), n = e ? Pn(o.doc, o.selection) : o.selection; let s = n.$from, r = n.$to; for (; r.parentOffset >= r.parent.nodeSize - 2 && r.depth > 0; ) r = o.doc.resolve(r.pos + 1); for (; r.parentOffset === 0 && r.depth > 0; ) r = o.doc.resolve(r.pos - 1); for (; s.parentOffset === 0 && s.depth > 0; ) s = o.doc.resolve(s.pos - 1); for (; s.parentOffset >= s.parent.nodeSize - 2 && s.depth > 0; ) s = o.doc.resolve(s.pos + 1); const i = Dt$2(o.doc.slice(s.pos, r.pos, true), t); return { _meta: { startPos: s.pos, endPos: r.pos, }, ...i, }; } function vn(o) { const { bnBlock: e } = Ot$2(o), t = ht(o.doc), n = o.doc.resolve(e.beforePos), s = n.nodeBefore, r = o.doc.resolve(e.afterPos).nodeAfter; let i; return ( n.depth > 1 && ((i = n.node()), i.type.isInGroup("bnBlock") || (i = n.node(n.depth - 1))), { block: L$2(e.node, t), prevBlock: s === null ? void 0 : L$2(s, t), nextBlock: r === null ? void 0 : L$2(r, t), parentBlock: i === void 0 ? void 0 : L$2(i, t), } ); } function De(o, e, t = "start") { const n = typeof e == "string" ? e : e.id, s = ht(o.doc), r = J$1(s), i = Bt$1(n, o.doc); if (!i) throw new Error(`Block with ID ${n} not found`); const l = Z(i), a = r.blockSchema[l.blockNoteType].content; if (l.isBlockContainer) { const c = l.blockContent; if (a === "none") { o.setSelection(NodeSelection.create(o.doc, c.beforePos)); return; } if (a === "inline") t === "start" ? o.setSelection(TextSelection.create(o.doc, c.beforePos + 1)) : o.setSelection(TextSelection.create(o.doc, c.afterPos - 1)); else if (a === "table") t === "start" ? o.setSelection(TextSelection.create(o.doc, c.beforePos + 4)) : o.setSelection(TextSelection.create(o.doc, c.afterPos - 4)); else throw new O(a); } else { const c = t === "start" ? l.childContainer.node.firstChild : l.childContainer.node.lastChild; De(o, c.attrs.id, t); } } class In { constructor(e) { this.editor = e; } /** * Gets a snapshot of the current selection. This contains all blocks (included nested blocks) * that the selection spans across. * * If the selection starts / ends halfway through a block, the returned data will contain the entire block. */ getSelection() { return this.editor.transact((e) => Tn(e)); } /** * Gets a snapshot of the current selection. This contains all blocks (included nested blocks) * that the selection spans across. * * If the selection starts / ends halfway through a block, the returned block will be * only the part of the block that is included in the selection. */ getSelectionCutBlocks(e = false) { return this.editor.transact((t) => Mn(t, e)); } /** * Sets the selection to a range of blocks. * @param startBlock The identifier of the block that should be the start of the selection. * @param endBlock The identifier of the block that should be the end of the selection. */ setSelection(e, t) { return this.editor.transact((n) => wn(n, e, t)); } /** * Gets a snapshot of the current text cursor position. * @returns A snapshot of the current text cursor position. */ getTextCursorPosition() { return this.editor.transact((e) => vn(e)); } /** * Sets the text cursor position to the start or end of an existing block. Throws an error if the target block could * not be found. * @param targetBlock The identifier of an existing block that the text cursor should be moved to. * @param placement Whether the text cursor should be placed at the start or end of the block. */ setTextCursorPosition(e, t = "start") { return this.editor.transact((n) => De(n, e, t)); } /** * Gets the bounding box of the current selection. */ getSelectionBoundingBox() { if (!this.editor.prosemirrorView) return; const { selection: e } = this.editor.prosemirrorState, { ranges: t } = e, n = Math.min(...t.map((r) => r.$from.pos)), s = Math.max(...t.map((r) => r.$to.pos)); if (isNodeSelection(e)) { const r = this.editor.prosemirrorView.nodeDOM(n); if (r) return r.getBoundingClientRect(); } return posToDOMRect(this.editor.prosemirrorView, n, s).toJSON(); } } class An { constructor(e) { /** * Stores the currently active transaction, which is the accumulated transaction from all {@link dispatch} calls during a {@link transact} calls */ k(this, "activeTransaction", null); // Flag to indicate if we're in a `can` call k(this, "isInCan", false); this.editor = e; } /** * For any command that can be executed, you can check if it can be executed by calling `editor.can(command)`. * @example * ```ts * if (editor.can(editor.undo)) { * // show button * } else { * // hide button * } */ can(e) { try { return ((this.isInCan = !0), e()); } finally { this.isInCan = false; } } /** * Execute a prosemirror command. This is mostly for backwards compatibility with older code. * * @note You should prefer the {@link transact} method when possible, as it will automatically handle the dispatching of the transaction and work across blocknote transactions. * * @example * ```ts * editor.exec((state, dispatch, view) => { * dispatch(state.tr.insertText("Hello, world!")); * }); * ``` */ exec(e) { if (this.activeTransaction) throw new Error("`exec` should not be called within a `transact` call, move the `exec` call outside of the `transact` call"); if (this.isInCan) return this.canExec(e); const t = this.prosemirrorState, n = this.prosemirrorView; return e(t, (r) => this.prosemirrorView.dispatch(r), n); } /** * Check if a command can be executed. A command should return `false` if it is not valid in the current state. * * @example * ```ts * if (editor.canExec(command)) { * // show button * } else { * // hide button * } * ``` */ canExec(e) { if (this.activeTransaction) throw new Error("`canExec` should not be called within a `transact` call, move the `canExec` call outside of the `transact` call"); const t = this.prosemirrorState, n = this.prosemirrorView; return e(t, void 0, n); } /** * Execute a function within a "blocknote transaction". * All changes to the editor within the transaction will be grouped together, so that * we can dispatch them as a single operation (thus creating only a single undo step) * * @note There is no need to dispatch the transaction, as it will be automatically dispatched when the callback is complete. * * @example * ```ts * // All changes to the editor will be grouped together * editor.transact((tr) => { * tr.insertText("Hello, world!"); * // These two operations will be grouped together in a single undo step * editor.transact((tr) => { * tr.insertText("Hello, world!"); * }); * }); * ``` */ transact(e) { if (this.activeTransaction) return e(this.activeTransaction); try { this.activeTransaction = this.editor._tiptapEditor.state.tr; const t = e(this.activeTransaction), n = this.activeTransaction; return ( (this.activeTransaction = null), n && // Only dispatch if the transaction was actually modified in some way (n.docChanged || n.selectionSet || n.scrolledIntoView || n.storedMarksSet || !n.isGeneric) && this.prosemirrorView.dispatch(n), t ); } finally { this.activeTransaction = null; } } /** * Get the underlying prosemirror state * @note Prefer using `editor.transact` to read the current editor state, as that will ensure the state is up to date * @see https://prosemirror.net/docs/ref/#state.EditorState */ get prosemirrorState() { if (this.activeTransaction) throw new Error( "`prosemirrorState` should not be called within a `transact` call, move the `prosemirrorState` call outside of the `transact` call or use `editor.transact` to read the current editor state", ); return this.editor._tiptapEditor.state; } /** * Get the underlying prosemirror view * @see https://prosemirror.net/docs/ref/#view.EditorView */ get prosemirrorView() { return this.editor._tiptapEditor.view; } isFocused() { var e; return ((e = this.prosemirrorView) == null ? void 0 : e.hasFocus()) || false; } focus() { var e; (e = this.prosemirrorView) == null || e.focus(); } /** * Checks if the editor is currently editable, or if it's locked. * @returns True if the editor is editable, false otherwise. */ get isEditable() { if (!this.editor._tiptapEditor) { if (!this.editor.headless) throw new Error("no editor, but also not headless?"); return false; } return this.editor._tiptapEditor.isEditable === void 0 ? true : this.editor._tiptapEditor.isEditable; } /** * Makes the editor editable or locks it, depending on the argument passed. * @param editable True to make the editor editable, or false to lock it. */ set isEditable(e) { if (!this.editor._tiptapEditor) { if (!this.editor.headless) throw new Error("no editor, but also not headless?"); return; } this.editor._tiptapEditor.options.editable !== e && this.editor._tiptapEditor.setEditable(e); } /** * Undo the last action. */ undo() { const e = this.editor.getExtension("yUndo"); if (e) return this.exec(e.undoCommand); const t = this.editor.getExtension("history"); if (t) return this.exec(t.undoCommand); throw new Error("No undo plugin found"); } /** * Redo the last action. */ redo() { const e = this.editor.getExtension("yUndo"); if (e) return this.exec(e.redoCommand); const t = this.editor.getExtension("history"); if (t) return this.exec(t.redoCommand); throw new Error("No redo plugin found"); } } function Nn(o, e, t, n = { updateSelection: true }) { let { from: s, to: r } = typeof e == "number" ? { from: e, to: e } : { from: e.from, to: e.to }, i = true, l = true, a = ""; if ( (t.forEach((c) => { (c.check(), i && c.isText && c.marks.length === 0 ? (a += c.text) : (i = false), (l = l ? c.isBlock : false)); }), s === r && l) ) { const { parent: c } = o.doc.resolve(s); c.isTextblock && !c.type.spec.code && !c.childCount && ((s -= 1), (r += 1)); } return (i ? o.insertText(a, s, r) : o.replaceWith(s, r, t), n.updateSelection && selectionToInsertionEnd(o, o.steps.length - 1, -1), true); } class Ln { constructor(e) { this.editor = e; } /** * Insert a piece of content at the current cursor position. * * @param content can be a string, or array of partial inline content elements */ insertInlineContent(e, { updateSelection: t = false } = {}) { const n = T$1(e, this.editor.pmSchema); this.editor.transact((s) => { Nn( s, { from: s.selection.from, to: s.selection.to, }, n, { updateSelection: t, }, ); }); } /** * Gets the active text styles at the text cursor position or at the end of the current selection if it's active. */ getActiveStyles() { return this.editor.transact((e) => { const t = {}, n = e.selection.$to.marks(); for (const s of n) { const r = this.editor.schema.styleSchema[s.type.name]; if (!r) { // Links are not considered styles in blocknote s.type.name !== "link" && // "blocknoteIgnore" tagged marks (such as comments) are also not considered BlockNote "styles" !s.type.spec.blocknoteIgnore && console.warn("mark not found in styleschema", s.type.name); continue; } r.propSchema === "boolean" ? (t[r.type] = true) : (t[r.type] = s.attrs.stringValue); } return t; }); } /** * Adds styles to the currently selected content. * @param styles The styles to add. */ addStyles(e) { for (const [t, n] of Object.entries(e)) { const s = this.editor.schema.styleSchema[t]; if (!s) throw new Error(`style ${t} not found in styleSchema`); if (s.propSchema === "boolean") this.editor._tiptapEditor.commands.setMark(t); else if (s.propSchema === "string") this.editor._tiptapEditor.commands.setMark(t, { stringValue: n, }); else throw new O(s.propSchema); } } /** * Removes styles from the currently selected content. * @param styles The styles to remove. */ removeStyles(e) { for (const t of Object.keys(e)) this.editor._tiptapEditor.commands.unsetMark(t); } /** * Toggles styles on the currently selected content. * @param styles The styles to toggle. */ toggleStyles(e) { for (const [t, n] of Object.entries(e)) { const s = this.editor.schema.styleSchema[t]; if (!s) throw new Error(`style ${t} not found in styleSchema`); if (s.propSchema === "boolean") this.editor._tiptapEditor.commands.toggleMark(t); else if (s.propSchema === "string") this.editor._tiptapEditor.commands.toggleMark(t, { stringValue: n, }); else throw new O(s.propSchema); } } /** * Gets the currently selected text. */ getSelectedText() { return this.editor.transact((e) => e.doc.textBetween(e.selection.from, e.selection.to)); } /** * Gets the URL of the last link in the current selection, or `undefined` if there are no links in the selection. */ getSelectedLinkUrl() { return this.editor._tiptapEditor.getAttributes("link").href; } /** * Creates a new link to replace the selected content. * @param url The link URL. * @param text The text to display the link with. */ createLink(e, t) { if (e === "") return; const n = this.editor.pmSchema.mark("link", { href: e }); this.editor.transact((s) => { const { from: r, to: i } = s.selection; t ? s.insertText(t, r, i).addMark(r, r + t.length, n) : s.setSelection(TextSelection.create(s.doc, i)).addMark(r, i, n); }); } } function _n(o) { return findParentNodeClosestToPos(o.state.selection.$from, (e) => e.type.name === "tableCell" || e.type.name === "tableHeader") !== void 0; } function Oe(o, e) { var s; const t = e.nodes.hardBreak; let n = Fragment.empty; return ( o.forEach((r) => { r.isTextblock && r.childCount > 0 ? ((n = n.append(r.content)), (n = n.addToEnd(t.create()))) : r.isText ? (n = n.addToEnd(r)) : r.isBlock && r.childCount > 0 && ((n = n.append(Oe(r.content, e))), (n = n.addToEnd(t.create()))); }), ((s = n.lastChild) == null ? void 0 : s.type) === t && (n = n.cut(0, n.size - 1)), n ); } function Dn(o, e) { const t = []; return ( o.forEach((n, s, r) => { r !== e && t.push(n); }), Fragment.from(t) ); } function On(o, e) { const t = []; for (let n = 0; n < o.childCount; n++) if (o.child(n).type.name === "tableRow") if (t.length > 0 && t[t.length - 1].type.name === "table") { const s = t[t.length - 1], r = s.copy(s.content.addToEnd(o.child(n))); t[t.length - 1] = r; } else { const s = e.nodes.table.createChecked(void 0, o.child(n)); t.push(s); } else t.push(o.child(n)); return ((o = Fragment.from(t)), o); } function $n(o, e) { let t = Fragment.from(o.content); if (((t = On(t, e.state.schema)), _n(e))) { let n = false; if ( (t.descendants((s) => { s.type.isInGroup("tableContent") && (n = true); }), !n && // is the content valid for a table paragraph? !e.state.schema.nodes.tableParagraph.validContent(t)) ) return new Slice(Oe(t, e.state.schema), 0, 0); } if (!Hn(t, e)) return new Slice(t, o.openStart, o.openEnd); for (let n = 0; n < t.childCount; n++) if (t.child(n).type.spec.group === "blockContent") { const s = [t.child(n)]; if (n + 1 < t.childCount && t.child(n + 1).type.name === "blockGroup") { const i = t .child(n + 1) .child(0) .child(0); (i.type.name === "bulletListItem" || i.type.name === "numberedListItem" || i.type.name === "checkListItem") && (s.push(t.child(n + 1)), (t = Dn(t, n + 1))); } const r = e.state.schema.nodes.blockContainer.createChecked(void 0, s); t = t.replaceChild(n, r); } return new Slice(t, o.openStart, o.openEnd); } function Hn(o, e) { var r, i; const t = o.childCount === 1, n = ((r = o.firstChild) == null ? void 0 : r.type.spec.content) === "inline*", s = ((i = o.firstChild) == null ? void 0 : i.type.spec.content) === "tableRow+"; if (t) { if (n) return false; if (s) { const l = Tt$2(e.state); if (l.isBlockContainer) return !(l.blockContent.node.type.spec.content === "tableRow+"); } } return true; } const Fn = { enableInputRules: true, enablePasteRules: true, enableCoreExtensions: false, }; class $e extends f { constructor(t) { var c, d, u, p, h, f, m, b, g, x; super(); /** * The underlying prosemirror schema */ k(this, "pmSchema"); k(this, "_tiptapEditor"); /** * Used by React to store a reference to an `ElementRenderer` helper utility to make sure we can render React elements * in the correct context (used by `ReactRenderUtil`) */ k(this, "elementRenderer", null); /** * Cache of all blocks. This makes sure we don't have to "recompute" blocks if underlying Prosemirror Nodes haven't changed. * This is especially useful when we want to keep track of the same block across multiple operations, * with this cache, blocks stay the same object reference (referential equality with ===). */ k(this, "blockCache", /* @__PURE__ */ new WeakMap()); /** * The dictionary contains translations for the editor. */ k(this, "dictionary"); /** * The schema of the editor. The schema defines which Blocks, InlineContent, and Styles are available in the editor. */ k(this, "schema"); k(this, "blockImplementations"); k(this, "inlineContentImplementations"); k(this, "styleImplementations"); /** * The `uploadFile` method is what the editor uses when files need to be uploaded (for example when selecting an image to upload). * This method should set when creating the editor as this is application-specific. * * `undefined` means the application doesn't support file uploads. * * @param file The file that should be uploaded. * @returns The URL of the uploaded file OR an object containing props that should be set on the file block (such as an id) */ k(this, "uploadFile"); k(this, "onUploadStartCallbacks", []); k(this, "onUploadEndCallbacks", []); k(this, "resolveFileUrl"); /** * Editor settings */ k(this, "settings"); // Manager instances k(this, "_blockManager"); k(this, "_eventManager"); k(this, "_exportManager"); k(this, "_extensionManager"); k(this, "_selectionManager"); k(this, "_stateManager"); k(this, "_styleManager"); /** * Remove extension(s) from the editor */ k(this, "unregisterExtension", (...t) => this._extensionManager.unregisterExtension(...t)); /** * Register extension(s) to the editor */ k(this, "registerExtension", (...t) => this._extensionManager.registerExtension(...t)); /** * Get an extension from the editor */ k(this, "getExtension", (...t) => this._extensionManager.getExtension(...t)); /** * Mount the editor to a DOM element. * * @warning Not needed to call manually when using React, use BlockNoteView to take care of mounting */ k(this, "mount", (t) => { this._tiptapEditor.mount({ mount: t }); }); /** * Unmount the editor from the DOM element it is bound to */ k(this, "unmount", () => { this._tiptapEditor.unmount(); }); ((this.options = t), (this.dictionary = t.dictionary || i), (this.settings = { tables: { splitCells: ((c = t == null ? void 0 : t.tables) == null ? void 0 : c.splitCells) ?? false, cellBackgroundColor: ((d = t == null ? void 0 : t.tables) == null ? void 0 : d.cellBackgroundColor) ?? false, cellTextColor: ((u = t == null ? void 0 : t.tables) == null ? void 0 : u.cellTextColor) ?? false, headers: ((p = t == null ? void 0 : t.tables) == null ? void 0 : p.headers) ?? false, }, })); const n = { defaultStyles: true, schema: t.schema || h$1.create(), ...t, placeholders: { ...this.dictionary.placeholders, ...t.placeholders, }, }; if ( ((this.schema = n.schema), (this.blockImplementations = n.schema.blockSpecs), (this.inlineContentImplementations = n.schema.inlineContentSpecs), (this.styleImplementations = n.schema.styleSpecs), n.uploadFile) ) { const y = n.uploadFile; this.uploadFile = async (I, w) => { this.onUploadStartCallbacks.forEach((A) => A.apply(this, [w])); try { return await y(I, w); } finally { this.onUploadEndCallbacks.forEach((A) => A.apply(this, [w])); } }; } ((this.resolveFileUrl = n.resolveFileUrl), (this._eventManager = new Eo(this)), (this._extensionManager = new En(this, n))); const s = this._extensionManager.getTiptapExtensions(), r = this._extensionManager.hasExtension("ySync") || this._extensionManager.hasExtension("liveblocksExtension"); r && n.initialContent && console.warn("When using Collaboration, initialContent might cause conflicts, because changes should come from the collaboration provider"); const i$1 = { ...Fn, ...n._tiptapOptions, element: null, autofocus: n.autofocus ?? false, extensions: s, editorProps: { ...((h = n._tiptapOptions) == null ? void 0 : h.editorProps), attributes: { // As of TipTap v2.5.0 the tabIndex is removed when the editor is not // editable, so you can't focus it. We want to revert this as we have // UI behaviour that relies on it. tabIndex: "0", ...((m = (f = n._tiptapOptions) == null ? void 0 : f.editorProps) == null ? void 0 : m.attributes), ...((b = n.domAttributes) == null ? void 0 : b.editor), class: D$2("bn-editor", n.defaultStyles ? "bn-default-styles" : "", ((x = (g = n.domAttributes) == null ? void 0 : g.editor) == null ? void 0 : x.class) || ""), }, transformPasted: $n, }, }; try { const y = n.initialContent || (r ? [ { type: "paragraph", id: "initialBlockId", }, ] : [ { type: "paragraph", id: Q$2.options.generateID(), }, ]); if (!Array.isArray(y) || y.length === 0) throw new Error("initialContent must be a non-empty array of blocks, received: " + y); const I = getSchema(i$1.extensions), w = y.map((He) => bt$1(He, I, this.schema.styleSchema).toJSON()), A = createDocument( { type: "doc", content: [ { type: "blockGroup", content: w, }, ], }, I, i$1.parseOptions, ); ((this._tiptapEditor = new Editor({ ...i$1, content: A.toJSON(), })), (this.pmSchema = this._tiptapEditor.schema)); } catch (y) { throw new Error("Error creating document from blocks passed as `initialContent`", { cause: y }); } let l; const a = this.pmSchema.nodes.doc.createAndFill; ((this.pmSchema.nodes.doc.createAndFill = (...y) => { if (l) return l; const I = a.apply(this.pmSchema.nodes.doc, y), w = JSON.parse(JSON.stringify(I.toJSON())); return ((w.content[0].content[0].attrs.id = "initialBlockId"), (l = Node$1.fromJSON(this.pmSchema, w)), l); }), (this.pmSchema.cached.blockNoteEditor = this), (this._blockManager = new xo(this)), (this._exportManager = new $o(this)), (this._selectionManager = new In(this)), (this._stateManager = new An(this)), (this._styleManager = new Ln(this)), this.emit("create")); } static create(t) { return new $e(t ?? {}); } /** * BlockNote extensions that are added to the editor, keyed by the extension key */ get extensions() { return this._extensionManager.getExtensions(); } /** * Execute a prosemirror command. This is mostly for backwards compatibility with older code. * * @note You should prefer the {@link transact} method when possible, as it will automatically handle the dispatching of the transaction and work across blocknote transactions. * * @example * ```ts * editor.exec((state, dispatch, view) => { * dispatch(state.tr.insertText("Hello, world!")); * }); * ``` */ exec(t) { return this._stateManager.exec(t); } /** * Check if a command can be executed. A command should return `false` if it is not valid in the current state. * * @example * ```ts * if (editor.canExec(command)) { * // show button * } else { * // hide button * } * ``` */ canExec(t) { return this._stateManager.canExec(t); } /** * Execute a function within a "blocknote transaction". * All changes to the editor within the transaction will be grouped together, so that * we can dispatch them as a single operation (thus creating only a single undo step) * * @note There is no need to dispatch the transaction, as it will be automatically dispatched when the callback is complete. * * @example * ```ts * // All changes to the editor will be grouped together * editor.transact((tr) => { * tr.insertText("Hello, world!"); * // These two operations will be grouped together in a single undo step * editor.transact((tr) => { * tr.insertText("Hello, world!"); * }); * }); * ``` */ transact(t) { return this._stateManager.transact(t); } /** * Get the underlying prosemirror state * @note Prefer using `editor.transact` to read the current editor state, as that will ensure the state is up to date * @see https://prosemirror.net/docs/ref/#state.EditorState */ get prosemirrorState() { return this._stateManager.prosemirrorState; } /** * Get the underlying prosemirror view * @see https://prosemirror.net/docs/ref/#view.EditorView */ get prosemirrorView() { return this._stateManager.prosemirrorView; } get domElement() { var t; if (!this.headless) return (t = this.prosemirrorView) == null ? void 0 : t.dom; } isFocused() { var t; return this.headless ? false : ((t = this.prosemirrorView) == null ? void 0 : t.hasFocus()) || false; } get headless() { return !this._tiptapEditor.isInitialized; } /** * Focus on the editor */ focus() { this.headless || this.prosemirrorView.focus(); } /** * Blur the editor */ blur() { var t; this.headless || (t = this.domElement) == null || t.blur(); } // TODO move to extension onUploadStart(t) { return ( this.onUploadStartCallbacks.push(t), () => { const n = this.onUploadStartCallbacks.indexOf(t); n > -1 && this.onUploadStartCallbacks.splice(n, 1); } ); } onUploadEnd(t) { return ( this.onUploadEndCallbacks.push(t), () => { const n = this.onUploadEndCallbacks.indexOf(t); n > -1 && this.onUploadEndCallbacks.splice(n, 1); } ); } /** * @deprecated, use `editor.document` instead */ get topLevelBlocks() { return this.document; } /** * Gets a snapshot of all top-level (non-nested) blocks in the editor. * @returns A snapshot of all top-level (non-nested) blocks in the editor. */ get document() { return this._blockManager.document; } /** * Gets a snapshot of an existing block from the editor. * @param blockIdentifier The identifier of an existing block that should be * retrieved. * @returns The block that matches the identifier, or `undefined` if no * matching block was found. */ getBlock(t) { return this._blockManager.getBlock(t); } /** * Gets a snapshot of the previous sibling of an existing block from the * editor. * @param blockIdentifier The identifier of an existing block for which the * previous sibling should be retrieved. * @returns The previous sibling of the block that matches the identifier. * `undefined` if no matching block was found, or it's the first child/block * in the document. */ getPrevBlock(t) { return this._blockManager.getPrevBlock(t); } /** * Gets a snapshot of the next sibling of an existing block from the editor. * @param blockIdentifier The identifier of an existing block for which the * next sibling should be retrieved. * @returns The next sibling of the block that matches the identifier. * `undefined` if no matching block was found, or it's the last child/block in * the document. */ getNextBlock(t) { return this._blockManager.getNextBlock(t); } /** * Gets a snapshot of the parent of an existing block from the editor. * @param blockIdentifier The identifier of an existing block for which the * parent should be retrieved. * @returns The parent of the block that matches the identifier. `undefined` * if no matching block was found, or the block isn't nested. */ getParentBlock(t) { return this._blockManager.getParentBlock(t); } /** * Traverses all blocks in the editor depth-first, and executes a callback for each. * @param callback The callback to execute for each block. Returning `false` stops the traversal. * @param reverse Whether the blocks should be traversed in reverse order. */ forEachBlock(t, n = false) { this._blockManager.forEachBlock(t, n); } /** * Executes a callback whenever the editor's contents change. * @param callback The callback to execute. * * @deprecated use {@link BlockNoteEditor.onChange} instead */ onEditorContentChange(t) { this._tiptapEditor.on("update", t); } /** * Executes a callback whenever the editor's selection changes. * @param callback The callback to execute. * * @deprecated use `onSelectionChange` instead */ onEditorSelectionChange(t) { this._tiptapEditor.on("selectionUpdate", t); } /** * Executes a callback before any change is applied to the editor, allowing you to cancel the change. * @param callback The callback to execute. * @returns A function to remove the callback. */ onBeforeChange(t) { return this._extensionManager.getExtension(Do$1).subscribe(t); } /** * Gets a snapshot of the current text cursor position. * @returns A snapshot of the current text cursor position. */ getTextCursorPosition() { return this._selectionManager.getTextCursorPosition(); } /** * Sets the text cursor position to the start or end of an existing block. Throws an error if the target block could * not be found. * @param targetBlock The identifier of an existing block that the text cursor should be moved to. * @param placement Whether the text cursor should be placed at the start or end of the block. */ setTextCursorPosition(t, n = "start") { return this._selectionManager.setTextCursorPosition(t, n); } /** * Gets a snapshot of the current selection. This contains all blocks (included nested blocks) * that the selection spans across. * * If the selection starts / ends halfway through a block, the returned data will contain the entire block. */ getSelection() { return this._selectionManager.getSelection(); } /** * Gets a snapshot of the current selection. This contains all blocks (included nested blocks) * that the selection spans across. * * If the selection starts / ends halfway through a block, the returned block will be * only the part of the block that is included in the selection. */ getSelectionCutBlocks(t = false) { return this._selectionManager.getSelectionCutBlocks(t); } /** * Sets the selection to a range of blocks. * @param startBlock The identifier of the block that should be the start of the selection. * @param endBlock The identifier of the block that should be the end of the selection. */ setSelection(t, n) { return this._selectionManager.setSelection(t, n); } /** * Checks if the editor is currently editable, or if it's locked. * @returns True if the editor is editable, false otherwise. */ get isEditable() { return this._stateManager.isEditable; } /** * Makes the editor editable or locks it, depending on the argument passed. * @param editable True to make the editor editable, or false to lock it. */ set isEditable(t) { this._stateManager.isEditable = t; } /** * Inserts new blocks into the editor. If a block's `id` is undefined, BlockNote generates one automatically. Throws an * error if the reference block could not be found. * @param blocksToInsert An array of partial blocks that should be inserted. * @param referenceBlock An identifier for an existing block, at which the new blocks should be inserted. * @param placement Whether the blocks should be inserted just before, just after, or nested inside the * `referenceBlock`. */ insertBlocks(t, n, s = "before") { return this._blockManager.insertBlocks(t, n, s); } /** * Updates an existing block in the editor. Since updatedBlock is a PartialBlock object, some fields might not be * defined. These undefined fields are kept as-is from the existing block. Throws an error if the block to update could * not be found. * @param blockToUpdate The block that should be updated. * @param update A partial block which defines how the existing block should be changed. */ updateBlock(t, n) { return this._blockManager.updateBlock(t, n); } /** * Removes existing blocks from the editor. Throws an error if any of the blocks could not be found. * @param blocksToRemove An array of identifiers for existing blocks that should be removed. */ removeBlocks(t) { return this._blockManager.removeBlocks(t); } /** * Replaces existing blocks in the editor with new blocks. If the blocks that should be removed are not adjacent or * are at different nesting levels, `blocksToInsert` will be inserted at the position of the first block in * `blocksToRemove`. Throws an error if any of the blocks to remove could not be found. * @param blocksToRemove An array of blocks that should be replaced. * @param blocksToInsert An array of partial blocks to replace the old ones with. */ replaceBlocks(t, n) { return this._blockManager.replaceBlocks(t, n); } /** * Undo the last action. */ undo() { return this._stateManager.undo(); } /** * Redo the last action. */ redo() { return this._stateManager.redo(); } /** * Insert a piece of content at the current cursor position. * * @param content can be a string, or array of partial inline content elements */ insertInlineContent(t, { updateSelection: n = false } = {}) { this._styleManager.insertInlineContent(t, { updateSelection: n }); } /** * Gets the active text styles at the text cursor position or at the end of the current selection if it's active. */ getActiveStyles() { return this._styleManager.getActiveStyles(); } /** * Adds styles to the currently selected content. * @param styles The styles to add. */ addStyles(t) { this._styleManager.addStyles(t); } /** * Removes styles from the currently selected content. * @param styles The styles to remove. */ removeStyles(t) { this._styleManager.removeStyles(t); } /** * Toggles styles on the currently selected content. * @param styles The styles to toggle. */ toggleStyles(t) { this._styleManager.toggleStyles(t); } /** * Gets the currently selected text. */ getSelectedText() { return this._styleManager.getSelectedText(); } /** * Gets the URL of the last link in the current selection, or `undefined` if there are no links in the selection. */ getSelectedLinkUrl() { return this._styleManager.getSelectedLinkUrl(); } /** * Creates a new link to replace the selected content. * @param url The link URL. * @param text The text to display the link with. */ createLink(t, n) { this._styleManager.createLink(t, n); } /** * Checks if the block containing the text cursor can be nested. */ canNestBlock() { return this._blockManager.canNestBlock(); } /** * Nests the block containing the text cursor into the block above it. */ nestBlock() { this._blockManager.nestBlock(); } /** * Checks if the block containing the text cursor is nested. */ canUnnestBlock() { return this._blockManager.canUnnestBlock(); } /** * Lifts the block containing the text cursor out of its parent. */ unnestBlock() { this._blockManager.unnestBlock(); } /** * Moves the selected blocks up. If the previous block has children, moves * them to the end of its children. If there is no previous block, but the * current blocks share a common parent, moves them out of & before it. */ moveBlocksUp() { return this._blockManager.moveBlocksUp(); } /** * Moves the selected blocks down. If the next block has children, moves * them to the start of its children. If there is no next block, but the * current blocks share a common parent, moves them out of & after it. */ moveBlocksDown() { return this._blockManager.moveBlocksDown(); } /** * Exports blocks into a simplified HTML string. To better conform to HTML standards, children of blocks which aren't list * items are un-nested in the output HTML. * * @param blocks An array of blocks that should be serialized into HTML. * @returns The blocks, serialized as an HTML string. */ blocksToHTMLLossy(t = this.document) { return this._exportManager.blocksToHTMLLossy(t); } /** * Serializes blocks into an HTML string in the format that would normally be rendered by the editor. * * Use this method if you want to server-side render HTML (for example, a blog post that has been edited in BlockNote) * and serve it to users without loading the editor on the client (i.e.: displaying the blog post) * * @param blocks An array of blocks that should be serialized into HTML. * @returns The blocks, serialized as an HTML string. */ blocksToFullHTML(t = this.document) { return this._exportManager.blocksToFullHTML(t); } /** * Parses blocks from an HTML string. Tries to create `Block` objects out of any HTML block-level elements, and * `InlineNode` objects from any HTML inline elements, though not all element types are recognized. If BlockNote * doesn't recognize an HTML element's tag, it will parse it as a paragraph or plain text. * @param html The HTML string to parse blocks from. * @returns The blocks parsed from the HTML string. */ tryParseHTMLToBlocks(t) { return this._exportManager.tryParseHTMLToBlocks(t); } /** * Serializes blocks into a Markdown string. The output is simplified as Markdown does not support all features of * BlockNote - children of blocks which aren't list items are un-nested and certain styles are removed. * @param blocks An array of blocks that should be serialized into Markdown. * @returns The blocks, serialized as a Markdown string. */ blocksToMarkdownLossy(t = this.document) { return this._exportManager.blocksToMarkdownLossy(t); } /** * Creates a list of blocks from a Markdown string. Tries to create `Block` and `InlineNode` objects based on * Markdown syntax, though not all symbols are recognized. If BlockNote doesn't recognize a symbol, it will parse it * as text. * @param markdown The Markdown string to parse blocks from. * @returns The blocks parsed from the Markdown string. */ tryParseMarkdownToBlocks(t) { return this._exportManager.tryParseMarkdownToBlocks(t); } /** * A callback function that runs whenever the editor's contents change. * * @param callback The callback to execute. * @returns A function to remove the callback. */ onChange(t, n) { return this._eventManager.onChange(t, n); } /** * A callback function that runs whenever the text cursor position or selection changes. * * @param callback The callback to execute. * @returns A function to remove the callback. */ onSelectionChange(t, n) { return this._eventManager.onSelectionChange(t, n); } /** * A callback function that runs when the editor has been mounted. * * This can be useful for plugins to initialize themselves after the editor has been mounted. * * @param callback The callback to execute. * @returns A function to remove the callback. */ onMount(t) { this._eventManager.onMount(t); } /** * A callback function that runs when the editor has been unmounted. * * This can be useful for plugins to clean up themselves after the editor has been unmounted. * * @param callback The callback to execute. * @returns A function to remove the callback. */ onUnmount(t) { this._eventManager.onUnmount(t); } /** * Gets the bounding box of the current selection. * @returns The bounding box of the current selection. */ getSelectionBoundingBox() { return this._selectionManager.getSelectionBoundingBox(); } get isEmpty() { const t = this.document; return t.length === 0 || (t.length === 1 && t[0].type === "paragraph" && t[0].content.length === 0); } /** * Paste HTML into the editor. Defaults to converting HTML to BlockNote HTML. * @param html The HTML to paste. * @param raw Whether to paste the HTML as is, or to convert it to BlockNote HTML. */ pasteHTML(t, n = false) { this._exportManager.pasteHTML(t, n); } /** * Paste text into the editor. Defaults to interpreting text as markdown. * @param text The text to paste. */ pasteText(t) { return this._exportManager.pasteText(t); } /** * Paste markdown into the editor. * @param markdown The markdown to paste. */ pasteMarkdown(t) { return this._exportManager.pasteMarkdown(t); } } // src/components/blocknote-editor/schemas.ts var defaultSchema = h$1.create({ blockSpecs: { ...Po$2, }, styleSpecs: { ...$n$1, }, }); var { heading: _heading, table: _table, image: _image, video: _video, audio: _audio, file: _file, ...conservativeBlockSpecs } = Po$2; var conservativeSchema = h$1.create({ blockSpecs: conservativeBlockSpecs, styleSpecs: { ...$n$1, }, }); var courseContentSchema = h$1.create({ blockSpecs: { ...Po$2, }, styleSpecs: { ...$n$1, }, }); function r(e) { var t, f, n = ""; if ("string" == typeof e || "number" == typeof e) n += e; else if ("object" == typeof e) if (Array.isArray(e)) { var o = e.length; for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), (n += f)); } else for (f in e) e[f] && (n && (n += " "), (n += f)); return n; } function clsx() { for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), (n += t)); return n; } const CLASS_PART_SEPARATOR = "-"; const createClassGroupUtils = (config) => { const classMap = createClassMap(config); const { conflictingClassGroups, conflictingClassGroupModifiers } = config; const getClassGroupId = (className) => { const classParts = className.split(CLASS_PART_SEPARATOR); // Classes like `-inset-1` produce an empty string as first classPart. We assume that classes for negative values are used correctly and remove it from classParts. if (classParts[0] === "" && classParts.length !== 1) { classParts.shift(); } return getGroupRecursive(classParts, classMap) || getGroupIdForArbitraryProperty(className); }; const getConflictingClassGroupIds = (classGroupId, hasPostfixModifier) => { const conflicts = conflictingClassGroups[classGroupId] || []; if (hasPostfixModifier && conflictingClassGroupModifiers[classGroupId]) { return [...conflicts, ...conflictingClassGroupModifiers[classGroupId]]; } return conflicts; }; return { getClassGroupId, getConflictingClassGroupIds, }; }; const getGroupRecursive = (classParts, classPartObject) => { if (classParts.length === 0) { return classPartObject.classGroupId; } const currentClassPart = classParts[0]; const nextClassPartObject = classPartObject.nextPart.get(currentClassPart); const classGroupFromNextClassPart = nextClassPartObject ? getGroupRecursive(classParts.slice(1), nextClassPartObject) : undefined; if (classGroupFromNextClassPart) { return classGroupFromNextClassPart; } if (classPartObject.validators.length === 0) { return undefined; } const classRest = classParts.join(CLASS_PART_SEPARATOR); return classPartObject.validators.find(({ validator }) => validator(classRest))?.classGroupId; }; const arbitraryPropertyRegex = /^\[(.+)\]$/; const getGroupIdForArbitraryProperty = (className) => { if (arbitraryPropertyRegex.test(className)) { const arbitraryPropertyClassName = arbitraryPropertyRegex.exec(className)[1]; const property = arbitraryPropertyClassName?.substring(0, arbitraryPropertyClassName.indexOf(":")); if (property) { // I use two dots here because one dot is used as prefix for class groups in plugins return "arbitrary.." + property; } } }; /** * Exported for testing only */ const createClassMap = (config) => { const { theme, prefix } = config; const classMap = { nextPart: new Map(), validators: [], }; const prefixedClassGroupEntries = getPrefixedClassGroupEntries(Object.entries(config.classGroups), prefix); prefixedClassGroupEntries.forEach(([classGroupId, classGroup]) => { processClassesRecursively(classGroup, classMap, classGroupId, theme); }); return classMap; }; const processClassesRecursively = (classGroup, classPartObject, classGroupId, theme) => { classGroup.forEach((classDefinition) => { if (typeof classDefinition === "string") { const classPartObjectToEdit = classDefinition === "" ? classPartObject : getPart(classPartObject, classDefinition); classPartObjectToEdit.classGroupId = classGroupId; return; } if (typeof classDefinition === "function") { if (isThemeGetter(classDefinition)) { processClassesRecursively(classDefinition(theme), classPartObject, classGroupId, theme); return; } classPartObject.validators.push({ validator: classDefinition, classGroupId, }); return; } Object.entries(classDefinition).forEach(([key, classGroup]) => { processClassesRecursively(classGroup, getPart(classPartObject, key), classGroupId, theme); }); }); }; const getPart = (classPartObject, path) => { let currentClassPartObject = classPartObject; path.split(CLASS_PART_SEPARATOR).forEach((pathPart) => { if (!currentClassPartObject.nextPart.has(pathPart)) { currentClassPartObject.nextPart.set(pathPart, { nextPart: new Map(), validators: [], }); } currentClassPartObject = currentClassPartObject.nextPart.get(pathPart); }); return currentClassPartObject; }; const isThemeGetter = (func) => func.isThemeGetter; const getPrefixedClassGroupEntries = (classGroupEntries, prefix) => { if (!prefix) { return classGroupEntries; } return classGroupEntries.map(([classGroupId, classGroup]) => { const prefixedClassGroup = classGroup.map((classDefinition) => { if (typeof classDefinition === "string") { return prefix + classDefinition; } if (typeof classDefinition === "object") { return Object.fromEntries(Object.entries(classDefinition).map(([key, value]) => [prefix + key, value])); } return classDefinition; }); return [classGroupId, prefixedClassGroup]; }); }; // LRU cache inspired from hashlru (https://github.com/dominictarr/hashlru/blob/v1.0.4/index.js) but object replaced with Map to improve performance const createLruCache = (maxCacheSize) => { if (maxCacheSize < 1) { return { get: () => undefined, set: () => {}, }; } let cacheSize = 0; let cache = new Map(); let previousCache = new Map(); const update = (key, value) => { cache.set(key, value); cacheSize++; if (cacheSize > maxCacheSize) { cacheSize = 0; previousCache = cache; cache = new Map(); } }; return { get(key) { let value = cache.get(key); if (value !== undefined) { return value; } if ((value = previousCache.get(key)) !== undefined) { update(key, value); return value; } }, set(key, value) { if (cache.has(key)) { cache.set(key, value); } else { update(key, value); } }, }; }; const IMPORTANT_MODIFIER = "!"; const createParseClassName = (config) => { const { separator, experimentalParseClassName } = config; const isSeparatorSingleCharacter = separator.length === 1; const firstSeparatorCharacter = separator[0]; const separatorLength = separator.length; // parseClassName inspired by https://github.com/tailwindlabs/tailwindcss/blob/v3.2.2/src/util/splitAtTopLevelOnly.js const parseClassName = (className) => { const modifiers = []; let bracketDepth = 0; let modifierStart = 0; let postfixModifierPosition; for (let index = 0; index < className.length; index++) { let currentCharacter = className[index]; if (bracketDepth === 0) { if (currentCharacter === firstSeparatorCharacter && (isSeparatorSingleCharacter || className.slice(index, index + separatorLength) === separator)) { modifiers.push(className.slice(modifierStart, index)); modifierStart = index + separatorLength; continue; } if (currentCharacter === "/") { postfixModifierPosition = index; continue; } } if (currentCharacter === "[") { bracketDepth++; } else if (currentCharacter === "]") { bracketDepth--; } } const baseClassNameWithImportantModifier = modifiers.length === 0 ? className : className.substring(modifierStart); const hasImportantModifier = baseClassNameWithImportantModifier.startsWith(IMPORTANT_MODIFIER); const baseClassName = hasImportantModifier ? baseClassNameWithImportantModifier.substring(1) : baseClassNameWithImportantModifier; const maybePostfixModifierPosition = postfixModifierPosition && postfixModifierPosition > modifierStart ? postfixModifierPosition - modifierStart : undefined; return { modifiers, hasImportantModifier, baseClassName, maybePostfixModifierPosition, }; }; if (experimentalParseClassName) { return (className) => experimentalParseClassName({ className, parseClassName, }); } return parseClassName; }; /** * Sorts modifiers according to following schema: * - Predefined modifiers are sorted alphabetically * - When an arbitrary variant appears, it must be preserved which modifiers are before and after it */ const sortModifiers = (modifiers) => { if (modifiers.length <= 1) { return modifiers; } const sortedModifiers = []; let unsortedModifiers = []; modifiers.forEach((modifier) => { const isArbitraryVariant = modifier[0] === "["; if (isArbitraryVariant) { sortedModifiers.push(...unsortedModifiers.sort(), modifier); unsortedModifiers = []; } else { unsortedModifiers.push(modifier); } }); sortedModifiers.push(...unsortedModifiers.sort()); return sortedModifiers; }; const createConfigUtils = (config) => ({ cache: createLruCache(config.cacheSize), parseClassName: createParseClassName(config), ...createClassGroupUtils(config), }); const SPLIT_CLASSES_REGEX = /\s+/; const mergeClassList = (classList, configUtils) => { const { parseClassName, getClassGroupId, getConflictingClassGroupIds } = configUtils; /** * Set of classGroupIds in following format: * `{importantModifier}{variantModifiers}{classGroupId}` * @example 'float' * @example 'hover:focus:bg-color' * @example 'md:!pr' */ const classGroupsInConflict = []; const classNames = classList.trim().split(SPLIT_CLASSES_REGEX); let result = ""; for (let index = classNames.length - 1; index >= 0; index -= 1) { const originalClassName = classNames[index]; const { modifiers, hasImportantModifier, baseClassName, maybePostfixModifierPosition } = parseClassName(originalClassName); let hasPostfixModifier = Boolean(maybePostfixModifierPosition); let classGroupId = getClassGroupId(hasPostfixModifier ? baseClassName.substring(0, maybePostfixModifierPosition) : baseClassName); if (!classGroupId) { if (!hasPostfixModifier) { // Not a Tailwind class result = originalClassName + (result.length > 0 ? " " + result : result); continue; } classGroupId = getClassGroupId(baseClassName); if (!classGroupId) { // Not a Tailwind class result = originalClassName + (result.length > 0 ? " " + result : result); continue; } hasPostfixModifier = false; } const variantModifier = sortModifiers(modifiers).join(":"); const modifierId = hasImportantModifier ? variantModifier + IMPORTANT_MODIFIER : variantModifier; const classId = modifierId + classGroupId; if (classGroupsInConflict.includes(classId)) { // Tailwind class omitted due to conflict continue; } classGroupsInConflict.push(classId); const conflictGroups = getConflictingClassGroupIds(classGroupId, hasPostfixModifier); for (let i = 0; i < conflictGroups.length; ++i) { const group = conflictGroups[i]; classGroupsInConflict.push(modifierId + group); } // Tailwind class not in conflict result = originalClassName + (result.length > 0 ? " " + result : result); } return result; }; /** * The code in this file is copied from https://github.com/lukeed/clsx and modified to suit the needs of tailwind-merge better. * * Specifically: * - Runtime code from https://github.com/lukeed/clsx/blob/v1.2.1/src/index.js * - TypeScript types from https://github.com/lukeed/clsx/blob/v1.2.1/clsx.d.ts * * Original code has MIT license: Copyright (c) Luke Edwards (lukeed.com) */ function twJoin() { let index = 0; let argument; let resolvedValue; let string = ""; while (index < arguments.length) { if ((argument = arguments[index++])) { if ((resolvedValue = toValue(argument))) { string && (string += " "); string += resolvedValue; } } } return string; } const toValue = (mix) => { if (typeof mix === "string") { return mix; } let resolvedValue; let string = ""; for (let k = 0; k < mix.length; k++) { if (mix[k]) { if ((resolvedValue = toValue(mix[k]))) { string && (string += " "); string += resolvedValue; } } } return string; }; function createTailwindMerge(createConfigFirst, ...createConfigRest) { let configUtils; let cacheGet; let cacheSet; let functionToCall = initTailwindMerge; function initTailwindMerge(classList) { const config = createConfigRest.reduce((previousConfig, createConfigCurrent) => createConfigCurrent(previousConfig), createConfigFirst()); configUtils = createConfigUtils(config); cacheGet = configUtils.cache.get; cacheSet = configUtils.cache.set; functionToCall = tailwindMerge; return tailwindMerge(classList); } function tailwindMerge(classList) { const cachedResult = cacheGet(classList); if (cachedResult) { return cachedResult; } const result = mergeClassList(classList, configUtils); cacheSet(classList, result); return result; } return function callTailwindMerge() { return functionToCall(twJoin.apply(null, arguments)); }; } const fromTheme = (key) => { const themeGetter = (theme) => theme[key] || []; themeGetter.isThemeGetter = true; return themeGetter; }; const arbitraryValueRegex = /^\[(?:([a-z-]+):)?(.+)\]$/i; const fractionRegex = /^\d+\/\d+$/; const stringLengths = /*#__PURE__*/ new Set(["px", "full", "screen"]); const tshirtUnitRegex = /^(\d+(\.\d+)?)?(xs|sm|md|lg|xl)$/; const lengthUnitRegex = /\d+(%|px|r?em|[sdl]?v([hwib]|min|max)|pt|pc|in|cm|mm|cap|ch|ex|r?lh|cq(w|h|i|b|min|max))|\b(calc|min|max|clamp)\(.+\)|^0$/; const colorFunctionRegex = /^(rgba?|hsla?|hwb|(ok)?(lab|lch)|color-mix)\(.+\)$/; // Shadow always begins with x and y offset separated by underscore optionally prepended by inset const shadowRegex = /^(inset_)?-?((\d+)?\.?(\d+)[a-z]+|0)_-?((\d+)?\.?(\d+)[a-z]+|0)/; const imageRegex = /^(url|image|image-set|cross-fade|element|(repeating-)?(linear|radial|conic)-gradient)\(.+\)$/; const isLength = (value) => isNumber(value) || stringLengths.has(value) || fractionRegex.test(value); const isArbitraryLength = (value) => getIsArbitraryValue(value, "length", isLengthOnly); const isNumber = (value) => Boolean(value) && !Number.isNaN(Number(value)); const isArbitraryNumber = (value) => getIsArbitraryValue(value, "number", isNumber); const isInteger = (value) => Boolean(value) && Number.isInteger(Number(value)); const isPercent = (value) => value.endsWith("%") && isNumber(value.slice(0, -1)); const isArbitraryValue = (value) => arbitraryValueRegex.test(value); const isTshirtSize = (value) => tshirtUnitRegex.test(value); const sizeLabels = /*#__PURE__*/ new Set(["length", "size", "percentage"]); const isArbitrarySize = (value) => getIsArbitraryValue(value, sizeLabels, isNever); const isArbitraryPosition = (value) => getIsArbitraryValue(value, "position", isNever); const imageLabels = /*#__PURE__*/ new Set(["image", "url"]); const isArbitraryImage = (value) => getIsArbitraryValue(value, imageLabels, isImage); const isArbitraryShadow = (value) => getIsArbitraryValue(value, "", isShadow); const isAny = () => true; const getIsArbitraryValue = (value, label, testValue) => { const result = arbitraryValueRegex.exec(value); if (result) { if (result[1]) { return typeof label === "string" ? result[1] === label : label.has(result[1]); } return testValue(result[2]); } return false; }; const isLengthOnly = (value) => // `colorFunctionRegex` check is necessary because color functions can have percentages in them which which would be incorrectly classified as lengths. // For example, `hsl(0 0% 0%)` would be classified as a length without this check. // I could also use lookbehind assertion in `lengthUnitRegex` but that isn't supported widely enough. lengthUnitRegex.test(value) && !colorFunctionRegex.test(value); const isNever = () => false; const isShadow = (value) => shadowRegex.test(value); const isImage = (value) => imageRegex.test(value); const getDefaultConfig = () => { const colors = fromTheme("colors"); const spacing = fromTheme("spacing"); const blur = fromTheme("blur"); const brightness = fromTheme("brightness"); const borderColor = fromTheme("borderColor"); const borderRadius = fromTheme("borderRadius"); const borderSpacing = fromTheme("borderSpacing"); const borderWidth = fromTheme("borderWidth"); const contrast = fromTheme("contrast"); const grayscale = fromTheme("grayscale"); const hueRotate = fromTheme("hueRotate"); const invert = fromTheme("invert"); const gap = fromTheme("gap"); const gradientColorStops = fromTheme("gradientColorStops"); const gradientColorStopPositions = fromTheme("gradientColorStopPositions"); const inset = fromTheme("inset"); const margin = fromTheme("margin"); const opacity = fromTheme("opacity"); const padding = fromTheme("padding"); const saturate = fromTheme("saturate"); const scale = fromTheme("scale"); const sepia = fromTheme("sepia"); const skew = fromTheme("skew"); const space = fromTheme("space"); const translate = fromTheme("translate"); const getOverscroll = () => ["auto", "contain", "none"]; const getOverflow = () => ["auto", "hidden", "clip", "visible", "scroll"]; const getSpacingWithAutoAndArbitrary = () => ["auto", isArbitraryValue, spacing]; const getSpacingWithArbitrary = () => [isArbitraryValue, spacing]; const getLengthWithEmptyAndArbitrary = () => ["", isLength, isArbitraryLength]; const getNumberWithAutoAndArbitrary = () => ["auto", isNumber, isArbitraryValue]; const getPositions = () => ["bottom", "center", "left", "left-bottom", "left-top", "right", "right-bottom", "right-top", "top"]; const getLineStyles = () => ["solid", "dashed", "dotted", "double", "none"]; const getBlendModes = () => [ "normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge", "color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue", "saturation", "color", "luminosity", ]; const getAlign = () => ["start", "end", "center", "between", "around", "evenly", "stretch"]; const getZeroAndEmpty = () => ["", "0", isArbitraryValue]; const getBreaks = () => ["auto", "avoid", "all", "avoid-page", "page", "left", "right", "column"]; const getNumberAndArbitrary = () => [isNumber, isArbitraryValue]; return { cacheSize: 500, separator: ":", theme: { colors: [isAny], spacing: [isLength, isArbitraryLength], blur: ["none", "", isTshirtSize, isArbitraryValue], brightness: getNumberAndArbitrary(), borderColor: [colors], borderRadius: ["none", "", "full", isTshirtSize, isArbitraryValue], borderSpacing: getSpacingWithArbitrary(), borderWidth: getLengthWithEmptyAndArbitrary(), contrast: getNumberAndArbitrary(), grayscale: getZeroAndEmpty(), hueRotate: getNumberAndArbitrary(), invert: getZeroAndEmpty(), gap: getSpacingWithArbitrary(), gradientColorStops: [colors], gradientColorStopPositions: [isPercent, isArbitraryLength], inset: getSpacingWithAutoAndArbitrary(), margin: getSpacingWithAutoAndArbitrary(), opacity: getNumberAndArbitrary(), padding: getSpacingWithArbitrary(), saturate: getNumberAndArbitrary(), scale: getNumberAndArbitrary(), sepia: getZeroAndEmpty(), skew: getNumberAndArbitrary(), space: getSpacingWithArbitrary(), translate: getSpacingWithArbitrary(), }, classGroups: { // Layout /** * Aspect Ratio * @see https://tailwindcss.com/docs/aspect-ratio */ "aspect": [ { aspect: ["auto", "square", "video", isArbitraryValue], }, ], /** * Container * @see https://tailwindcss.com/docs/container */ "container": ["container"], /** * Columns * @see https://tailwindcss.com/docs/columns */ "columns": [ { columns: [isTshirtSize], }, ], /** * Break After * @see https://tailwindcss.com/docs/break-after */ "break-after": [ { "break-after": getBreaks(), }, ], /** * Break Before * @see https://tailwindcss.com/docs/break-before */ "break-before": [ { "break-before": getBreaks(), }, ], /** * Break Inside * @see https://tailwindcss.com/docs/break-inside */ "break-inside": [ { "break-inside": ["auto", "avoid", "avoid-page", "avoid-column"], }, ], /** * Box Decoration Break * @see https://tailwindcss.com/docs/box-decoration-break */ "box-decoration": [ { "box-decoration": ["slice", "clone"], }, ], /** * Box Sizing * @see https://tailwindcss.com/docs/box-sizing */ "box": [ { box: ["border", "content"], }, ], /** * Display * @see https://tailwindcss.com/docs/display */ "display": [ "block", "inline-block", "inline", "flex", "inline-flex", "table", "inline-table", "table-caption", "table-cell", "table-column", "table-column-group", "table-footer-group", "table-header-group", "table-row-group", "table-row", "flow-root", "grid", "inline-grid", "contents", "list-item", "hidden", ], /** * Floats * @see https://tailwindcss.com/docs/float */ "float": [ { float: ["right", "left", "none", "start", "end"], }, ], /** * Clear * @see https://tailwindcss.com/docs/clear */ "clear": [ { clear: ["left", "right", "both", "none", "start", "end"], }, ], /** * Isolation * @see https://tailwindcss.com/docs/isolation */ "isolation": ["isolate", "isolation-auto"], /** * Object Fit * @see https://tailwindcss.com/docs/object-fit */ "object-fit": [ { object: ["contain", "cover", "fill", "none", "scale-down"], }, ], /** * Object Position * @see https://tailwindcss.com/docs/object-position */ "object-position": [ { object: [...getPositions(), isArbitraryValue], }, ], /** * Overflow * @see https://tailwindcss.com/docs/overflow */ "overflow": [ { overflow: getOverflow(), }, ], /** * Overflow X * @see https://tailwindcss.com/docs/overflow */ "overflow-x": [ { "overflow-x": getOverflow(), }, ], /** * Overflow Y * @see https://tailwindcss.com/docs/overflow */ "overflow-y": [ { "overflow-y": getOverflow(), }, ], /** * Overscroll Behavior * @see https://tailwindcss.com/docs/overscroll-behavior */ "overscroll": [ { overscroll: getOverscroll(), }, ], /** * Overscroll Behavior X * @see https://tailwindcss.com/docs/overscroll-behavior */ "overscroll-x": [ { "overscroll-x": getOverscroll(), }, ], /** * Overscroll Behavior Y * @see https://tailwindcss.com/docs/overscroll-behavior */ "overscroll-y": [ { "overscroll-y": getOverscroll(), }, ], /** * Position * @see https://tailwindcss.com/docs/position */ "position": ["static", "fixed", "absolute", "relative", "sticky"], /** * Top / Right / Bottom / Left * @see https://tailwindcss.com/docs/top-right-bottom-left */ "inset": [ { inset: [inset], }, ], /** * Right / Left * @see https://tailwindcss.com/docs/top-right-bottom-left */ "inset-x": [ { "inset-x": [inset], }, ], /** * Top / Bottom * @see https://tailwindcss.com/docs/top-right-bottom-left */ "inset-y": [ { "inset-y": [inset], }, ], /** * Start * @see https://tailwindcss.com/docs/top-right-bottom-left */ "start": [ { start: [inset], }, ], /** * End * @see https://tailwindcss.com/docs/top-right-bottom-left */ "end": [ { end: [inset], }, ], /** * Top * @see https://tailwindcss.com/docs/top-right-bottom-left */ "top": [ { top: [inset], }, ], /** * Right * @see https://tailwindcss.com/docs/top-right-bottom-left */ "right": [ { right: [inset], }, ], /** * Bottom * @see https://tailwindcss.com/docs/top-right-bottom-left */ "bottom": [ { bottom: [inset], }, ], /** * Left * @see https://tailwindcss.com/docs/top-right-bottom-left */ "left": [ { left: [inset], }, ], /** * Visibility * @see https://tailwindcss.com/docs/visibility */ "visibility": ["visible", "invisible", "collapse"], /** * Z-Index * @see https://tailwindcss.com/docs/z-index */ "z": [ { z: ["auto", isInteger, isArbitraryValue], }, ], // Flexbox and Grid /** * Flex Basis * @see https://tailwindcss.com/docs/flex-basis */ "basis": [ { basis: getSpacingWithAutoAndArbitrary(), }, ], /** * Flex Direction * @see https://tailwindcss.com/docs/flex-direction */ "flex-direction": [ { flex: ["row", "row-reverse", "col", "col-reverse"], }, ], /** * Flex Wrap * @see https://tailwindcss.com/docs/flex-wrap */ "flex-wrap": [ { flex: ["wrap", "wrap-reverse", "nowrap"], }, ], /** * Flex * @see https://tailwindcss.com/docs/flex */ "flex": [ { flex: ["1", "auto", "initial", "none", isArbitraryValue], }, ], /** * Flex Grow * @see https://tailwindcss.com/docs/flex-grow */ "grow": [ { grow: getZeroAndEmpty(), }, ], /** * Flex Shrink * @see https://tailwindcss.com/docs/flex-shrink */ "shrink": [ { shrink: getZeroAndEmpty(), }, ], /** * Order * @see https://tailwindcss.com/docs/order */ "order": [ { order: ["first", "last", "none", isInteger, isArbitraryValue], }, ], /** * Grid Template Columns * @see https://tailwindcss.com/docs/grid-template-columns */ "grid-cols": [ { "grid-cols": [isAny], }, ], /** * Grid Column Start / End * @see https://tailwindcss.com/docs/grid-column */ "col-start-end": [ { col: [ "auto", { span: ["full", isInteger, isArbitraryValue], }, isArbitraryValue, ], }, ], /** * Grid Column Start * @see https://tailwindcss.com/docs/grid-column */ "col-start": [ { "col-start": getNumberWithAutoAndArbitrary(), }, ], /** * Grid Column End * @see https://tailwindcss.com/docs/grid-column */ "col-end": [ { "col-end": getNumberWithAutoAndArbitrary(), }, ], /** * Grid Template Rows * @see https://tailwindcss.com/docs/grid-template-rows */ "grid-rows": [ { "grid-rows": [isAny], }, ], /** * Grid Row Start / End * @see https://tailwindcss.com/docs/grid-row */ "row-start-end": [ { row: [ "auto", { span: [isInteger, isArbitraryValue], }, isArbitraryValue, ], }, ], /** * Grid Row Start * @see https://tailwindcss.com/docs/grid-row */ "row-start": [ { "row-start": getNumberWithAutoAndArbitrary(), }, ], /** * Grid Row End * @see https://tailwindcss.com/docs/grid-row */ "row-end": [ { "row-end": getNumberWithAutoAndArbitrary(), }, ], /** * Grid Auto Flow * @see https://tailwindcss.com/docs/grid-auto-flow */ "grid-flow": [ { "grid-flow": ["row", "col", "dense", "row-dense", "col-dense"], }, ], /** * Grid Auto Columns * @see https://tailwindcss.com/docs/grid-auto-columns */ "auto-cols": [ { "auto-cols": ["auto", "min", "max", "fr", isArbitraryValue], }, ], /** * Grid Auto Rows * @see https://tailwindcss.com/docs/grid-auto-rows */ "auto-rows": [ { "auto-rows": ["auto", "min", "max", "fr", isArbitraryValue], }, ], /** * Gap * @see https://tailwindcss.com/docs/gap */ "gap": [ { gap: [gap], }, ], /** * Gap X * @see https://tailwindcss.com/docs/gap */ "gap-x": [ { "gap-x": [gap], }, ], /** * Gap Y * @see https://tailwindcss.com/docs/gap */ "gap-y": [ { "gap-y": [gap], }, ], /** * Justify Content * @see https://tailwindcss.com/docs/justify-content */ "justify-content": [ { justify: ["normal", ...getAlign()], }, ], /** * Justify Items * @see https://tailwindcss.com/docs/justify-items */ "justify-items": [ { "justify-items": ["start", "end", "center", "stretch"], }, ], /** * Justify Self * @see https://tailwindcss.com/docs/justify-self */ "justify-self": [ { "justify-self": ["auto", "start", "end", "center", "stretch"], }, ], /** * Align Content * @see https://tailwindcss.com/docs/align-content */ "align-content": [ { content: ["normal", ...getAlign(), "baseline"], }, ], /** * Align Items * @see https://tailwindcss.com/docs/align-items */ "align-items": [ { items: ["start", "end", "center", "baseline", "stretch"], }, ], /** * Align Self * @see https://tailwindcss.com/docs/align-self */ "align-self": [ { self: ["auto", "start", "end", "center", "stretch", "baseline"], }, ], /** * Place Content * @see https://tailwindcss.com/docs/place-content */ "place-content": [ { "place-content": [...getAlign(), "baseline"], }, ], /** * Place Items * @see https://tailwindcss.com/docs/place-items */ "place-items": [ { "place-items": ["start", "end", "center", "baseline", "stretch"], }, ], /** * Place Self * @see https://tailwindcss.com/docs/place-self */ "place-self": [ { "place-self": ["auto", "start", "end", "center", "stretch"], }, ], // Spacing /** * Padding * @see https://tailwindcss.com/docs/padding */ "p": [ { p: [padding], }, ], /** * Padding X * @see https://tailwindcss.com/docs/padding */ "px": [ { px: [padding], }, ], /** * Padding Y * @see https://tailwindcss.com/docs/padding */ "py": [ { py: [padding], }, ], /** * Padding Start * @see https://tailwindcss.com/docs/padding */ "ps": [ { ps: [padding], }, ], /** * Padding End * @see https://tailwindcss.com/docs/padding */ "pe": [ { pe: [padding], }, ], /** * Padding Top * @see https://tailwindcss.com/docs/padding */ "pt": [ { pt: [padding], }, ], /** * Padding Right * @see https://tailwindcss.com/docs/padding */ "pr": [ { pr: [padding], }, ], /** * Padding Bottom * @see https://tailwindcss.com/docs/padding */ "pb": [ { pb: [padding], }, ], /** * Padding Left * @see https://tailwindcss.com/docs/padding */ "pl": [ { pl: [padding], }, ], /** * Margin * @see https://tailwindcss.com/docs/margin */ "m": [ { m: [margin], }, ], /** * Margin X * @see https://tailwindcss.com/docs/margin */ "mx": [ { mx: [margin], }, ], /** * Margin Y * @see https://tailwindcss.com/docs/margin */ "my": [ { my: [margin], }, ], /** * Margin Start * @see https://tailwindcss.com/docs/margin */ "ms": [ { ms: [margin], }, ], /** * Margin End * @see https://tailwindcss.com/docs/margin */ "me": [ { me: [margin], }, ], /** * Margin Top * @see https://tailwindcss.com/docs/margin */ "mt": [ { mt: [margin], }, ], /** * Margin Right * @see https://tailwindcss.com/docs/margin */ "mr": [ { mr: [margin], }, ], /** * Margin Bottom * @see https://tailwindcss.com/docs/margin */ "mb": [ { mb: [margin], }, ], /** * Margin Left * @see https://tailwindcss.com/docs/margin */ "ml": [ { ml: [margin], }, ], /** * Space Between X * @see https://tailwindcss.com/docs/space */ "space-x": [ { "space-x": [space], }, ], /** * Space Between X Reverse * @see https://tailwindcss.com/docs/space */ "space-x-reverse": ["space-x-reverse"], /** * Space Between Y * @see https://tailwindcss.com/docs/space */ "space-y": [ { "space-y": [space], }, ], /** * Space Between Y Reverse * @see https://tailwindcss.com/docs/space */ "space-y-reverse": ["space-y-reverse"], // Sizing /** * Width * @see https://tailwindcss.com/docs/width */ "w": [ { w: ["auto", "min", "max", "fit", "svw", "lvw", "dvw", isArbitraryValue, spacing], }, ], /** * Min-Width * @see https://tailwindcss.com/docs/min-width */ "min-w": [ { "min-w": [isArbitraryValue, spacing, "min", "max", "fit"], }, ], /** * Max-Width * @see https://tailwindcss.com/docs/max-width */ "max-w": [ { "max-w": [ isArbitraryValue, spacing, "none", "full", "min", "max", "fit", "prose", { screen: [isTshirtSize], }, isTshirtSize, ], }, ], /** * Height * @see https://tailwindcss.com/docs/height */ "h": [ { h: [isArbitraryValue, spacing, "auto", "min", "max", "fit", "svh", "lvh", "dvh"], }, ], /** * Min-Height * @see https://tailwindcss.com/docs/min-height */ "min-h": [ { "min-h": [isArbitraryValue, spacing, "min", "max", "fit", "svh", "lvh", "dvh"], }, ], /** * Max-Height * @see https://tailwindcss.com/docs/max-height */ "max-h": [ { "max-h": [isArbitraryValue, spacing, "min", "max", "fit", "svh", "lvh", "dvh"], }, ], /** * Size * @see https://tailwindcss.com/docs/size */ "size": [ { size: [isArbitraryValue, spacing, "auto", "min", "max", "fit"], }, ], // Typography /** * Font Size * @see https://tailwindcss.com/docs/font-size */ "font-size": [ { text: ["base", isTshirtSize, isArbitraryLength], }, ], /** * Font Smoothing * @see https://tailwindcss.com/docs/font-smoothing */ "font-smoothing": ["antialiased", "subpixel-antialiased"], /** * Font Style * @see https://tailwindcss.com/docs/font-style */ "font-style": ["italic", "not-italic"], /** * Font Weight * @see https://tailwindcss.com/docs/font-weight */ "font-weight": [ { font: ["thin", "extralight", "light", "normal", "medium", "semibold", "bold", "extrabold", "black", isArbitraryNumber], }, ], /** * Font Family * @see https://tailwindcss.com/docs/font-family */ "font-family": [ { font: [isAny], }, ], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric */ "fvn-normal": ["normal-nums"], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric */ "fvn-ordinal": ["ordinal"], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric */ "fvn-slashed-zero": ["slashed-zero"], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric */ "fvn-figure": ["lining-nums", "oldstyle-nums"], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric */ "fvn-spacing": ["proportional-nums", "tabular-nums"], /** * Font Variant Numeric * @see https://tailwindcss.com/docs/font-variant-numeric */ "fvn-fraction": ["diagonal-fractions", "stacked-fractions"], /** * Letter Spacing * @see https://tailwindcss.com/docs/letter-spacing */ "tracking": [ { tracking: ["tighter", "tight", "normal", "wide", "wider", "widest", isArbitraryValue], }, ], /** * Line Clamp * @see https://tailwindcss.com/docs/line-clamp */ "line-clamp": [ { "line-clamp": ["none", isNumber, isArbitraryNumber], }, ], /** * Line Height * @see https://tailwindcss.com/docs/line-height */ "leading": [ { leading: ["none", "tight", "snug", "normal", "relaxed", "loose", isLength, isArbitraryValue], }, ], /** * List Style Image * @see https://tailwindcss.com/docs/list-style-image */ "list-image": [ { "list-image": ["none", isArbitraryValue], }, ], /** * List Style Type * @see https://tailwindcss.com/docs/list-style-type */ "list-style-type": [ { list: ["none", "disc", "decimal", isArbitraryValue], }, ], /** * List Style Position * @see https://tailwindcss.com/docs/list-style-position */ "list-style-position": [ { list: ["inside", "outside"], }, ], /** * Placeholder Color * @deprecated since Tailwind CSS v3.0.0 * @see https://tailwindcss.com/docs/placeholder-color */ "placeholder-color": [ { placeholder: [colors], }, ], /** * Placeholder Opacity * @see https://tailwindcss.com/docs/placeholder-opacity */ "placeholder-opacity": [ { "placeholder-opacity": [opacity], }, ], /** * Text Alignment * @see https://tailwindcss.com/docs/text-align */ "text-alignment": [ { text: ["left", "center", "right", "justify", "start", "end"], }, ], /** * Text Color * @see https://tailwindcss.com/docs/text-color */ "text-color": [ { text: [colors], }, ], /** * Text Opacity * @see https://tailwindcss.com/docs/text-opacity */ "text-opacity": [ { "text-opacity": [opacity], }, ], /** * Text Decoration * @see https://tailwindcss.com/docs/text-decoration */ "text-decoration": ["underline", "overline", "line-through", "no-underline"], /** * Text Decoration Style * @see https://tailwindcss.com/docs/text-decoration-style */ "text-decoration-style": [ { decoration: [...getLineStyles(), "wavy"], }, ], /** * Text Decoration Thickness * @see https://tailwindcss.com/docs/text-decoration-thickness */ "text-decoration-thickness": [ { decoration: ["auto", "from-font", isLength, isArbitraryLength], }, ], /** * Text Underline Offset * @see https://tailwindcss.com/docs/text-underline-offset */ "underline-offset": [ { "underline-offset": ["auto", isLength, isArbitraryValue], }, ], /** * Text Decoration Color * @see https://tailwindcss.com/docs/text-decoration-color */ "text-decoration-color": [ { decoration: [colors], }, ], /** * Text Transform * @see https://tailwindcss.com/docs/text-transform */ "text-transform": ["uppercase", "lowercase", "capitalize", "normal-case"], /** * Text Overflow * @see https://tailwindcss.com/docs/text-overflow */ "text-overflow": ["truncate", "text-ellipsis", "text-clip"], /** * Text Wrap * @see https://tailwindcss.com/docs/text-wrap */ "text-wrap": [ { text: ["wrap", "nowrap", "balance", "pretty"], }, ], /** * Text Indent * @see https://tailwindcss.com/docs/text-indent */ "indent": [ { indent: getSpacingWithArbitrary(), }, ], /** * Vertical Alignment * @see https://tailwindcss.com/docs/vertical-align */ "vertical-align": [ { align: ["baseline", "top", "middle", "bottom", "text-top", "text-bottom", "sub", "super", isArbitraryValue], }, ], /** * Whitespace * @see https://tailwindcss.com/docs/whitespace */ "whitespace": [ { whitespace: ["normal", "nowrap", "pre", "pre-line", "pre-wrap", "break-spaces"], }, ], /** * Word Break * @see https://tailwindcss.com/docs/word-break */ "break": [ { break: ["normal", "words", "all", "keep"], }, ], /** * Hyphens * @see https://tailwindcss.com/docs/hyphens */ "hyphens": [ { hyphens: ["none", "manual", "auto"], }, ], /** * Content * @see https://tailwindcss.com/docs/content */ "content": [ { content: ["none", isArbitraryValue], }, ], // Backgrounds /** * Background Attachment * @see https://tailwindcss.com/docs/background-attachment */ "bg-attachment": [ { bg: ["fixed", "local", "scroll"], }, ], /** * Background Clip * @see https://tailwindcss.com/docs/background-clip */ "bg-clip": [ { "bg-clip": ["border", "padding", "content", "text"], }, ], /** * Background Opacity * @deprecated since Tailwind CSS v3.0.0 * @see https://tailwindcss.com/docs/background-opacity */ "bg-opacity": [ { "bg-opacity": [opacity], }, ], /** * Background Origin * @see https://tailwindcss.com/docs/background-origin */ "bg-origin": [ { "bg-origin": ["border", "padding", "content"], }, ], /** * Background Position * @see https://tailwindcss.com/docs/background-position */ "bg-position": [ { bg: [...getPositions(), isArbitraryPosition], }, ], /** * Background Repeat * @see https://tailwindcss.com/docs/background-repeat */ "bg-repeat": [ { bg: [ "no-repeat", { repeat: ["", "x", "y", "round", "space"], }, ], }, ], /** * Background Size * @see https://tailwindcss.com/docs/background-size */ "bg-size": [ { bg: ["auto", "cover", "contain", isArbitrarySize], }, ], /** * Background Image * @see https://tailwindcss.com/docs/background-image */ "bg-image": [ { bg: [ "none", { "gradient-to": ["t", "tr", "r", "br", "b", "bl", "l", "tl"], }, isArbitraryImage, ], }, ], /** * Background Color * @see https://tailwindcss.com/docs/background-color */ "bg-color": [ { bg: [colors], }, ], /** * Gradient Color Stops From Position * @see https://tailwindcss.com/docs/gradient-color-stops */ "gradient-from-pos": [ { from: [gradientColorStopPositions], }, ], /** * Gradient Color Stops Via Position * @see https://tailwindcss.com/docs/gradient-color-stops */ "gradient-via-pos": [ { via: [gradientColorStopPositions], }, ], /** * Gradient Color Stops To Position * @see https://tailwindcss.com/docs/gradient-color-stops */ "gradient-to-pos": [ { to: [gradientColorStopPositions], }, ], /** * Gradient Color Stops From * @see https://tailwindcss.com/docs/gradient-color-stops */ "gradient-from": [ { from: [gradientColorStops], }, ], /** * Gradient Color Stops Via * @see https://tailwindcss.com/docs/gradient-color-stops */ "gradient-via": [ { via: [gradientColorStops], }, ], /** * Gradient Color Stops To * @see https://tailwindcss.com/docs/gradient-color-stops */ "gradient-to": [ { to: [gradientColorStops], }, ], // Borders /** * Border Radius * @see https://tailwindcss.com/docs/border-radius */ "rounded": [ { rounded: [borderRadius], }, ], /** * Border Radius Start * @see https://tailwindcss.com/docs/border-radius */ "rounded-s": [ { "rounded-s": [borderRadius], }, ], /** * Border Radius End * @see https://tailwindcss.com/docs/border-radius */ "rounded-e": [ { "rounded-e": [borderRadius], }, ], /** * Border Radius Top * @see https://tailwindcss.com/docs/border-radius */ "rounded-t": [ { "rounded-t": [borderRadius], }, ], /** * Border Radius Right * @see https://tailwindcss.com/docs/border-radius */ "rounded-r": [ { "rounded-r": [borderRadius], }, ], /** * Border Radius Bottom * @see https://tailwindcss.com/docs/border-radius */ "rounded-b": [ { "rounded-b": [borderRadius], }, ], /** * Border Radius Left * @see https://tailwindcss.com/docs/border-radius */ "rounded-l": [ { "rounded-l": [borderRadius], }, ], /** * Border Radius Start Start * @see https://tailwindcss.com/docs/border-radius */ "rounded-ss": [ { "rounded-ss": [borderRadius], }, ], /** * Border Radius Start End * @see https://tailwindcss.com/docs/border-radius */ "rounded-se": [ { "rounded-se": [borderRadius], }, ], /** * Border Radius End End * @see https://tailwindcss.com/docs/border-radius */ "rounded-ee": [ { "rounded-ee": [borderRadius], }, ], /** * Border Radius End Start * @see https://tailwindcss.com/docs/border-radius */ "rounded-es": [ { "rounded-es": [borderRadius], }, ], /** * Border Radius Top Left * @see https://tailwindcss.com/docs/border-radius */ "rounded-tl": [ { "rounded-tl": [borderRadius], }, ], /** * Border Radius Top Right * @see https://tailwindcss.com/docs/border-radius */ "rounded-tr": [ { "rounded-tr": [borderRadius], }, ], /** * Border Radius Bottom Right * @see https://tailwindcss.com/docs/border-radius */ "rounded-br": [ { "rounded-br": [borderRadius], }, ], /** * Border Radius Bottom Left * @see https://tailwindcss.com/docs/border-radius */ "rounded-bl": [ { "rounded-bl": [borderRadius], }, ], /** * Border Width * @see https://tailwindcss.com/docs/border-width */ "border-w": [ { border: [borderWidth], }, ], /** * Border Width X * @see https://tailwindcss.com/docs/border-width */ "border-w-x": [ { "border-x": [borderWidth], }, ], /** * Border Width Y * @see https://tailwindcss.com/docs/border-width */ "border-w-y": [ { "border-y": [borderWidth], }, ], /** * Border Width Start * @see https://tailwindcss.com/docs/border-width */ "border-w-s": [ { "border-s": [borderWidth], }, ], /** * Border Width End * @see https://tailwindcss.com/docs/border-width */ "border-w-e": [ { "border-e": [borderWidth], }, ], /** * Border Width Top * @see https://tailwindcss.com/docs/border-width */ "border-w-t": [ { "border-t": [borderWidth], }, ], /** * Border Width Right * @see https://tailwindcss.com/docs/border-width */ "border-w-r": [ { "border-r": [borderWidth], }, ], /** * Border Width Bottom * @see https://tailwindcss.com/docs/border-width */ "border-w-b": [ { "border-b": [borderWidth], }, ], /** * Border Width Left * @see https://tailwindcss.com/docs/border-width */ "border-w-l": [ { "border-l": [borderWidth], }, ], /** * Border Opacity * @see https://tailwindcss.com/docs/border-opacity */ "border-opacity": [ { "border-opacity": [opacity], }, ], /** * Border Style * @see https://tailwindcss.com/docs/border-style */ "border-style": [ { border: [...getLineStyles(), "hidden"], }, ], /** * Divide Width X * @see https://tailwindcss.com/docs/divide-width */ "divide-x": [ { "divide-x": [borderWidth], }, ], /** * Divide Width X Reverse * @see https://tailwindcss.com/docs/divide-width */ "divide-x-reverse": ["divide-x-reverse"], /** * Divide Width Y * @see https://tailwindcss.com/docs/divide-width */ "divide-y": [ { "divide-y": [borderWidth], }, ], /** * Divide Width Y Reverse * @see https://tailwindcss.com/docs/divide-width */ "divide-y-reverse": ["divide-y-reverse"], /** * Divide Opacity * @see https://tailwindcss.com/docs/divide-opacity */ "divide-opacity": [ { "divide-opacity": [opacity], }, ], /** * Divide Style * @see https://tailwindcss.com/docs/divide-style */ "divide-style": [ { divide: getLineStyles(), }, ], /** * Border Color * @see https://tailwindcss.com/docs/border-color */ "border-color": [ { border: [borderColor], }, ], /** * Border Color X * @see https://tailwindcss.com/docs/border-color */ "border-color-x": [ { "border-x": [borderColor], }, ], /** * Border Color Y * @see https://tailwindcss.com/docs/border-color */ "border-color-y": [ { "border-y": [borderColor], }, ], /** * Border Color S * @see https://tailwindcss.com/docs/border-color */ "border-color-s": [ { "border-s": [borderColor], }, ], /** * Border Color E * @see https://tailwindcss.com/docs/border-color */ "border-color-e": [ { "border-e": [borderColor], }, ], /** * Border Color Top * @see https://tailwindcss.com/docs/border-color */ "border-color-t": [ { "border-t": [borderColor], }, ], /** * Border Color Right * @see https://tailwindcss.com/docs/border-color */ "border-color-r": [ { "border-r": [borderColor], }, ], /** * Border Color Bottom * @see https://tailwindcss.com/docs/border-color */ "border-color-b": [ { "border-b": [borderColor], }, ], /** * Border Color Left * @see https://tailwindcss.com/docs/border-color */ "border-color-l": [ { "border-l": [borderColor], }, ], /** * Divide Color * @see https://tailwindcss.com/docs/divide-color */ "divide-color": [ { divide: [borderColor], }, ], /** * Outline Style * @see https://tailwindcss.com/docs/outline-style */ "outline-style": [ { outline: ["", ...getLineStyles()], }, ], /** * Outline Offset * @see https://tailwindcss.com/docs/outline-offset */ "outline-offset": [ { "outline-offset": [isLength, isArbitraryValue], }, ], /** * Outline Width * @see https://tailwindcss.com/docs/outline-width */ "outline-w": [ { outline: [isLength, isArbitraryLength], }, ], /** * Outline Color * @see https://tailwindcss.com/docs/outline-color */ "outline-color": [ { outline: [colors], }, ], /** * Ring Width * @see https://tailwindcss.com/docs/ring-width */ "ring-w": [ { ring: getLengthWithEmptyAndArbitrary(), }, ], /** * Ring Width Inset * @see https://tailwindcss.com/docs/ring-width */ "ring-w-inset": ["ring-inset"], /** * Ring Color * @see https://tailwindcss.com/docs/ring-color */ "ring-color": [ { ring: [colors], }, ], /** * Ring Opacity * @see https://tailwindcss.com/docs/ring-opacity */ "ring-opacity": [ { "ring-opacity": [opacity], }, ], /** * Ring Offset Width * @see https://tailwindcss.com/docs/ring-offset-width */ "ring-offset-w": [ { "ring-offset": [isLength, isArbitraryLength], }, ], /** * Ring Offset Color * @see https://tailwindcss.com/docs/ring-offset-color */ "ring-offset-color": [ { "ring-offset": [colors], }, ], // Effects /** * Box Shadow * @see https://tailwindcss.com/docs/box-shadow */ "shadow": [ { shadow: ["", "inner", "none", isTshirtSize, isArbitraryShadow], }, ], /** * Box Shadow Color * @see https://tailwindcss.com/docs/box-shadow-color */ "shadow-color": [ { shadow: [isAny], }, ], /** * Opacity * @see https://tailwindcss.com/docs/opacity */ "opacity": [ { opacity: [opacity], }, ], /** * Mix Blend Mode * @see https://tailwindcss.com/docs/mix-blend-mode */ "mix-blend": [ { "mix-blend": [...getBlendModes(), "plus-lighter", "plus-darker"], }, ], /** * Background Blend Mode * @see https://tailwindcss.com/docs/background-blend-mode */ "bg-blend": [ { "bg-blend": getBlendModes(), }, ], // Filters /** * Filter * @deprecated since Tailwind CSS v3.0.0 * @see https://tailwindcss.com/docs/filter */ "filter": [ { filter: ["", "none"], }, ], /** * Blur * @see https://tailwindcss.com/docs/blur */ "blur": [ { blur: [blur], }, ], /** * Brightness * @see https://tailwindcss.com/docs/brightness */ "brightness": [ { brightness: [brightness], }, ], /** * Contrast * @see https://tailwindcss.com/docs/contrast */ "contrast": [ { contrast: [contrast], }, ], /** * Drop Shadow * @see https://tailwindcss.com/docs/drop-shadow */ "drop-shadow": [ { "drop-shadow": ["", "none", isTshirtSize, isArbitraryValue], }, ], /** * Grayscale * @see https://tailwindcss.com/docs/grayscale */ "grayscale": [ { grayscale: [grayscale], }, ], /** * Hue Rotate * @see https://tailwindcss.com/docs/hue-rotate */ "hue-rotate": [ { "hue-rotate": [hueRotate], }, ], /** * Invert * @see https://tailwindcss.com/docs/invert */ "invert": [ { invert: [invert], }, ], /** * Saturate * @see https://tailwindcss.com/docs/saturate */ "saturate": [ { saturate: [saturate], }, ], /** * Sepia * @see https://tailwindcss.com/docs/sepia */ "sepia": [ { sepia: [sepia], }, ], /** * Backdrop Filter * @deprecated since Tailwind CSS v3.0.0 * @see https://tailwindcss.com/docs/backdrop-filter */ "backdrop-filter": [ { "backdrop-filter": ["", "none"], }, ], /** * Backdrop Blur * @see https://tailwindcss.com/docs/backdrop-blur */ "backdrop-blur": [ { "backdrop-blur": [blur], }, ], /** * Backdrop Brightness * @see https://tailwindcss.com/docs/backdrop-brightness */ "backdrop-brightness": [ { "backdrop-brightness": [brightness], }, ], /** * Backdrop Contrast * @see https://tailwindcss.com/docs/backdrop-contrast */ "backdrop-contrast": [ { "backdrop-contrast": [contrast], }, ], /** * Backdrop Grayscale * @see https://tailwindcss.com/docs/backdrop-grayscale */ "backdrop-grayscale": [ { "backdrop-grayscale": [grayscale], }, ], /** * Backdrop Hue Rotate * @see https://tailwindcss.com/docs/backdrop-hue-rotate */ "backdrop-hue-rotate": [ { "backdrop-hue-rotate": [hueRotate], }, ], /** * Backdrop Invert * @see https://tailwindcss.com/docs/backdrop-invert */ "backdrop-invert": [ { "backdrop-invert": [invert], }, ], /** * Backdrop Opacity * @see https://tailwindcss.com/docs/backdrop-opacity */ "backdrop-opacity": [ { "backdrop-opacity": [opacity], }, ], /** * Backdrop Saturate * @see https://tailwindcss.com/docs/backdrop-saturate */ "backdrop-saturate": [ { "backdrop-saturate": [saturate], }, ], /** * Backdrop Sepia * @see https://tailwindcss.com/docs/backdrop-sepia */ "backdrop-sepia": [ { "backdrop-sepia": [sepia], }, ], // Tables /** * Border Collapse * @see https://tailwindcss.com/docs/border-collapse */ "border-collapse": [ { border: ["collapse", "separate"], }, ], /** * Border Spacing * @see https://tailwindcss.com/docs/border-spacing */ "border-spacing": [ { "border-spacing": [borderSpacing], }, ], /** * Border Spacing X * @see https://tailwindcss.com/docs/border-spacing */ "border-spacing-x": [ { "border-spacing-x": [borderSpacing], }, ], /** * Border Spacing Y * @see https://tailwindcss.com/docs/border-spacing */ "border-spacing-y": [ { "border-spacing-y": [borderSpacing], }, ], /** * Table Layout * @see https://tailwindcss.com/docs/table-layout */ "table-layout": [ { table: ["auto", "fixed"], }, ], /** * Caption Side * @see https://tailwindcss.com/docs/caption-side */ "caption": [ { caption: ["top", "bottom"], }, ], // Transitions and Animation /** * Tranisition Property * @see https://tailwindcss.com/docs/transition-property */ "transition": [ { transition: ["none", "all", "", "colors", "opacity", "shadow", "transform", isArbitraryValue], }, ], /** * Transition Duration * @see https://tailwindcss.com/docs/transition-duration */ "duration": [ { duration: getNumberAndArbitrary(), }, ], /** * Transition Timing Function * @see https://tailwindcss.com/docs/transition-timing-function */ "ease": [ { ease: ["linear", "in", "out", "in-out", isArbitraryValue], }, ], /** * Transition Delay * @see https://tailwindcss.com/docs/transition-delay */ "delay": [ { delay: getNumberAndArbitrary(), }, ], /** * Animation * @see https://tailwindcss.com/docs/animation */ "animate": [ { animate: ["none", "spin", "ping", "pulse", "bounce", isArbitraryValue], }, ], // Transforms /** * Transform * @see https://tailwindcss.com/docs/transform */ "transform": [ { transform: ["", "gpu", "none"], }, ], /** * Scale * @see https://tailwindcss.com/docs/scale */ "scale": [ { scale: [scale], }, ], /** * Scale X * @see https://tailwindcss.com/docs/scale */ "scale-x": [ { "scale-x": [scale], }, ], /** * Scale Y * @see https://tailwindcss.com/docs/scale */ "scale-y": [ { "scale-y": [scale], }, ], /** * Rotate * @see https://tailwindcss.com/docs/rotate */ "rotate": [ { rotate: [isInteger, isArbitraryValue], }, ], /** * Translate X * @see https://tailwindcss.com/docs/translate */ "translate-x": [ { "translate-x": [translate], }, ], /** * Translate Y * @see https://tailwindcss.com/docs/translate */ "translate-y": [ { "translate-y": [translate], }, ], /** * Skew X * @see https://tailwindcss.com/docs/skew */ "skew-x": [ { "skew-x": [skew], }, ], /** * Skew Y * @see https://tailwindcss.com/docs/skew */ "skew-y": [ { "skew-y": [skew], }, ], /** * Transform Origin * @see https://tailwindcss.com/docs/transform-origin */ "transform-origin": [ { origin: ["center", "top", "top-right", "right", "bottom-right", "bottom", "bottom-left", "left", "top-left", isArbitraryValue], }, ], // Interactivity /** * Accent Color * @see https://tailwindcss.com/docs/accent-color */ "accent": [ { accent: ["auto", colors], }, ], /** * Appearance * @see https://tailwindcss.com/docs/appearance */ "appearance": [ { appearance: ["none", "auto"], }, ], /** * Cursor * @see https://tailwindcss.com/docs/cursor */ "cursor": [ { cursor: [ "auto", "default", "pointer", "wait", "text", "move", "help", "not-allowed", "none", "context-menu", "progress", "cell", "crosshair", "vertical-text", "alias", "copy", "no-drop", "grab", "grabbing", "all-scroll", "col-resize", "row-resize", "n-resize", "e-resize", "s-resize", "w-resize", "ne-resize", "nw-resize", "se-resize", "sw-resize", "ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "zoom-in", "zoom-out", isArbitraryValue, ], }, ], /** * Caret Color * @see https://tailwindcss.com/docs/just-in-time-mode#caret-color-utilities */ "caret-color": [ { caret: [colors], }, ], /** * Pointer Events * @see https://tailwindcss.com/docs/pointer-events */ "pointer-events": [ { "pointer-events": ["none", "auto"], }, ], /** * Resize * @see https://tailwindcss.com/docs/resize */ "resize": [ { resize: ["none", "y", "x", ""], }, ], /** * Scroll Behavior * @see https://tailwindcss.com/docs/scroll-behavior */ "scroll-behavior": [ { scroll: ["auto", "smooth"], }, ], /** * Scroll Margin * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-m": [ { "scroll-m": getSpacingWithArbitrary(), }, ], /** * Scroll Margin X * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-mx": [ { "scroll-mx": getSpacingWithArbitrary(), }, ], /** * Scroll Margin Y * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-my": [ { "scroll-my": getSpacingWithArbitrary(), }, ], /** * Scroll Margin Start * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-ms": [ { "scroll-ms": getSpacingWithArbitrary(), }, ], /** * Scroll Margin End * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-me": [ { "scroll-me": getSpacingWithArbitrary(), }, ], /** * Scroll Margin Top * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-mt": [ { "scroll-mt": getSpacingWithArbitrary(), }, ], /** * Scroll Margin Right * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-mr": [ { "scroll-mr": getSpacingWithArbitrary(), }, ], /** * Scroll Margin Bottom * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-mb": [ { "scroll-mb": getSpacingWithArbitrary(), }, ], /** * Scroll Margin Left * @see https://tailwindcss.com/docs/scroll-margin */ "scroll-ml": [ { "scroll-ml": getSpacingWithArbitrary(), }, ], /** * Scroll Padding * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-p": [ { "scroll-p": getSpacingWithArbitrary(), }, ], /** * Scroll Padding X * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-px": [ { "scroll-px": getSpacingWithArbitrary(), }, ], /** * Scroll Padding Y * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-py": [ { "scroll-py": getSpacingWithArbitrary(), }, ], /** * Scroll Padding Start * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-ps": [ { "scroll-ps": getSpacingWithArbitrary(), }, ], /** * Scroll Padding End * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-pe": [ { "scroll-pe": getSpacingWithArbitrary(), }, ], /** * Scroll Padding Top * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-pt": [ { "scroll-pt": getSpacingWithArbitrary(), }, ], /** * Scroll Padding Right * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-pr": [ { "scroll-pr": getSpacingWithArbitrary(), }, ], /** * Scroll Padding Bottom * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-pb": [ { "scroll-pb": getSpacingWithArbitrary(), }, ], /** * Scroll Padding Left * @see https://tailwindcss.com/docs/scroll-padding */ "scroll-pl": [ { "scroll-pl": getSpacingWithArbitrary(), }, ], /** * Scroll Snap Align * @see https://tailwindcss.com/docs/scroll-snap-align */ "snap-align": [ { snap: ["start", "end", "center", "align-none"], }, ], /** * Scroll Snap Stop * @see https://tailwindcss.com/docs/scroll-snap-stop */ "snap-stop": [ { snap: ["normal", "always"], }, ], /** * Scroll Snap Type * @see https://tailwindcss.com/docs/scroll-snap-type */ "snap-type": [ { snap: ["none", "x", "y", "both"], }, ], /** * Scroll Snap Type Strictness * @see https://tailwindcss.com/docs/scroll-snap-type */ "snap-strictness": [ { snap: ["mandatory", "proximity"], }, ], /** * Touch Action * @see https://tailwindcss.com/docs/touch-action */ "touch": [ { touch: ["auto", "none", "manipulation"], }, ], /** * Touch Action X * @see https://tailwindcss.com/docs/touch-action */ "touch-x": [ { "touch-pan": ["x", "left", "right"], }, ], /** * Touch Action Y * @see https://tailwindcss.com/docs/touch-action */ "touch-y": [ { "touch-pan": ["y", "up", "down"], }, ], /** * Touch Action Pinch Zoom * @see https://tailwindcss.com/docs/touch-action */ "touch-pz": ["touch-pinch-zoom"], /** * User Select * @see https://tailwindcss.com/docs/user-select */ "select": [ { select: ["none", "text", "all", "auto"], }, ], /** * Will Change * @see https://tailwindcss.com/docs/will-change */ "will-change": [ { "will-change": ["auto", "scroll", "contents", "transform", isArbitraryValue], }, ], // SVG /** * Fill * @see https://tailwindcss.com/docs/fill */ "fill": [ { fill: [colors, "none"], }, ], /** * Stroke Width * @see https://tailwindcss.com/docs/stroke-width */ "stroke-w": [ { stroke: [isLength, isArbitraryLength, isArbitraryNumber], }, ], /** * Stroke * @see https://tailwindcss.com/docs/stroke */ "stroke": [ { stroke: [colors, "none"], }, ], // Accessibility /** * Screen Readers * @see https://tailwindcss.com/docs/screen-readers */ "sr": ["sr-only", "not-sr-only"], /** * Forced Color Adjust * @see https://tailwindcss.com/docs/forced-color-adjust */ "forced-color-adjust": [ { "forced-color-adjust": ["auto", "none"], }, ], }, conflictingClassGroups: { "overflow": ["overflow-x", "overflow-y"], "overscroll": ["overscroll-x", "overscroll-y"], "inset": ["inset-x", "inset-y", "start", "end", "top", "right", "bottom", "left"], "inset-x": ["right", "left"], "inset-y": ["top", "bottom"], "flex": ["basis", "grow", "shrink"], "gap": ["gap-x", "gap-y"], "p": ["px", "py", "ps", "pe", "pt", "pr", "pb", "pl"], "px": ["pr", "pl"], "py": ["pt", "pb"], "m": ["mx", "my", "ms", "me", "mt", "mr", "mb", "ml"], "mx": ["mr", "ml"], "my": ["mt", "mb"], "size": ["w", "h"], "font-size": ["leading"], "fvn-normal": ["fvn-ordinal", "fvn-slashed-zero", "fvn-figure", "fvn-spacing", "fvn-fraction"], "fvn-ordinal": ["fvn-normal"], "fvn-slashed-zero": ["fvn-normal"], "fvn-figure": ["fvn-normal"], "fvn-spacing": ["fvn-normal"], "fvn-fraction": ["fvn-normal"], "line-clamp": ["display", "overflow"], "rounded": [ "rounded-s", "rounded-e", "rounded-t", "rounded-r", "rounded-b", "rounded-l", "rounded-ss", "rounded-se", "rounded-ee", "rounded-es", "rounded-tl", "rounded-tr", "rounded-br", "rounded-bl", ], "rounded-s": ["rounded-ss", "rounded-es"], "rounded-e": ["rounded-se", "rounded-ee"], "rounded-t": ["rounded-tl", "rounded-tr"], "rounded-r": ["rounded-tr", "rounded-br"], "rounded-b": ["rounded-br", "rounded-bl"], "rounded-l": ["rounded-tl", "rounded-bl"], "border-spacing": ["border-spacing-x", "border-spacing-y"], "border-w": ["border-w-s", "border-w-e", "border-w-t", "border-w-r", "border-w-b", "border-w-l"], "border-w-x": ["border-w-r", "border-w-l"], "border-w-y": ["border-w-t", "border-w-b"], "border-color": ["border-color-s", "border-color-e", "border-color-t", "border-color-r", "border-color-b", "border-color-l"], "border-color-x": ["border-color-r", "border-color-l"], "border-color-y": ["border-color-t", "border-color-b"], "scroll-m": ["scroll-mx", "scroll-my", "scroll-ms", "scroll-me", "scroll-mt", "scroll-mr", "scroll-mb", "scroll-ml"], "scroll-mx": ["scroll-mr", "scroll-ml"], "scroll-my": ["scroll-mt", "scroll-mb"], "scroll-p": ["scroll-px", "scroll-py", "scroll-ps", "scroll-pe", "scroll-pt", "scroll-pr", "scroll-pb", "scroll-pl"], "scroll-px": ["scroll-pr", "scroll-pl"], "scroll-py": ["scroll-pt", "scroll-pb"], "touch": ["touch-x", "touch-y", "touch-pz"], "touch-x": ["touch"], "touch-y": ["touch"], "touch-pz": ["touch"], }, conflictingClassGroupModifiers: { "font-size": ["leading"], }, }; }; const twMerge = /*#__PURE__*/ createTailwindMerge(getDefaultConfig); // src/primitive.tsx function composeEventHandlers(originalEventHandler, ourEventHandler, { checkForDefaultPrevented = true } = {}) { return function handleEvent(event) { originalEventHandler?.(event); if (checkForDefaultPrevented === false || !event.defaultPrevented) { return ourEventHandler?.(event); } }; } // src/create-context.tsx const React$w = await importShared("react"); function createContextScope(scopeName, createContextScopeDeps = []) { let defaultContexts = []; function createContext3(rootComponentName, defaultContext) { const BaseContext = React$w.createContext(defaultContext); BaseContext.displayName = rootComponentName + "Context"; const index = defaultContexts.length; defaultContexts = [...defaultContexts, defaultContext]; const Provider = (props) => { const { scope, children, ...context } = props; const Context = scope?.[scopeName]?.[index] || BaseContext; const value = React$w.useMemo(() => context, Object.values(context)); return /* @__PURE__ */ jsxRuntimeExports.jsx(Context.Provider, { value, children }); }; Provider.displayName = rootComponentName + "Provider"; function useContext2(consumerName, scope) { const Context = scope?.[scopeName]?.[index] || BaseContext; const context = React$w.useContext(Context); if (context) return context; if (defaultContext !== void 0) return defaultContext; throw new Error(`\`${consumerName}\` must be used within \`${rootComponentName}\``); } return [Provider, useContext2]; } const createScope = () => { const scopeContexts = defaultContexts.map((defaultContext) => { return React$w.createContext(defaultContext); }); return function useScope(scope) { const contexts = scope?.[scopeName] || scopeContexts; return React$w.useMemo(() => ({ [`__scope${scopeName}`]: { ...scope, [scopeName]: contexts } }), [scope, contexts]); }; }; createScope.scopeName = scopeName; return [createContext3, composeContextScopes(createScope, ...createContextScopeDeps)]; } function composeContextScopes(...scopes) { const baseScope = scopes[0]; if (scopes.length === 1) return baseScope; const createScope = () => { const scopeHooks = scopes.map((createScope2) => ({ useScope: createScope2(), scopeName: createScope2.scopeName, })); return function useComposedScopes(overrideScopes) { const nextScopes = scopeHooks.reduce((nextScopes2, { useScope, scopeName }) => { const scopeProps = useScope(overrideScopes); const currentScope = scopeProps[`__scope${scopeName}`]; return { ...nextScopes2, ...currentScope }; }, {}); return React$w.useMemo(() => ({ [`__scope${baseScope.scopeName}`]: nextScopes }), [nextScopes]); }; }; createScope.scopeName = baseScope.scopeName; return createScope; } // src/compose-refs.tsx const React$v = await importShared("react"); function setRef$1(ref, value) { if (typeof ref === "function") { return ref(value); } else if (ref !== null && ref !== void 0) { ref.current = value; } } function composeRefs(...refs) { return (node) => { let hasCleanup = false; const cleanups = refs.map((ref) => { const cleanup = setRef$1(ref, node); if (!hasCleanup && typeof cleanup == "function") { hasCleanup = true; } return cleanup; }); if (hasCleanup) { return () => { for (let i = 0; i < cleanups.length; i++) { const cleanup = cleanups[i]; if (typeof cleanup == "function") { cleanup(); } else { setRef$1(refs[i], null); } } }; } }; } function useComposedRefs(...refs) { return React$v.useCallback(composeRefs(...refs), refs); } // src/slot.tsx const React$u = await importShared("react"); // @__NO_SIDE_EFFECTS__ function createSlot(ownerName) { const Slot2 = React$u.forwardRef((props, forwardedRef) => { let { children, ...slotProps } = props; let slottableElement = null; let hasSlottable = false; const newChildren = []; if (isLazyComponent(children) && typeof use === "function") { children = use(children._payload); } React$u.Children.forEach(children, (maybeSlottable) => { if (isSlottable(maybeSlottable)) { hasSlottable = true; const slottable = maybeSlottable; let child = "child" in slottable.props ? slottable.props.child : slottable.props.children; if (isLazyComponent(child) && typeof use === "function") { child = use(child._payload); } slottableElement = getSlottableElementFromSlottable(slottable, child); newChildren.push(slottableElement?.props?.children); } else { newChildren.push(maybeSlottable); } }); if (slottableElement) { slottableElement = React$u.cloneElement(slottableElement, void 0, newChildren); } else if ( // A `Slottable` was found but it didn't resolve to a single element (e.g. // it wrapped multiple elements, text, or a render-prop `child` that // wasn't an element). Don't fall back to treating the `Slottable` wrapper // itself as the slot target — throw a descriptive error below instead. !hasSlottable && React$u.Children.count(children) === 1 && React$u.isValidElement(children) ) { slottableElement = children; } const slottableElementRef = slottableElement ? getElementRef$1(slottableElement) : void 0; const composedRef = useComposedRefs(forwardedRef, slottableElementRef); if (!slottableElement) { if (children || children === 0) { throw new Error(hasSlottable ? createSlottableError(ownerName) : createSlotError(ownerName)); } return children; } const mergedProps = mergeProps(slotProps, slottableElement.props ?? {}); if (slottableElement.type !== React$u.Fragment) { mergedProps.ref = forwardedRef ? composedRef : slottableElementRef; } return React$u.cloneElement(slottableElement, mergedProps); }); Slot2.displayName = `${ownerName}.Slot`; return Slot2; } var Slot$1 = /* @__PURE__ */ createSlot("Slot"); var SLOTTABLE_IDENTIFIER = Symbol.for("radix.slottable"); // @__NO_SIDE_EFFECTS__ function createSlottable(ownerName) { const Slottable2 = (props) => ("child" in props ? props.children(props.child) : props.children); Slottable2.displayName = `${ownerName}.Slottable`; Slottable2.__radixId = SLOTTABLE_IDENTIFIER; return Slottable2; } var getSlottableElementFromSlottable = (slottable, child) => { if ("child" in slottable.props) { const child2 = slottable.props.child; if (!React$u.isValidElement(child2)) return null; return React$u.cloneElement(child2, void 0, slottable.props.children(child2.props.children)); } return React$u.isValidElement(child) ? child : null; }; function mergeProps(slotProps, childProps) { const overrideProps = { ...childProps }; for (const propName in childProps) { const slotPropValue = slotProps[propName]; const childPropValue = childProps[propName]; const isHandler = /^on[A-Z]/.test(propName); if (isHandler) { if (slotPropValue && childPropValue) { overrideProps[propName] = (...args) => { const result = childPropValue(...args); slotPropValue(...args); return result; }; } else if (slotPropValue) { overrideProps[propName] = slotPropValue; } } else if (propName === "style") { overrideProps[propName] = { ...slotPropValue, ...childPropValue }; } else if (propName === "className") { overrideProps[propName] = [slotPropValue, childPropValue].filter(Boolean).join(" "); } } return { ...slotProps, ...overrideProps }; } function getElementRef$1(element) { let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get; let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning; if (mayWarn) { return element.ref; } getter = Object.getOwnPropertyDescriptor(element, "ref")?.get; mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning; if (mayWarn) { return element.props.ref; } return element.props.ref || element.ref; } function isSlottable(child) { return React$u.isValidElement(child) && typeof child.type === "function" && "__radixId" in child.type && child.type.__radixId === SLOTTABLE_IDENTIFIER; } var REACT_LAZY_TYPE = Symbol.for("react.lazy"); function isLazyComponent(element) { return element != null && typeof element === "object" && "$$typeof" in element && element.$$typeof === REACT_LAZY_TYPE && "_payload" in element && isPromiseLike(element._payload); } function isPromiseLike(value) { return typeof value === "object" && value !== null && "then" in value; } var createSlotError = (ownerName) => { return `${ownerName} failed to slot onto its children. Expected a single React element child or \`Slottable\`.`; }; var createSlottableError = (ownerName) => { return `${ownerName} failed to slot onto its \`Slottable\`. Expected \`Slottable\` to receive a single React element child.`; }; var use = React$u[" use ".trim().toString()]; // src/collection-legacy.tsx const React$t = await importShared("react"); function createCollection(name) { const PROVIDER_NAME = name + "CollectionProvider"; const [createCollectionContext, createCollectionScope] = createContextScope(PROVIDER_NAME); const [CollectionProviderImpl, useCollectionContext] = createCollectionContext(PROVIDER_NAME, { collectionRef: { current: null }, itemMap: /* @__PURE__ */ new Map() }); const CollectionProvider = (props) => { const { scope, children } = props; const ref = React$t.useRef(null); const itemMap = React$t.useRef(/* @__PURE__ */ new Map()).current; return /* @__PURE__ */ jsxRuntimeExports.jsx(CollectionProviderImpl, { scope, itemMap, collectionRef: ref, children }); }; CollectionProvider.displayName = PROVIDER_NAME; const COLLECTION_SLOT_NAME = name + "CollectionSlot"; const CollectionSlotImpl = createSlot(COLLECTION_SLOT_NAME); const CollectionSlot = React$t.forwardRef((props, forwardedRef) => { const { scope, children } = props; const context = useCollectionContext(COLLECTION_SLOT_NAME, scope); const composedRefs = useComposedRefs(forwardedRef, context.collectionRef); return /* @__PURE__ */ jsxRuntimeExports.jsx(CollectionSlotImpl, { ref: composedRefs, children }); }); CollectionSlot.displayName = COLLECTION_SLOT_NAME; const ITEM_SLOT_NAME = name + "CollectionItemSlot"; const ITEM_DATA_ATTR = "data-radix-collection-item"; const CollectionItemSlotImpl = createSlot(ITEM_SLOT_NAME); const CollectionItemSlot = React$t.forwardRef((props, forwardedRef) => { const { scope, children, ...itemData } = props; const ref = React$t.useRef(null); const composedRefs = useComposedRefs(forwardedRef, ref); const context = useCollectionContext(ITEM_SLOT_NAME, scope); React$t.useEffect(() => { context.itemMap.set(ref, { ref, ...itemData }); return () => void context.itemMap.delete(ref); }); return /* @__PURE__ */ jsxRuntimeExports.jsx(CollectionItemSlotImpl, { ...{ [ITEM_DATA_ATTR]: "" }, ref: composedRefs, children }); }); CollectionItemSlot.displayName = ITEM_SLOT_NAME; function useCollection(scope) { const context = useCollectionContext(name + "CollectionConsumer", scope); const getItems = React$t.useCallback(() => { const collectionNode = context.collectionRef.current; if (!collectionNode) return []; const orderedNodes = Array.from(collectionNode.querySelectorAll(`[${ITEM_DATA_ATTR}]`)); const items = Array.from(context.itemMap.values()); const orderedItems = items.sort((a, b) => orderedNodes.indexOf(a.ref.current) - orderedNodes.indexOf(b.ref.current)); return orderedItems; }, [context.collectionRef, context.itemMap]); return getItems; } return [{ Provider: CollectionProvider, Slot: CollectionSlot, ItemSlot: CollectionItemSlot }, useCollection, createCollectionScope]; } // src/collection.tsx await importShared("react"); // src/use-layout-effect.tsx const React$s = await importShared("react"); var useLayoutEffect2 = globalThis?.document ? React$s.useLayoutEffect : () => {}; // src/id.tsx const React$r = await importShared("react"); var useReactId = React$r[" useId ".trim().toString()] || (() => void 0); var count$1 = 0; function useId(deterministicId) { const [id, setId] = React$r.useState(useReactId()); useLayoutEffect2(() => { setId((reactId) => reactId ?? String(count$1++)); }, [deterministicId]); return id ? `radix-${id}` : ""; } // src/primitive.tsx const React$q = await importShared("react"); const ReactDOM$3 = await importShared("react-dom"); var NODES = ["a", "button", "div", "form", "h2", "h3", "img", "input", "label", "li", "nav", "ol", "p", "select", "span", "svg", "ul"]; var Primitive = NODES.reduce((primitive, node) => { const Slot = createSlot(`Primitive.${node}`); const Node = React$q.forwardRef((props, forwardedRef) => { const { asChild, ...primitiveProps } = props; const Comp = asChild ? Slot : node; if (typeof window !== "undefined") { window[Symbol.for("radix-ui")] = true; } return /* @__PURE__ */ jsxRuntimeExports.jsx(Comp, { ...primitiveProps, ref: forwardedRef }); }); Node.displayName = `Primitive.${node}`; return { ...primitive, [node]: Node }; }, {}); function dispatchDiscreteCustomEvent(target, event) { if (target) ReactDOM$3.flushSync(() => target.dispatchEvent(event)); } // src/use-callback-ref.tsx const React$p = await importShared("react"); function useCallbackRef$1(callback) { const callbackRef = React$p.useRef(callback); React$p.useEffect(() => { callbackRef.current = callback; }); return React$p.useMemo( () => (...args) => callbackRef.current?.(...args), [], ); } const React$o = await importShared("react"); var useReactEffectEvent = React$o[" useEffectEvent ".trim().toString()]; var useReactInsertionEffect = React$o[" useInsertionEffect ".trim().toString()]; function useEffectEvent(callback) { if (typeof useReactEffectEvent === "function") { return useReactEffectEvent(callback); } const ref = React$o.useRef(() => { throw new Error("Cannot call an event handler while rendering."); }); if (typeof useReactInsertionEffect === "function") { useReactInsertionEffect(() => { ref.current = callback; }); } else { useLayoutEffect2(() => { ref.current = callback; }); } return React$o.useMemo( () => (...args) => ref.current?.(...args), [], ); } // src/use-controllable-state.tsx const React$n = await importShared("react"); var useInsertionEffect = React$n[" useInsertionEffect ".trim().toString()] || useLayoutEffect2; function useControllableState({ prop, defaultProp, onChange = () => {}, caller }) { const [uncontrolledProp, setUncontrolledProp, onChangeRef] = useUncontrolledState({ defaultProp, onChange, }); const isControlled = prop !== void 0; const value = isControlled ? prop : uncontrolledProp; { const isControlledRef = React$n.useRef(prop !== void 0); React$n.useEffect(() => { const wasControlled = isControlledRef.current; if (wasControlled !== isControlled) { const from = wasControlled ? "controlled" : "uncontrolled"; const to = isControlled ? "controlled" : "uncontrolled"; console.warn( `${caller} is changing from ${from} to ${to}. Components should not switch from controlled to uncontrolled (or vice versa). Decide between using a controlled or uncontrolled value for the lifetime of the component.`, ); } isControlledRef.current = isControlled; }, [isControlled, caller]); } const setValue = React$n.useCallback( (nextValue) => { if (isControlled) { const value2 = isFunction$1(nextValue) ? nextValue(prop) : nextValue; if (value2 !== prop) { onChangeRef.current?.(value2); } } else { setUncontrolledProp(nextValue); } }, [isControlled, prop, setUncontrolledProp, onChangeRef], ); return [value, setValue]; } function useUncontrolledState({ defaultProp, onChange }) { const [value, setValue] = React$n.useState(defaultProp); const prevValueRef = React$n.useRef(value); const onChangeRef = React$n.useRef(onChange); useInsertionEffect(() => { onChangeRef.current = onChange; }, [onChange]); React$n.useEffect(() => { if (prevValueRef.current !== value) { onChangeRef.current?.(value); prevValueRef.current = value; } }, [value, prevValueRef]); return [value, setValue, onChangeRef]; } function isFunction$1(value) { return typeof value === "function"; } // src/use-controllable-state-reducer.tsx await importShared("react"); // src/direction.tsx const React$m = await importShared("react"); var DirectionContext = React$m.createContext(void 0); function useDirection(localDir) { const globalDir = React$m.useContext(DirectionContext); return localDir || globalDir || "ltr"; } // src/roving-focus-group.tsx const React$l = await importShared("react"); var ENTRY_FOCUS = "rovingFocusGroup.onEntryFocus"; var EVENT_OPTIONS$1 = { bubbles: false, cancelable: true }; var GROUP_NAME$1 = "RovingFocusGroup"; var [Collection$1, useCollection$1, createCollectionScope$1] = createCollection(GROUP_NAME$1); var [createRovingFocusGroupContext, createRovingFocusGroupScope] = createContextScope(GROUP_NAME$1, [createCollectionScope$1]); var [RovingFocusProvider, useRovingFocusContext] = createRovingFocusGroupContext(GROUP_NAME$1); var RovingFocusGroup = React$l.forwardRef((props, forwardedRef) => { return /* @__PURE__ */ jsxRuntimeExports.jsx(Collection$1.Provider, { scope: props.__scopeRovingFocusGroup, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Collection$1.Slot, { scope: props.__scopeRovingFocusGroup, children: /* @__PURE__ */ jsxRuntimeExports.jsx(RovingFocusGroupImpl, { ...props, ref: forwardedRef }), }), }); }); RovingFocusGroup.displayName = GROUP_NAME$1; var RovingFocusGroupImpl = React$l.forwardRef((props, forwardedRef) => { const { __scopeRovingFocusGroup, orientation, loop = false, dir, currentTabStopId: currentTabStopIdProp, defaultCurrentTabStopId, onCurrentTabStopIdChange, onEntryFocus, preventScrollOnEntryFocus = false, ...groupProps } = props; const ref = React$l.useRef(null); const composedRefs = useComposedRefs(forwardedRef, ref); const direction = useDirection(dir); const [currentTabStopId, setCurrentTabStopId] = useControllableState({ prop: currentTabStopIdProp, defaultProp: defaultCurrentTabStopId ?? null, onChange: onCurrentTabStopIdChange, caller: GROUP_NAME$1, }); const [isTabbingBackOut, setIsTabbingBackOut] = React$l.useState(false); const handleEntryFocus = useCallbackRef$1(onEntryFocus); const getItems = useCollection$1(__scopeRovingFocusGroup); const isClickFocusRef = React$l.useRef(false); const [focusableItemsCount, setFocusableItemsCount] = React$l.useState(0); React$l.useEffect(() => { const node = ref.current; if (node) { node.addEventListener(ENTRY_FOCUS, handleEntryFocus); return () => node.removeEventListener(ENTRY_FOCUS, handleEntryFocus); } }, [handleEntryFocus]); return /* @__PURE__ */ jsxRuntimeExports.jsx(RovingFocusProvider, { scope: __scopeRovingFocusGroup, orientation, dir: direction, loop, currentTabStopId, onItemFocus: React$l.useCallback((tabStopId) => setCurrentTabStopId(tabStopId), [setCurrentTabStopId]), onItemShiftTab: React$l.useCallback(() => setIsTabbingBackOut(true), []), onFocusableItemAdd: React$l.useCallback(() => setFocusableItemsCount((prevCount) => prevCount + 1), []), onFocusableItemRemove: React$l.useCallback(() => setFocusableItemsCount((prevCount) => prevCount - 1), []), children: /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { "tabIndex": isTabbingBackOut || focusableItemsCount === 0 ? -1 : 0, "data-orientation": orientation, ...groupProps, "ref": composedRefs, "style": { outline: "none", ...props.style }, "onMouseDown": composeEventHandlers(props.onMouseDown, () => { isClickFocusRef.current = true; }), "onFocus": composeEventHandlers(props.onFocus, (event) => { const isKeyboardFocus = !isClickFocusRef.current; if (event.target === event.currentTarget && isKeyboardFocus && !isTabbingBackOut) { const entryFocusEvent = new CustomEvent(ENTRY_FOCUS, EVENT_OPTIONS$1); event.currentTarget.dispatchEvent(entryFocusEvent); if (!entryFocusEvent.defaultPrevented) { const items = getItems().filter((item) => item.focusable); const activeItem = items.find((item) => item.active); const currentItem = items.find((item) => item.id === currentTabStopId); const candidateItems = [activeItem, currentItem, ...items].filter(Boolean); const candidateNodes = candidateItems.map((item) => item.ref.current); focusFirst$1(candidateNodes, preventScrollOnEntryFocus); } } isClickFocusRef.current = false; }), "onBlur": composeEventHandlers(props.onBlur, () => setIsTabbingBackOut(false)), }), }); }); var ITEM_NAME$1 = "RovingFocusGroupItem"; var RovingFocusGroupItem = React$l.forwardRef((props, forwardedRef) => { const { __scopeRovingFocusGroup, focusable = true, active = false, tabStopId, children, ...itemProps } = props; const autoId = useId(); const id = tabStopId || autoId; const context = useRovingFocusContext(ITEM_NAME$1, __scopeRovingFocusGroup); const isCurrentTabStop = context.currentTabStopId === id; const getItems = useCollection$1(__scopeRovingFocusGroup); const { onFocusableItemAdd, onFocusableItemRemove, currentTabStopId } = context; React$l.useEffect(() => { if (focusable) { onFocusableItemAdd(); return () => onFocusableItemRemove(); } }, [focusable, onFocusableItemAdd, onFocusableItemRemove]); return /* @__PURE__ */ jsxRuntimeExports.jsx(Collection$1.ItemSlot, { scope: __scopeRovingFocusGroup, id, focusable, active, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.span, { "tabIndex": isCurrentTabStop ? 0 : -1, "data-orientation": context.orientation, ...itemProps, "ref": forwardedRef, "onMouseDown": composeEventHandlers(props.onMouseDown, (event) => { if (!focusable) event.preventDefault(); else context.onItemFocus(id); }), "onFocus": composeEventHandlers(props.onFocus, () => context.onItemFocus(id)), "onKeyDown": composeEventHandlers(props.onKeyDown, (event) => { if (event.key === "Tab" && event.shiftKey) { context.onItemShiftTab(); return; } if (event.target !== event.currentTarget) return; const focusIntent = getFocusIntent(event, context.orientation, context.dir); if (focusIntent !== void 0) { if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return; event.preventDefault(); const items = getItems().filter((item) => item.focusable); let candidateNodes = items.map((item) => item.ref.current); if (focusIntent === "last") candidateNodes.reverse(); else if (focusIntent === "prev" || focusIntent === "next") { if (focusIntent === "prev") candidateNodes.reverse(); const currentIndex = candidateNodes.indexOf(event.currentTarget); candidateNodes = context.loop ? wrapArray$1(candidateNodes, currentIndex + 1) : candidateNodes.slice(currentIndex + 1); } setTimeout(() => focusFirst$1(candidateNodes)); } }), "children": typeof children === "function" ? children({ isCurrentTabStop, hasTabStop: currentTabStopId != null }) : children, }), }); }); RovingFocusGroupItem.displayName = ITEM_NAME$1; var MAP_KEY_TO_FOCUS_INTENT = { ArrowLeft: "prev", ArrowUp: "prev", ArrowRight: "next", ArrowDown: "next", PageUp: "first", Home: "first", PageDown: "last", End: "last", }; function getDirectionAwareKey(key, dir) { if (dir !== "rtl") return key; return ( key === "ArrowLeft" ? "ArrowRight" : key === "ArrowRight" ? "ArrowLeft" : key ); } function getFocusIntent(event, orientation, dir) { const key = getDirectionAwareKey(event.key, dir); if (orientation === "vertical" && ["ArrowLeft", "ArrowRight"].includes(key)) return void 0; if (orientation === "horizontal" && ["ArrowUp", "ArrowDown"].includes(key)) return void 0; return MAP_KEY_TO_FOCUS_INTENT[key]; } function focusFirst$1(candidates, preventScroll = false) { const PREVIOUSLY_FOCUSED_ELEMENT = document.activeElement; for (const candidate of candidates) { if (candidate === PREVIOUSLY_FOCUSED_ELEMENT) return; candidate.focus({ preventScroll }); if (document.activeElement !== PREVIOUSLY_FOCUSED_ELEMENT) return; } } function wrapArray$1(array, startIndex) { return array.map((_, index) => array[(startIndex + index) % array.length]); } var Root$3 = RovingFocusGroup; var Item = RovingFocusGroupItem; // src/presence.tsx const React2 = await importShared("react"); // src/use-state-machine.tsx const React$k = await importShared("react"); function useStateMachine(initialState, machine) { return React$k.useReducer((state, event) => { const nextState = machine[state][event]; return nextState ?? state; }, initialState); } // src/presence.tsx var Presence = (props) => { const { present, children } = props; const presence = usePresence(present); const child = typeof children === "function" ? children({ present: presence.isPresent }) : React2.Children.only(children); const ref = useStableComposedRefs(presence.ref, getElementRef(child)); const forceMount = typeof children === "function"; return forceMount || presence.isPresent ? React2.cloneElement(child, { ref }) : null; }; Presence.displayName = "Presence"; function usePresence(present) { const [node, setNode] = React2.useState(); const stylesRef = React2.useRef(null); const prevPresentRef = React2.useRef(present); const prevAnimationNameRef = React2.useRef("none"); const initialState = present ? "mounted" : "unmounted"; const [state, send] = useStateMachine(initialState, { mounted: { UNMOUNT: "unmounted", ANIMATION_OUT: "unmountSuspended", }, unmountSuspended: { MOUNT: "mounted", ANIMATION_END: "unmounted", }, unmounted: { MOUNT: "mounted", }, }); React2.useEffect(() => { const currentAnimationName = getAnimationName(stylesRef.current); prevAnimationNameRef.current = state === "mounted" ? currentAnimationName : "none"; }, [state]); useLayoutEffect2(() => { const styles = stylesRef.current; const wasPresent = prevPresentRef.current; const hasPresentChanged = wasPresent !== present; if (hasPresentChanged) { const prevAnimationName = prevAnimationNameRef.current; const currentAnimationName = getAnimationName(styles); if (present) { send("MOUNT"); } else if (currentAnimationName === "none" || styles?.display === "none") { send("UNMOUNT"); } else { const isAnimating = prevAnimationName !== currentAnimationName; if (wasPresent && isAnimating) { send("ANIMATION_OUT"); } else { send("UNMOUNT"); } } prevPresentRef.current = present; } }, [present, send]); useLayoutEffect2(() => { if (node) { let timeoutId; const ownerWindow = node.ownerDocument.defaultView ?? window; const handleAnimationEnd = (event) => { const currentAnimationName = getAnimationName(stylesRef.current); const isCurrentAnimation = currentAnimationName.includes(CSS.escape(event.animationName)); if (event.target === node && isCurrentAnimation) { send("ANIMATION_END"); if (!prevPresentRef.current) { const currentFillMode = node.style.animationFillMode; node.style.animationFillMode = "forwards"; timeoutId = ownerWindow.setTimeout(() => { if (node.style.animationFillMode === "forwards") { node.style.animationFillMode = currentFillMode; } }); } } }; const handleAnimationStart = (event) => { if (event.target === node) { prevAnimationNameRef.current = getAnimationName(stylesRef.current); } }; node.addEventListener("animationstart", handleAnimationStart); node.addEventListener("animationcancel", handleAnimationEnd); node.addEventListener("animationend", handleAnimationEnd); return () => { ownerWindow.clearTimeout(timeoutId); node.removeEventListener("animationstart", handleAnimationStart); node.removeEventListener("animationcancel", handleAnimationEnd); node.removeEventListener("animationend", handleAnimationEnd); }; } else { send("ANIMATION_END"); } }, [node, send]); return { isPresent: ["mounted", "unmountSuspended"].includes(state), ref: React2.useCallback((node2) => { stylesRef.current = node2 ? getComputedStyle(node2) : null; setNode(node2); }, []), }; } function setRef(ref, value) { if (typeof ref === "function") { return ref(value); } else if (ref !== null && ref !== void 0) { ref.current = value; } } function useStableComposedRefs(...refs) { const refsRef = React2.useRef(refs); refsRef.current = refs; return React2.useCallback((node) => { const currentRefs = refsRef.current; let hasCleanup = false; const cleanups = currentRefs.map((ref) => { const cleanup = setRef(ref, node); if (!hasCleanup && typeof cleanup === "function") { hasCleanup = true; } return cleanup; }); if (hasCleanup) { return () => { for (let i = 0; i < cleanups.length; i++) { const cleanup = cleanups[i]; if (typeof cleanup === "function") { cleanup(); } else { setRef(currentRefs[i], null); } } }; } }, []); } function getAnimationName(styles) { return styles?.animationName || "none"; } function getElementRef(element) { let getter = Object.getOwnPropertyDescriptor(element.props, "ref")?.get; let mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning; if (mayWarn) { return element.ref; } getter = Object.getOwnPropertyDescriptor(element, "ref")?.get; mayWarn = getter && "isReactWarning" in getter && getter.isReactWarning; if (mayWarn) { return element.props.ref; } return element.props.ref || element.ref; } // src/tabs.tsx const React$j = await importShared("react"); var TABS_NAME = "Tabs"; var [createTabsContext] = createContextScope(TABS_NAME, [createRovingFocusGroupScope]); var useRovingFocusGroupScope = createRovingFocusGroupScope(); var [TabsProvider, useTabsContext] = createTabsContext(TABS_NAME); var Tabs = React$j.forwardRef((props, forwardedRef) => { const { __scopeTabs, value: valueProp, onValueChange, defaultValue, orientation = "horizontal", dir, activationMode = "automatic", ...tabsProps } = props; const direction = useDirection(dir); const [value, setValue] = useControllableState({ prop: valueProp, onChange: onValueChange, defaultProp: defaultValue ?? "", caller: TABS_NAME, }); return /* @__PURE__ */ jsxRuntimeExports.jsx(TabsProvider, { scope: __scopeTabs, baseId: useId(), value, onValueChange: setValue, orientation, dir: direction, activationMode, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { "dir": direction, "data-orientation": orientation, ...tabsProps, "ref": forwardedRef, }), }); }); Tabs.displayName = TABS_NAME; var TAB_LIST_NAME = "TabsList"; var TabsList = React$j.forwardRef((props, forwardedRef) => { const { __scopeTabs, loop = true, ...listProps } = props; const context = useTabsContext(TAB_LIST_NAME, __scopeTabs); const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeTabs); return /* @__PURE__ */ jsxRuntimeExports.jsx(Root$3, { asChild: true, ...rovingFocusGroupScope, orientation: context.orientation, dir: context.dir, loop, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { "role": "tablist", "aria-orientation": context.orientation, ...listProps, "ref": forwardedRef, }), }); }); TabsList.displayName = TAB_LIST_NAME; var TRIGGER_NAME$1 = "TabsTrigger"; var TabsTrigger = React$j.forwardRef((props, forwardedRef) => { const { __scopeTabs, value, disabled = false, ...triggerProps } = props; const context = useTabsContext(TRIGGER_NAME$1, __scopeTabs); const rovingFocusGroupScope = useRovingFocusGroupScope(__scopeTabs); const triggerId = makeTriggerId(context.baseId, value); const contentId = makeContentId(context.baseId, value); const isSelected = value === context.value; return /* @__PURE__ */ jsxRuntimeExports.jsx(Item, { asChild: true, ...rovingFocusGroupScope, focusable: !disabled, active: isSelected, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.button, { "type": "button", "role": "tab", "aria-selected": isSelected, "aria-controls": contentId, "data-state": isSelected ? "active" : "inactive", "data-disabled": disabled ? "" : void 0, disabled, "id": triggerId, ...triggerProps, "ref": forwardedRef, "onMouseDown": composeEventHandlers(props.onMouseDown, (event) => { if (!disabled && event.button === 0 && event.ctrlKey === false) { context.onValueChange(value); } else { event.preventDefault(); } }), "onKeyDown": composeEventHandlers(props.onKeyDown, (event) => { if ([" ", "Enter"].includes(event.key)) context.onValueChange(value); }), "onFocus": composeEventHandlers(props.onFocus, () => { const isAutomaticActivation = context.activationMode !== "manual"; if (!isSelected && !disabled && isAutomaticActivation) { context.onValueChange(value); } }), }), }); }); TabsTrigger.displayName = TRIGGER_NAME$1; var CONTENT_NAME$2 = "TabsContent"; var TabsContent = React$j.forwardRef((props, forwardedRef) => { const { __scopeTabs, value, forceMount, children, ...contentProps } = props; const context = useTabsContext(CONTENT_NAME$2, __scopeTabs); const triggerId = makeTriggerId(context.baseId, value); const contentId = makeContentId(context.baseId, value); const isSelected = value === context.value; const isMountAnimationPreventedRef = React$j.useRef(isSelected); React$j.useEffect(() => { const rAF = requestAnimationFrame(() => (isMountAnimationPreventedRef.current = false)); return () => cancelAnimationFrame(rAF); }, []); return /* @__PURE__ */ jsxRuntimeExports.jsx(Presence, { present: forceMount || isSelected, children: ({ present }) => /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { "data-state": isSelected ? "active" : "inactive", "data-orientation": context.orientation, "role": "tabpanel", "aria-labelledby": triggerId, "hidden": !present, "id": contentId, "tabIndex": 0, ...contentProps, "ref": forwardedRef, "style": { ...props.style, animationDuration: isMountAnimationPreventedRef.current ? "0s" : void 0, }, "children": present && children, }), }); }); TabsContent.displayName = CONTENT_NAME$2; function makeTriggerId(baseId, value) { return `${baseId}-trigger-${value}`; } function makeContentId(baseId, value) { return `${baseId}-content-${value}`; } var Root2$1 = Tabs; var List = TabsList; var Trigger = TabsTrigger; var Content$1 = TabsContent; // src/number.ts function clamp$1(value, [min, max]) { return Math.min(max, Math.max(min, value)); } // src/dismissable-layer.tsx const React$i = await importShared("react"); var DISMISSABLE_LAYER_NAME = "DismissableLayer"; var CONTEXT_UPDATE = "dismissableLayer.update"; var POINTER_DOWN_OUTSIDE = "dismissableLayer.pointerDownOutside"; var FOCUS_OUTSIDE = "dismissableLayer.focusOutside"; var originalBodyPointerEvents; var DismissableLayerContext = React$i.createContext({ layers: /* @__PURE__ */ new Set(), layersWithOutsidePointerEventsDisabled: /* @__PURE__ */ new Set(), branches: /* @__PURE__ */ new Set(), // Outside elements that belong to a layer's own dismiss affordance (eg, a // dialog overlay). Pressing them should dismiss the layer regardless of // whether or not they stop propagation. // // See https://github.com/radix-ui/primitives/issues/3346 dismissableSurfaces: /* @__PURE__ */ new Set(), }); var DismissableLayer = React$i.forwardRef((props, forwardedRef) => { const { disableOutsidePointerEvents = false, deferPointerDownOutside = false, onEscapeKeyDown, onPointerDownOutside, onFocusOutside, onInteractOutside, onDismiss, ...layerProps } = props; const context = React$i.useContext(DismissableLayerContext); const [node, setNode] = React$i.useState(null); const ownerDocument = node?.ownerDocument ?? globalThis?.document; const [, force] = React$i.useState({}); const composedRefs = useComposedRefs(forwardedRef, setNode); const layers = Array.from(context.layers); const [highestLayerWithOutsidePointerEventsDisabled] = [...context.layersWithOutsidePointerEventsDisabled].slice(-1); const highestLayerWithOutsidePointerEventsDisabledIndex = layers.indexOf(highestLayerWithOutsidePointerEventsDisabled); const index = node ? layers.indexOf(node) : -1; const isBodyPointerEventsDisabled = context.layersWithOutsidePointerEventsDisabled.size > 0; const isPointerEventsEnabled = index >= highestLayerWithOutsidePointerEventsDisabledIndex; const isDeferredPointerDownOutsideRef = React$i.useRef(false); const pointerDownOutside = usePointerDownOutside( (event) => { const target = event.target; if (!(target instanceof Node)) { return; } const isPointerDownOnBranch = [...context.branches].some((branch) => branch.contains(target)); if (!isPointerEventsEnabled || isPointerDownOnBranch) return; onPointerDownOutside?.(event); onInteractOutside?.(event); if (!event.defaultPrevented) onDismiss?.(); }, { ownerDocument, deferPointerDownOutside, isDeferredPointerDownOutsideRef, dismissableSurfaces: context.dismissableSurfaces, }, ); const focusOutside = useFocusOutside((event) => { if (deferPointerDownOutside && isDeferredPointerDownOutsideRef.current) { return; } const target = event.target; const isFocusInBranch = [...context.branches].some((branch) => branch.contains(target)); if (isFocusInBranch) return; onFocusOutside?.(event); onInteractOutside?.(event); if (!event.defaultPrevented) onDismiss?.(); }, ownerDocument); const isHighestLayer = node ? index === layers.length - 1 : false; const handleKeyDown = useEffectEvent((event) => { if (event.key !== "Escape") { return; } onEscapeKeyDown?.(event); if (!event.defaultPrevented && onDismiss) { event.preventDefault(); onDismiss(); } }); React$i.useEffect(() => { if (!isHighestLayer) { return; } ownerDocument.addEventListener("keydown", handleKeyDown, { capture: true }); return () => ownerDocument.removeEventListener("keydown", handleKeyDown, { capture: true }); }, [ownerDocument, isHighestLayer]); React$i.useEffect(() => { if (!node) return; if (disableOutsidePointerEvents) { if (context.layersWithOutsidePointerEventsDisabled.size === 0) { originalBodyPointerEvents = ownerDocument.body.style.pointerEvents; ownerDocument.body.style.pointerEvents = "none"; } context.layersWithOutsidePointerEventsDisabled.add(node); } context.layers.add(node); dispatchUpdate(); return () => { if (disableOutsidePointerEvents) { context.layersWithOutsidePointerEventsDisabled.delete(node); if (context.layersWithOutsidePointerEventsDisabled.size === 0) { ownerDocument.body.style.pointerEvents = originalBodyPointerEvents; } } }; }, [node, ownerDocument, disableOutsidePointerEvents, context]); React$i.useEffect(() => { return () => { if (!node) return; context.layers.delete(node); context.layersWithOutsidePointerEventsDisabled.delete(node); dispatchUpdate(); }; }, [node, context]); React$i.useEffect(() => { const handleUpdate = () => force({}); document.addEventListener(CONTEXT_UPDATE, handleUpdate); return () => document.removeEventListener(CONTEXT_UPDATE, handleUpdate); }, []); return /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { ...layerProps, ref: composedRefs, style: { pointerEvents: isBodyPointerEventsDisabled ? isPointerEventsEnabled ? "auto" : "none" : void 0, ...props.style, }, onFocusCapture: composeEventHandlers(props.onFocusCapture, focusOutside.onFocusCapture), onBlurCapture: composeEventHandlers(props.onBlurCapture, focusOutside.onBlurCapture), onPointerDownCapture: composeEventHandlers(props.onPointerDownCapture, pointerDownOutside.onPointerDownCapture), }); }); DismissableLayer.displayName = DISMISSABLE_LAYER_NAME; var BRANCH_NAME = "DismissableLayerBranch"; var DismissableLayerBranch = React$i.forwardRef((props, forwardedRef) => { const context = React$i.useContext(DismissableLayerContext); const ref = React$i.useRef(null); const composedRefs = useComposedRefs(forwardedRef, ref); React$i.useEffect(() => { const node = ref.current; if (node) { context.branches.add(node); return () => { context.branches.delete(node); }; } }, [context.branches]); return /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { ...props, ref: composedRefs }); }); DismissableLayerBranch.displayName = BRANCH_NAME; function useDismissableLayerSurface() { const context = React$i.useContext(DismissableLayerContext); const [node, setNode] = React$i.useState(null); React$i.useEffect(() => { if (!node) { return; } context.dismissableSurfaces.add(node); return () => { context.dismissableSurfaces.delete(node); }; }, [node, context.dismissableSurfaces]); return setNode; } function usePointerDownOutside(onPointerDownOutside, args) { const { ownerDocument = globalThis?.document, deferPointerDownOutside = false, isDeferredPointerDownOutsideRef, dismissableSurfaces } = args; const handlePointerDownOutside = useCallbackRef$1(onPointerDownOutside); const isPointerInsideReactTreeRef = React$i.useRef(false); const isPointerDownOutsideRef = React$i.useRef(false); const interceptedOutsideInteractionEventsRef = React$i.useRef(/* @__PURE__ */ new Map()); const handleClickRef = React$i.useRef(() => {}); React$i.useEffect(() => { function resetOutsideInteraction() { isPointerDownOutsideRef.current = false; isDeferredPointerDownOutsideRef.current = false; interceptedOutsideInteractionEventsRef.current.clear(); } function isOutsideInteractionIntercepted() { return Array.from(interceptedOutsideInteractionEventsRef.current.values()).some(Boolean); } function handleInteractionCapture(event) { if (!isPointerDownOutsideRef.current) { return; } const target = event.target; const isDismissableSurface = target instanceof Node && [...dismissableSurfaces].some((surface) => surface.contains(target)); if (!isDismissableSurface) { interceptedOutsideInteractionEventsRef.current.set(event.type, true); } if (event.type === "click") { window.setTimeout(() => { if (isPointerDownOutsideRef.current) { handleClickRef.current(); } }, 0); } } function handleInteractionBubble(event) { if (isPointerDownOutsideRef.current) { interceptedOutsideInteractionEventsRef.current.set(event.type, false); } } const handlePointerDown = (event) => { if (event.target && !isPointerInsideReactTreeRef.current) { let handleAndDispatchPointerDownOutsideEvent2 = function () { ownerDocument.removeEventListener("click", handleClickRef.current); const wasOutsideInteractionIntercepted = isOutsideInteractionIntercepted(); resetOutsideInteraction(); if (!wasOutsideInteractionIntercepted) { handleAndDispatchCustomEvent(POINTER_DOWN_OUTSIDE, handlePointerDownOutside, eventDetail, { discrete: true }); } }; const eventDetail = { originalEvent: event }; isPointerDownOutsideRef.current = true; isDeferredPointerDownOutsideRef.current = deferPointerDownOutside && event.button === 0; interceptedOutsideInteractionEventsRef.current.clear(); if (!deferPointerDownOutside || event.button !== 0) { handleAndDispatchPointerDownOutsideEvent2(); } else { ownerDocument.removeEventListener("click", handleClickRef.current); handleClickRef.current = handleAndDispatchPointerDownOutsideEvent2; ownerDocument.addEventListener("click", handleClickRef.current, { once: true }); } } else { ownerDocument.removeEventListener("click", handleClickRef.current); resetOutsideInteraction(); } isPointerInsideReactTreeRef.current = false; }; const outsideInteractionEvents = ["pointerup", "mousedown", "mouseup", "touchstart", "touchend", "click"]; for (const eventName of outsideInteractionEvents) { ownerDocument.addEventListener(eventName, handleInteractionCapture, true); ownerDocument.addEventListener(eventName, handleInteractionBubble); } const timerId = window.setTimeout(() => { ownerDocument.addEventListener("pointerdown", handlePointerDown); }, 0); return () => { window.clearTimeout(timerId); ownerDocument.removeEventListener("pointerdown", handlePointerDown); ownerDocument.removeEventListener("click", handleClickRef.current); for (const eventName of outsideInteractionEvents) { ownerDocument.removeEventListener(eventName, handleInteractionCapture, true); ownerDocument.removeEventListener(eventName, handleInteractionBubble); } }; }, [ownerDocument, handlePointerDownOutside, deferPointerDownOutside, isDeferredPointerDownOutsideRef, dismissableSurfaces]); return { // ensures we check React component tree (not just DOM tree) onPointerDownCapture: () => (isPointerInsideReactTreeRef.current = true), }; } function useFocusOutside(onFocusOutside, ownerDocument = globalThis?.document) { const handleFocusOutside = useCallbackRef$1(onFocusOutside); const isFocusInsideReactTreeRef = React$i.useRef(false); React$i.useEffect(() => { const handleFocus = (event) => { if (event.target && !isFocusInsideReactTreeRef.current) { const eventDetail = { originalEvent: event }; handleAndDispatchCustomEvent(FOCUS_OUTSIDE, handleFocusOutside, eventDetail, { discrete: false, }); } }; ownerDocument.addEventListener("focusin", handleFocus); return () => ownerDocument.removeEventListener("focusin", handleFocus); }, [ownerDocument, handleFocusOutside]); return { onFocusCapture: () => (isFocusInsideReactTreeRef.current = true), onBlurCapture: () => (isFocusInsideReactTreeRef.current = false), }; } function dispatchUpdate() { const event = new CustomEvent(CONTEXT_UPDATE); document.dispatchEvent(event); } function handleAndDispatchCustomEvent(name, handler, detail, { discrete }) { const target = detail.originalEvent.target; const event = new CustomEvent(name, { bubbles: false, cancelable: true, detail }); if (handler) target.addEventListener(name, handler, { once: true }); if (discrete) { dispatchDiscreteCustomEvent(target, event); } else { target.dispatchEvent(event); } } // src/focus-scope.tsx const React$h = await importShared("react"); var AUTOFOCUS_ON_MOUNT = "focusScope.autoFocusOnMount"; var AUTOFOCUS_ON_UNMOUNT = "focusScope.autoFocusOnUnmount"; var EVENT_OPTIONS = { bubbles: false, cancelable: true }; var FOCUS_SCOPE_NAME = "FocusScope"; var FocusScope = React$h.forwardRef((props, forwardedRef) => { const { loop = false, trapped = false, onMountAutoFocus: onMountAutoFocusProp, onUnmountAutoFocus: onUnmountAutoFocusProp, ...scopeProps } = props; const [container, setContainer] = React$h.useState(null); const onMountAutoFocus = useCallbackRef$1(onMountAutoFocusProp); const onUnmountAutoFocus = useCallbackRef$1(onUnmountAutoFocusProp); const lastFocusedElementRef = React$h.useRef(null); const composedRefs = useComposedRefs(forwardedRef, setContainer); const focusScope = React$h.useRef({ paused: false, pause() { this.paused = true; }, resume() { this.paused = false; }, }).current; React$h.useEffect(() => { if (trapped) { let handleFocusIn2 = function (event) { if (focusScope.paused || !container) return; const target = event.target; if (container.contains(target)) { lastFocusedElementRef.current = target; } else { focus(lastFocusedElementRef.current, { select: true }); } }, handleFocusOut2 = function (event) { if (focusScope.paused || !container) return; const relatedTarget = event.relatedTarget; if (relatedTarget === null) return; if (!container.contains(relatedTarget)) { focus(lastFocusedElementRef.current, { select: true }); } }, handleMutations2 = function (mutations) { const focusedElement = document.activeElement; if (focusedElement !== document.body) return; for (const mutation of mutations) { if (mutation.removedNodes.length > 0) focus(container); } }; document.addEventListener("focusin", handleFocusIn2); document.addEventListener("focusout", handleFocusOut2); const mutationObserver = new MutationObserver(handleMutations2); if (container) mutationObserver.observe(container, { childList: true, subtree: true }); return () => { document.removeEventListener("focusin", handleFocusIn2); document.removeEventListener("focusout", handleFocusOut2); mutationObserver.disconnect(); }; } }, [trapped, container, focusScope.paused]); React$h.useEffect(() => { if (container) { focusScopesStack.add(focusScope); const previouslyFocusedElement = document.activeElement; const hasFocusedCandidate = container.contains(previouslyFocusedElement); if (!hasFocusedCandidate) { const mountEvent = new CustomEvent(AUTOFOCUS_ON_MOUNT, EVENT_OPTIONS); container.addEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus); container.dispatchEvent(mountEvent); if (!mountEvent.defaultPrevented) { focusFirst(removeLinks(getTabbableCandidates(container)), { select: true }); if (document.activeElement === previouslyFocusedElement) { focus(container); } } } return () => { container.removeEventListener(AUTOFOCUS_ON_MOUNT, onMountAutoFocus); setTimeout(() => { const unmountEvent = new CustomEvent(AUTOFOCUS_ON_UNMOUNT, EVENT_OPTIONS); container.addEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus); container.dispatchEvent(unmountEvent); if (!unmountEvent.defaultPrevented) { focus(previouslyFocusedElement ?? document.body, { select: true }); } container.removeEventListener(AUTOFOCUS_ON_UNMOUNT, onUnmountAutoFocus); focusScopesStack.remove(focusScope); }, 0); }; } }, [container, onMountAutoFocus, onUnmountAutoFocus, focusScope]); const handleKeyDown = React$h.useCallback( (event) => { if (!loop && !trapped) return; if (focusScope.paused) return; const isTabKey = event.key === "Tab" && !event.altKey && !event.ctrlKey && !event.metaKey; const focusedElement = document.activeElement; if (isTabKey && focusedElement) { const container2 = event.currentTarget; const [first, last] = getTabbableEdges(container2); const hasTabbableElementsInside = first && last; if (!hasTabbableElementsInside) { if (focusedElement === container2) event.preventDefault(); } else { if (!event.shiftKey && focusedElement === last) { event.preventDefault(); if (loop) focus(first, { select: true }); } else if (event.shiftKey && focusedElement === first) { event.preventDefault(); if (loop) focus(last, { select: true }); } } } }, [loop, trapped, focusScope.paused], ); return /* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { tabIndex: -1, ...scopeProps, ref: composedRefs, onKeyDown: handleKeyDown }); }); FocusScope.displayName = FOCUS_SCOPE_NAME; function focusFirst(candidates, { select = false } = {}) { const previouslyFocusedElement = document.activeElement; for (const candidate of candidates) { focus(candidate, { select }); if (document.activeElement !== previouslyFocusedElement) return; } } function getTabbableEdges(container) { const candidates = getTabbableCandidates(container); const first = findVisible(candidates, container); const last = findVisible(candidates.reverse(), container); return [first, last]; } function getTabbableCandidates(container) { const nodes = []; const walker = document.createTreeWalker(container, NodeFilter.SHOW_ELEMENT, { acceptNode: (node) => { const isHiddenInput = node.tagName === "INPUT" && node.type === "hidden"; if (node.disabled || node.hidden || isHiddenInput) return NodeFilter.FILTER_SKIP; return node.tabIndex >= 0 ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP; }, }); while (walker.nextNode()) nodes.push(walker.currentNode); return nodes; } function findVisible(elements, container) { for (const element of elements) { if (!isHidden(element, { upTo: container })) return element; } } function isHidden(node, { upTo }) { if (getComputedStyle(node).visibility === "hidden") return true; while (node) { if (upTo !== void 0 && node === upTo) return false; if (getComputedStyle(node).display === "none") return true; node = node.parentElement; } return false; } function isSelectableInput(element) { return element instanceof HTMLInputElement && "select" in element; } function focus(element, { select = false } = {}) { if (element && element.focus) { const previouslyFocusedElement = document.activeElement; element.focus({ preventScroll: true }); if (element !== previouslyFocusedElement && isSelectableInput(element) && select) element.select(); } } var focusScopesStack = createFocusScopesStack(); function createFocusScopesStack() { let stack = []; return { add(focusScope) { const activeFocusScope = stack[0]; if (focusScope !== activeFocusScope) { activeFocusScope?.pause(); } stack = arrayRemove(stack, focusScope); stack.unshift(focusScope); }, remove(focusScope) { stack = arrayRemove(stack, focusScope); stack[0]?.resume(); }, }; } function arrayRemove(array, item) { const updatedArray = [...array]; const index = updatedArray.indexOf(item); if (index !== -1) { updatedArray.splice(index, 1); } return updatedArray; } function removeLinks(items) { return items.filter((item) => item.tagName !== "A"); } // src/portal.tsx const React$g = await importShared("react"); const ReactDOM$2 = await importShared("react-dom"); var PORTAL_NAME$1 = "Portal"; var Portal = React$g.forwardRef((props, forwardedRef) => { const { container: containerProp, ...portalProps } = props; const [mounted, setMounted] = React$g.useState(false); useLayoutEffect2(() => setMounted(true), []); const container = containerProp || (mounted && globalThis?.document?.body); return container ? ReactDOM$2.createPortal(/* @__PURE__ */ jsxRuntimeExports.jsx(Primitive.div, { ...portalProps, ref: forwardedRef }), container) : null; }); Portal.displayName = PORTAL_NAME$1; // src/focus-guards.tsx const React$f = await importShared("react"); var count = 0; var guards = null; function useFocusGuards() { React$f.useEffect(() => { if (!guards) { guards = { start: createFocusGuard(), end: createFocusGuard() }; } const { start, end } = guards; if (document.body.firstElementChild !== start) { document.body.insertAdjacentElement("afterbegin", start); } if (document.body.lastElementChild !== end) { document.body.insertAdjacentElement("beforeend", end); } count++; return () => { if (count === 1) { guards?.start.remove(); guards?.end.remove(); guards = null; } count = Math.max(0, count - 1); }; }, []); } function createFocusGuard() { const element = document.createElement("span"); element.setAttribute("data-radix-focus-guard", ""); element.tabIndex = 0; element.style.outline = "none"; element.style.opacity = "0"; element.style.position = "fixed"; element.style.pointerEvents = "none"; return element; } /****************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ /* global Reflect, Promise, SuppressedError, Symbol, Iterator */ var __assign = function () { __assign = Object.assign || function __assign(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; function __rest(s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === "function") for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; } function __spreadArray(to, from, pack) { if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { if (ar || !(i in from)) { if (!ar) ar = Array.prototype.slice.call(from, 0, i); ar[i] = from[i]; } } return to.concat(ar || Array.prototype.slice.call(from)); } typeof SuppressedError === "function" ? SuppressedError : ( function (error, suppressed, message) { var e = new Error(message); return ((e.name = "SuppressedError"), (e.error = error), (e.suppressed = suppressed), e); } ); var zeroRightClassName = "right-scroll-bar-position"; var fullWidthClassName = "width-before-scroll-bar"; var noScrollbarsClassName = "with-scroll-bars-hidden"; /** * Name of a CSS variable containing the amount of "hidden" scrollbar * ! might be undefined ! use will fallback! */ var removedBarSizeVariable = "--removed-body-scroll-bar-size"; /** * Assigns a value for a given ref, no matter of the ref format * @param {RefObject} ref - a callback function or ref object * @param value - a new value * * @see https://github.com/theKashey/use-callback-ref#assignref * @example * const refObject = useRef(); * const refFn = (ref) => {....} * * assignRef(refObject, "refValue"); * assignRef(refFn, "refValue"); */ function assignRef(ref, value) { if (typeof ref === "function") { ref(value); } else if (ref) { ref.current = value; } return ref; } const { useState } = await importShared("react"); /** * creates a MutableRef with ref change callback * @param initialValue - initial ref value * @param {Function} callback - a callback to run when value changes * * @example * const ref = useCallbackRef(0, (newValue, oldValue) => console.log(oldValue, '->', newValue); * ref.current = 1; * // prints 0 -> 1 * * @see https://reactjs.org/docs/hooks-reference.html#useref * @see https://github.com/theKashey/use-callback-ref#usecallbackref---to-replace-reactuseref * @returns {MutableRefObject} */ function useCallbackRef(initialValue, callback) { var ref = useState(function () { return { // value value: initialValue, // last callback callback: callback, // "memoized" public interface facade: { get current() { return ref.value; }, set current(value) { var last = ref.value; if (last !== value) { ref.value = value; ref.callback(value, last); } }, }, }; })[0]; // update callback ref.callback = callback; return ref.facade; } const React$e = await importShared("react"); var useIsomorphicLayoutEffect = typeof window !== "undefined" ? React$e.useLayoutEffect : React$e.useEffect; var currentValues = new WeakMap(); /** * Merges two or more refs together providing a single interface to set their value * @param {RefObject|Ref} refs * @returns {MutableRefObject} - a new ref, which translates all changes to {refs} * * @see {@link mergeRefs} a version without buit-in memoization * @see https://github.com/theKashey/use-callback-ref#usemergerefs * @example * const Component = React.forwardRef((props, ref) => { * const ownRef = useRef(); * const domRef = useMergeRefs([ref, ownRef]); // 👈 merge together * return
...
* } */ function useMergeRefs(refs, defaultValue) { var callbackRef = useCallbackRef(null, function (newValue) { return refs.forEach(function (ref) { return assignRef(ref, newValue); }); }); // handle refs changes - added or removed useIsomorphicLayoutEffect( function () { var oldValue = currentValues.get(callbackRef); if (oldValue) { var prevRefs_1 = new Set(oldValue); var nextRefs_1 = new Set(refs); var current_1 = callbackRef.current; prevRefs_1.forEach(function (ref) { if (!nextRefs_1.has(ref)) { assignRef(ref, null); } }); nextRefs_1.forEach(function (ref) { if (!prevRefs_1.has(ref)) { assignRef(ref, current_1); } }); } currentValues.set(callbackRef, refs); }, [refs], ); return callbackRef; } function ItoI(a) { return a; } function innerCreateMedium(defaults, middleware) { if (middleware === void 0) { middleware = ItoI; } var buffer = []; var assigned = false; var medium = { read: function () { if (assigned) { throw new Error("Sidecar: could not `read` from an `assigned` medium. `read` could be used only with `useMedium`."); } if (buffer.length) { return buffer[buffer.length - 1]; } return defaults; }, useMedium: function (data) { var item = middleware(data, assigned); buffer.push(item); return function () { buffer = buffer.filter(function (x) { return x !== item; }); }; }, assignSyncMedium: function (cb) { assigned = true; while (buffer.length) { var cbs = buffer; buffer = []; cbs.forEach(cb); } buffer = { push: function (x) { return cb(x); }, filter: function () { return buffer; }, }; }, assignMedium: function (cb) { assigned = true; var pendingQueue = []; if (buffer.length) { var cbs = buffer; buffer = []; cbs.forEach(cb); pendingQueue = buffer; } var executeQueue = function () { var cbs = pendingQueue; pendingQueue = []; cbs.forEach(cb); }; var cycle = function () { return Promise.resolve().then(executeQueue); }; cycle(); buffer = { push: function (x) { pendingQueue.push(x); cycle(); }, filter: function (filter) { pendingQueue = pendingQueue.filter(filter); return buffer; }, }; }, }; return medium; } // eslint-disable-next-line @typescript-eslint/ban-types function createSidecarMedium(options) { if (options === void 0) { options = {}; } var medium = innerCreateMedium(null); medium.options = __assign({ async: true, ssr: false }, options); return medium; } const React$d = await importShared("react"); var SideCar$1 = function (_a) { var sideCar = _a.sideCar, rest = __rest(_a, ["sideCar"]); if (!sideCar) { throw new Error("Sidecar: please provide `sideCar` property to import the right car"); } var Target = sideCar.read(); if (!Target) { throw new Error("Sidecar medium not found"); } return React$d.createElement(Target, __assign({}, rest)); }; SideCar$1.isSideCarExport = true; function exportSidecar(medium, exported) { medium.useMedium(exported); return SideCar$1; } var effectCar = createSidecarMedium(); const React$c = await importShared("react"); var nothing = function () { return; }; /** * Removes scrollbar from the page and contain the scroll within the Lock */ var RemoveScroll = React$c.forwardRef(function (props, parentRef) { var ref = React$c.useRef(null); var _a = React$c.useState({ onScrollCapture: nothing, onWheelCapture: nothing, onTouchMoveCapture: nothing, }), callbacks = _a[0], setCallbacks = _a[1]; var forwardProps = props.forwardProps, children = props.children, className = props.className, removeScrollBar = props.removeScrollBar, enabled = props.enabled, shards = props.shards, sideCar = props.sideCar, noRelative = props.noRelative, noIsolation = props.noIsolation, inert = props.inert, allowPinchZoom = props.allowPinchZoom, _b = props.as, Container = _b === void 0 ? "div" : _b, gapMode = props.gapMode, rest = __rest(props, ["forwardProps", "children", "className", "removeScrollBar", "enabled", "shards", "sideCar", "noRelative", "noIsolation", "inert", "allowPinchZoom", "as", "gapMode"]); var SideCar = sideCar; var containerRef = useMergeRefs([ref, parentRef]); var containerProps = __assign(__assign({}, rest), callbacks); return React$c.createElement( React$c.Fragment, null, enabled && React$c.createElement(SideCar, { sideCar: effectCar, removeScrollBar: removeScrollBar, shards: shards, noRelative: noRelative, noIsolation: noIsolation, inert: inert, setCallbacks: setCallbacks, allowPinchZoom: !!allowPinchZoom, lockRef: ref, gapMode: gapMode, }), forwardProps ? React$c.cloneElement(React$c.Children.only(children), __assign(__assign({}, containerProps), { ref: containerRef })) : React$c.createElement(Container, __assign({}, containerProps, { className: className, ref: containerRef }), children), ); }); RemoveScroll.defaultProps = { enabled: true, removeScrollBar: true, inert: false, }; RemoveScroll.classNames = { fullWidth: fullWidthClassName, zeroRight: zeroRightClassName, }; var getNonce = function () { if (typeof __webpack_nonce__ !== "undefined") { return __webpack_nonce__; } return undefined; }; function makeStyleTag() { if (!document) return null; var tag = document.createElement("style"); tag.type = "text/css"; var nonce = getNonce(); if (nonce) { tag.setAttribute("nonce", nonce); } return tag; } function injectStyles(tag, css) { // @ts-ignore if (tag.styleSheet) { // @ts-ignore tag.styleSheet.cssText = css; } else { tag.appendChild(document.createTextNode(css)); } } function insertStyleTag(tag) { var head = document.head || document.getElementsByTagName("head")[0]; head.appendChild(tag); } var stylesheetSingleton = function () { var counter = 0; var stylesheet = null; return { add: function (style) { if (counter == 0) { if ((stylesheet = makeStyleTag())) { injectStyles(stylesheet, style); insertStyleTag(stylesheet); } } counter++; }, remove: function () { counter--; if (!counter && stylesheet) { stylesheet.parentNode && stylesheet.parentNode.removeChild(stylesheet); stylesheet = null; } }, }; }; const React$b = await importShared("react"); /** * creates a hook to control style singleton * @see {@link styleSingleton} for a safer component version * @example * ```tsx * const useStyle = styleHookSingleton(); * /// * useStyle('body { overflow: hidden}'); */ var styleHookSingleton = function () { var sheet = stylesheetSingleton(); return function (styles, isDynamic) { React$b.useEffect( function () { sheet.add(styles); return function () { sheet.remove(); }; }, [styles && isDynamic], ); }; }; /** * create a Component to add styles on demand * - styles are added when first instance is mounted * - styles are removed when the last instance is unmounted * - changing styles in runtime does nothing unless dynamic is set. But with multiple components that can lead to the undefined behavior */ var styleSingleton = function () { var useStyle = styleHookSingleton(); var Sheet = function (_a) { var styles = _a.styles, dynamic = _a.dynamic; useStyle(styles, dynamic); return null; }; return Sheet; }; var zeroGap = { left: 0, top: 0, right: 0, gap: 0, }; var parse = function (x) { return parseInt(x || "", 10) || 0; }; var getOffset = function (gapMode) { var cs = window.getComputedStyle(document.body); var left = cs[gapMode === "padding" ? "paddingLeft" : "marginLeft"]; var top = cs[gapMode === "padding" ? "paddingTop" : "marginTop"]; var right = cs[gapMode === "padding" ? "paddingRight" : "marginRight"]; return [parse(left), parse(top), parse(right)]; }; var getGapWidth = function (gapMode) { if (gapMode === void 0) { gapMode = "margin"; } if (typeof window === "undefined") { return zeroGap; } var offsets = getOffset(gapMode); var documentWidth = document.documentElement.clientWidth; var windowWidth = window.innerWidth; return { left: offsets[0], top: offsets[1], right: offsets[2], gap: Math.max(0, windowWidth - documentWidth + offsets[2] - offsets[0]), }; }; const React$a = await importShared("react"); var Style = styleSingleton(); var lockAttribute = "data-scroll-locked"; // important tip - once we measure scrollBar width and remove them // we could not repeat this operation // thus we are using style-singleton - only the first "yet correct" style will be applied. var getStyles = function (_a, allowRelative, gapMode, important) { var left = _a.left, top = _a.top, right = _a.right, gap = _a.gap; if (gapMode === void 0) { gapMode = "margin"; } return "\n ." .concat(noScrollbarsClassName, " {\n overflow: hidden ") .concat(important, ";\n padding-right: ") .concat(gap, "px ") .concat(important, ";\n }\n body[") .concat(lockAttribute, "] {\n overflow: hidden ") .concat(important, ";\n overscroll-behavior: contain;\n ") .concat( [ allowRelative && "position: relative ".concat(important, ";"), gapMode === "margin" && "\n padding-left: " .concat(left, "px;\n padding-top: ") .concat(top, "px;\n padding-right: ") .concat(right, "px;\n margin-left:0;\n margin-top:0;\n margin-right: ") .concat(gap, "px ") .concat(important, ";\n "), gapMode === "padding" && "padding-right: ".concat(gap, "px ").concat(important, ";"), ] .filter(Boolean) .join(""), "\n }\n \n .", ) .concat(zeroRightClassName, " {\n right: ") .concat(gap, "px ") .concat(important, ";\n }\n \n .") .concat(fullWidthClassName, " {\n margin-right: ") .concat(gap, "px ") .concat(important, ";\n }\n \n .") .concat(zeroRightClassName, " .") .concat(zeroRightClassName, " {\n right: 0 ") .concat(important, ";\n }\n \n .") .concat(fullWidthClassName, " .") .concat(fullWidthClassName, " {\n margin-right: 0 ") .concat(important, ";\n }\n \n body[") .concat(lockAttribute, "] {\n ") .concat(removedBarSizeVariable, ": ") .concat(gap, "px;\n }\n"); }; var getCurrentUseCounter = function () { var counter = parseInt(document.body.getAttribute(lockAttribute) || "0", 10); return isFinite(counter) ? counter : 0; }; var useLockAttribute = function () { React$a.useEffect(function () { document.body.setAttribute(lockAttribute, (getCurrentUseCounter() + 1).toString()); return function () { var newCounter = getCurrentUseCounter() - 1; if (newCounter <= 0) { document.body.removeAttribute(lockAttribute); } else { document.body.setAttribute(lockAttribute, newCounter.toString()); } }; }, []); }; /** * Removes page scrollbar and blocks page scroll when mounted */ var RemoveScrollBar = function (_a) { var noRelative = _a.noRelative, noImportant = _a.noImportant, _b = _a.gapMode, gapMode = _b === void 0 ? "margin" : _b; useLockAttribute(); /* gap will be measured on every component mount however it will be used only by the "first" invocation due to singleton nature of