lib0
Version:
isomorphic utility functions
819 lines (801 loc) • 30.7 kB
JavaScript
import * as delta from '../delta.js'
import * as math from '../../math.js'
import { Transformer, Template, createTransformResult } from './core.js'
/**
* The (unexported) delta {@link import('../delta.js').createMark Mark} type, referenced here for the
* local mark-bucketing annotations.
*
* @typedef {ReturnType<typeof delta.createMark>} Mark
*/
/**
* One entry of the segment layout maintained by {@link InlineTransformer}. A segment is either a
* coalesced run of pass-through positions (root text + opaque non-inlined nodes - same length in both
* coordinate spaces) or a single inline node (1 structured position that expands to `inlineLen`
* inlined positions).
*/
class Seg {
/**
* @param {boolean} isInline whether this seg is an inline node (its children are spliced into the parent)
* @param {number} structLen
* @param {number} inlineLen
*/
constructor (isInline, structLen, inlineLen) {
this.isInline = isInline
this.structLen = structLen
this.inlineLen = inlineLen
}
}
/**
* Whether `el` is a child node that should be inlined - i.e. a node delta whose `name` is in the
* configured `names` set (use `null` to select anonymous/typeless nodes).
*
* @param {any} el
* @param {Array<string|null>} names
* @return {boolean}
*/
const isInlineNode = (el, names) => delta.$deltaAny.check(el) && names.includes(el.name)
/**
* Whether any direct child of `d` is an inline node - the only thing this transformer inlines. Called
* on each incoming change to feed the cached {@link InlineTransformer#hasInline} flag, which drives
* the fast-path: while no inline node has ever appeared, the structured and inlined representations
* are identical and a change is its own transform. An inline node nested inside a non-inlined node is
* opaque (one level) and intentionally not counted.
*
* @param {delta.DeltaAny} d
* @param {Array<string|null>} names
* @return {boolean}
*/
const hasInlineChild = (d, names) => {
for (const op of d.children) {
if (delta.$insertOp.check(op)) {
for (const el of op.insert) {
if (isInlineNode(el, names)) return true
}
}
}
return false
}
/**
* Net change in a parent's direct child count from applying a settled child-change delta: positions
* inserted minus positions deleted (retain/modify keep the count). Lets an inline node's inlined
* length be kept current when a `modify` edits it, without re-deriving the node's children.
*
* @param {delta.DeltaAny} d
* @return {number}
*/
const netLen = d => {
let n = 0
for (const op of d.children) {
if (delta.$insertOp.check(op) || delta.$textOp.check(op)) {
n += op.insert.length
} else if (delta.$deleteOp.check(op)) {
n -= op.delete
}
}
return n
}
/**
* Apply a settled *structured* change to the segment layout `segs` in place, advancing it to the
* layout of the changed state without re-deriving the whole list. The cursor `(si, so)` is a
* structured position - seg index plus structural offset within that seg. The layout is kept
* coalesced (no two adjacent pass-through segs, no empty pass-through seg; every inline node is its
* own length-1 seg, kept even when its inlined length is 0) so it stays identical to a from-scratch
* derivation of the settled state. One level only: a direct inline node's length is `childCnt`; an
* inline node nested deeper is an opaque element of length 1.
*
* @param {Array<Seg>} segs
* @param {delta.DeltaAny} change
* @param {Array<string|null>} names the node names that are inlined (see {@link isInlineNode})
*/
const applySegsChange = (segs, change, names) => {
let si = 0
let so = 0
// Move the cursor onto a seg boundary (so === 0), splitting the current pass-through seg if the
// cursor sits in its interior. A null seg has structLen 1, so the cursor is never in its interior.
const ensureBoundary = () => {
if (so === 0) return
const s = segs[si]
segs.splice(si + 1, 0, new Seg(false, s.structLen - so, s.inlineLen - so))
s.structLen = so
s.inlineLen = so
si++
so = 0
}
for (const op of change.children) {
if (delta.$retainOp.check(op)) {
// formats on a retain never change the layout - just advance the cursor
let rem = op.retain
while (rem > 0 && si < segs.length) {
const room = segs[si].structLen - so
if (rem < room) {
so += rem
rem = 0
} else {
rem -= room
si++
so = 0
}
}
} else if (delta.$textOp.check(op) || delta.$insertOp.check(op)) {
// Build the inserted layout: a text op is one pass-through run; an insert op is a mix of
// length-1 null segs and pass-through runs that coalesce the opaque (named) elements.
/** @type {Array<Seg>} */
const ins = []
if (delta.$textOp.check(op)) {
ins.push(new Seg(false, op.insert.length, op.insert.length))
} else {
for (const el of op.insert) {
const last = ins[ins.length - 1]
if (isInlineNode(el, names)) {
ins.push(new Seg(true, 1, /** @type {delta.DeltaAny} */ (el).childCnt))
} else if (last !== undefined && !last.isInline) {
last.structLen++
last.inlineLen++
} else {
ins.push(new Seg(false, 1, 1))
}
}
}
if (ins.length === 1 && !ins[0].isInline) {
// fast path - pure pass-through content grows an adjacent run (no split); the common case
const len = ins[0].structLen
if (so > 0) {
segs[si].structLen += len
segs[si].inlineLen += len
so += len
} else if (si > 0 && !segs[si - 1].isInline) {
segs[si - 1].structLen += len
segs[si - 1].inlineLen += len
} else if (si < segs.length && !segs[si].isInline) {
segs[si].structLen += len
segs[si].inlineLen += len
so = len
} else {
segs.splice(si, 0, ins[0])
si++
}
} else {
// general case (the content contains an inline node): land on a boundary, then coalesce the
// block's pass-through ends into the surrounding runs
ensureBoundary()
let block = ins
if (si > 0 && !segs[si - 1].isInline && !block[0].isInline) {
segs[si - 1].structLen += block[0].structLen
segs[si - 1].inlineLen += block[0].inlineLen
block = block.slice(1)
}
let tail = 0
if (si < segs.length && !segs[si].isInline && block.length > 0 && !block[block.length - 1].isInline) {
const lastSeg = block[block.length - 1]
tail = lastSeg.structLen
segs[si].structLen += lastSeg.structLen
segs[si].inlineLen += lastSeg.inlineLen
block = block.slice(0, -1)
}
segs.splice(si, 0, ...block)
si += block.length
so = tail
}
} else if (delta.$deleteOp.check(op)) {
ensureBoundary()
let rem = op.delete
while (rem > 0 && si < segs.length) {
const s = segs[si]
if (s.isInline) {
// the single structured position is the whole inline node
segs.splice(si, 1)
rem -= 1
} else {
const take = math.min(s.structLen, rem)
s.structLen -= take
s.inlineLen -= take
rem -= take
if (s.structLen === 0) segs.splice(si, 1)
// a partial delete leaves rem 0 and the shortened seg in place
}
}
// a deletion can bring two pass-through runs together - coalesce, keeping the cursor on the seam
if (si > 0 && si < segs.length && !segs[si - 1].isInline && !segs[si].isInline) {
const leftLen = segs[si - 1].structLen
segs[si - 1].structLen += segs[si].structLen
segs[si - 1].inlineLen += segs[si].inlineLen
segs.splice(si, 1)
si--
so = leftLen
}
} else if (delta.$modifyOp.check(op)) {
if (si < segs.length) {
const s = segs[si]
if (so === 0 && s.isInline) {
// editing inside an inline node changes only its inlined length
s.inlineLen += netLen(op.value)
si++
} else {
// an opaque (named) node - no layout change; step over its single structured position
so++
if (so >= s.structLen) {
si++
so = 0
}
}
}
}
}
}
/**
* Map a structured root-mark offset to its inlined offset through the seg layout. A mark on a
* pass-through position keeps its in-run offset; a mark on an inline node's own structured position
* maps to that node's inlined start (its `childStart`). A key past the end clamps to the inlined end
* (a change-delta root mark may sit at the trailing implicit-retain boundary).
*
* @param {Array<Seg>} segs
* @param {number} key
* @return {number}
*/
const structOffsetToInlineOffset = (segs, key) => {
let s = 0
let inl = 0
for (const seg of segs) {
if (key < s + seg.structLen) return seg.isInline ? inl : inl + (key - s)
s += seg.structLen
inl += seg.inlineLen
}
return inl
}
/**
* Map an inlined root-mark offset to a structured location. `inner` is `null` for a pass-through
* position (the mark is a structured root mark at `struct`); otherwise the offset fell inside the
* inline node at structured position `struct` (seg index `si`), and `inner` is the offset within that
* node. A key past the end clamps to the structured end.
*
* @param {Array<Seg>} segs
* @param {number} key
* @return {{ struct: number, inner: number|null, si: number }}
*/
const inlineOffsetToStructLocation = (segs, key) => {
let s = 0
let inl = 0
for (let si = 0; si < segs.length; si++) {
const seg = segs[si]
if (key < inl + seg.inlineLen) {
return seg.isInline ? { struct: s, inner: key - inl, si } : { struct: s + (key - inl), inner: null, si }
}
s += seg.structLen
inl += seg.inlineLen
}
return { struct: s, inner: null, si: segs.length }
}
/**
* Build an output delta that passes through the node name and attribute ops of `src` (only the
* children are transformed by this transformer).
*
* reason: returns `any` rather than `DeltaBuilderAny` on purpose. The result is internal scratch the
* transformer mutates with fluent builder calls; typing it would instantiate a `DeltaConfOverwrite`
* per call, and across this file that tips the shared TS2589 instantiation budget that the long
* pipe-type test (delta-transformer.test.js) sits against. The public output type lives on `init`.
*
* @param {delta.DeltaAny} src
* @return {any}
*/
const passThrough = src => {
const d = delta.create(/** @type {any} */ (src.name))
for (const op of src.attrs) {
// reason: `attrs` is a mapped type over the conf's attr keys, so a dynamic-key write can't be
// checked (same limitation delta.slice suppresses when copying attrs).
// @ts-ignore
d.attrs[op.key] = op.clone()
}
// carry the conservative mark flag (covers attr-value marks the direct assignment above bypasses);
// marks carried into the rebuilt children below flag it via the builder / addRootMark
d.maybeHasMarks = src.maybeHasMarks
return d
}
/**
* Stateful transformer that inlines child nodes whose name is in the configured `names` set. Side A
* is the structured representation (inline nodes present, e.g. `<p>some<>text</></p>`), side B is the
* inlined representation (inline nodes flattened, e.g. `<p>sometext</p>`).
*
* The state the user calls "offsets and inline node length" is the segment layout {@link Seg} stored
* in `segs`: a coalesced run-length view of the structured side where each inline node is one
* structured position expanding to `inlineLen` inlined positions, and every other position (root text
* and opaque non-inlined nodes) is a pass-through run. It is kept current incrementally - every mapped
* change is folded into `segs` by {@link applySegsChange} rather than re-derived from scratch.
*
* @extends {Transformer<any,any>}
*/
export class InlineTransformer extends Transformer {
/**
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
* @param {Array<string|null>} names the node names to inline (`null` selects anonymous nodes)
*/
constructor ($in, $out, names) {
super($in, $out)
/**
* @type {Array<string|null>}
*/
this.names = names
/**
* The segment layout of the structured side, maintained incrementally (see {@link applySegsChange}).
*
* @type {Array<Seg>}
*/
this.segs = []
/**
* Whether the structured side has ever contained a direct inline-node child. Monotonic: once a
* change introduces an inline node it stays set (even if that node is later deleted), which only
* costs a missed fast-path, never correctness.
*
* @type {boolean}
*/
this.hasInline = false
}
/**
* structured change -> inlined change
*
* @param {delta.DeltaBuilderAny} da
*/
applyA (da) {
this.hasInline ||= hasInlineChild(da, this.names)
if (!this.hasInline) {
// fast-path: nothing to inline now or after this change - structured === inlined, so the
// change passes through unchanged. Keep the layout current and hand back the same delta.
applySegsChange(this.segs, da, this.names)
return createTransformResult(null, da)
}
const segs = this.segs
const b = passThrough(da)
let si = 0
let so = 0
let srcInline = 0
let emitted = 0
// position in the resulting inlined doc that `b` has built up to (retain + insert advance it,
// delete does not). Used to anchor marks lifted out of a spliced inline node (A.3).
let outPos = 0
/**
* @param {number} target
*/
const alignB = target => {
if (target > emitted) {
b.retain(target - emitted)
outPos += target - emitted
emitted = target
}
}
/**
* Advance the structured cursor by `n` positions, returning the inlined length spanned.
*
* @param {number} n
*/
const advanceStruct = n => {
let rem = n
let inl = 0
while (rem > 0 && si < segs.length) {
const s = segs[si]
const take = math.min(s.structLen - so, rem)
inl += s.isInline ? s.inlineLen : take
so += take
rem -= take
if (so >= s.structLen) {
si++
so = 0
}
}
return inl
}
for (const op of da.children) {
if (delta.$retainOp.check(op)) {
if (op.format === undefined && op.attribution === undefined) {
srcInline += advanceStruct(op.retain)
} else {
// formatted retain: emit the format over the mapped inline range. An inline node's wrapper
// has no inlined representation, so its format is dropped (consistent with the inliner); root
// text and opaque nodes carry the format straight through.
alignB(srcInline)
let rem = op.retain
while (rem > 0 && si < segs.length) {
const s = segs[si]
const take = math.min(s.structLen - so, rem)
const inl = s.isInline ? s.inlineLen : take
b.retain(inl, s.isInline ? undefined : op.format, s.isInline ? undefined : op.attribution)
emitted += inl
outPos += inl
srcInline += inl
so += take
rem -= take
if (so >= s.structLen) { si++; so = 0 }
}
}
} else if (delta.$textOp.check(op)) {
alignB(srcInline)
b.insert(op.insert, op.format, op.attribution)
outPos += op.insert.length
} else if (delta.$insertOp.check(op)) {
alignB(srcInline)
for (const el of op.insert) {
if (isInlineNode(el, this.names)) {
// splice the inline node's children verbatim (one level), then lift the node's own root
// marks onto the flattened parent at `childStart + innerKey` (A.3). String-keyed (attr)
// marks on the inline node have no home in the flattened parent and are dropped.
const node = /** @type {delta.DeltaAny} */ (el)
const base = outPos
b.append(node)
outPos += node.childCnt
delta.mergeRootMarks(b, node, k => typeof k === 'number' ? base + k : null)
} else {
b.insert([el], op.format, op.attribution)
outPos += 1
}
}
} else if (delta.$deleteOp.check(op)) {
alignB(srcInline)
const delInline = advanceStruct(op.delete)
b.delete(delInline)
srcInline += delInline
emitted += delInline
} else if (delta.$modifyOp.check(op)) {
const s = segs[si]
alignB(srcInline)
if (s !== undefined && s.isInline) {
// data side edited inside an inline node: its inner ops act on the node's children, which are
// spliced into b at the same offset - replay them directly
const base = outPos
for (const inner of op.value.children) {
if (delta.$retainOp.check(inner)) {
b.retain(inner.retain, inner.format, inner.attribution)
emitted += inner.retain
outPos += inner.retain
} else if (delta.$textOp.check(inner) || delta.$insertOp.check(inner)) {
b.insert(inner.insert, inner.format, inner.attribution)
outPos += inner.insert.length
} else if (delta.$deleteOp.check(inner)) {
b.delete(inner.delete)
emitted += inner.delete
} else if (delta.$modifyOp.check(inner)) {
b.modify(delta.clone(inner.value), inner.format, inner.attribution)
emitted += 1
outPos += 1
}
}
// marks set inside the inline node by this change lift onto the flattened parent (A.3)
delta.mergeRootMarks(b, op.value, k => typeof k === 'number' ? base + k : null)
srcInline += s.inlineLen
advanceStruct(1)
} else {
// opaque (named) node: forward the modify verbatim
b.modify(delta.clone(op.value), op.format, op.attribution)
emitted += 1
outPos += 1
srcInline += advanceStruct(1)
}
}
}
applySegsChange(this.segs, da, this.names)
// carry the change's own (root) marks, re-anchoring number offsets through the post-change layout;
// attr-key marks pass through unchanged. deleteMarks ride verbatim (merged with any lifted above).
delta.mergeRootMarks(b, da, k => typeof k === 'number' ? structOffsetToInlineOffset(this.segs, k) : k)
b.done(false)
return createTransformResult(null, b)
}
/**
* inlined change -> structured change
*
* @param {delta.DeltaBuilderAny} db
*/
applyB (db) {
this.hasInline ||= hasInlineChild(db, this.names)
if (!this.hasInline) {
// fast-path: with no inline nodes the inlined and structured coordinate spaces coincide, so the
// inlined change is already the structured change.
applySegsChange(this.segs, db, this.names)
return createTransformResult(db, null)
}
const segs = this.segs
const a = passThrough(db)
// --- marks: bucket the change's own (root) marks against the (pre-change) layout. A pass-through
// offset (or an attr-key mark) becomes a structured root mark; an offset inside an inline node is
// routed into that node - seeded into its modify when the walk opens one (below), else emitted as a
// mark-only modify (pure cursor move) or collapsed to the node position (mixed change) after the
// walk. NB: a change that simultaneously restructures content and moves a cursor maps the cursor in
// the pre-change layout (best-effort - marks are local/ephemeral).
/** @type {Array<Mark>} */
const passMarks = []
/** @type {Map<number, Array<{ innerKey: number, mark: Mark }>>} */
const innerBySi = new Map()
if (db.marks !== null) {
for (const m of db.marks) {
if (typeof m.key !== 'number') { passMarks.push(m); continue }
const r = inlineOffsetToStructLocation(segs, m.key)
if (r.inner === null) {
passMarks.push(m.copy(r.struct))
} else {
const arr = innerBySi.get(r.si) ?? []
arr.push({ innerKey: r.inner, mark: m })
innerBySi.set(r.si, arr)
}
}
}
// structured start offset of each seg (pre-change), for placing inner marks after the walk
/** @type {Array<number>} */
const segStart = []
for (let i = 0, acc = 0; i < segs.length; i++) { segStart.push(acc); acc += segs[i].structLen }
let hadChildOps = false
let si = 0
let so = 0
let structBefore = 0
let emittedStruct = 0
// An accumulator for edits that fall inside the current inline node. Several inlined ops can
// target the same inline node, so they must coalesce into a single `modify` op. `openSegIdx` is
// the seg index it belongs to, or -1 when there is no open modify.
let openInner = /** @type {any} */ (delta.create())
let openNodeStruct = 0
let openInnerEmitted = 0
let openSegIdx = -1
/**
* @param {number} target
*/
const alignA = target => {
if (target > emittedStruct) {
a.retain(target - emittedStruct)
emittedStruct = target
}
}
const flushModify = () => {
if (openSegIdx !== -1) {
alignA(openNodeStruct)
a.modify(openInner.done(false))
emittedStruct += 1
openSegIdx = -1
}
}
/**
* @param {number} target inlined offset within the open inline node
*/
const alignInner = target => {
if (target > openInnerEmitted) {
openInner.retain(target - openInnerEmitted)
openInnerEmitted = target
}
}
/**
* @param {number} idx
*/
const openModifyFor = idx => {
openInner = /** @type {any} */ (delta.create())
openNodeStruct = structBefore
openInnerEmitted = 0
openSegIdx = idx
// seed any inner marks bound to this node so they ride on the modify's value (A.3, reverse)
const im = innerBySi.get(idx)
if (im !== undefined) {
for (const { innerKey, mark } of im) delta.addRootMark(openInner, mark.copy(innerKey))
innerBySi.delete(idx) // consumed - excluded from the post-walk fallback
}
}
// empty inline nodes (inlineLen 0) are zero-width in inlined coords - step over them. Only at a
// seg boundary (so === 0); `so` does not change while skipping.
const skipEmpty = () => {
if (so !== 0) return
while (si < segs.length && segs[si].isInline && segs[si].inlineLen === 0) {
structBefore += segs[si].structLen
si++
}
}
for (const op of db.children) {
hadChildOps = true
if (delta.$retainOp.check(op)) {
const fmt = op.format
const attr = op.attribution
let rem = op.retain
while (rem > 0) {
skipEmpty()
if (si >= segs.length) break
const s = segs[si]
const take = math.min(s.inlineLen - so, rem)
if (fmt !== undefined || attr !== undefined) {
// a formatted retain re-formats existing content: pass-through content takes it
// directly; content inside an inline node is reached through a modify on that node.
if (!s.isInline) {
flushModify()
alignA(structBefore + so)
a.retain(take, fmt, attr)
emittedStruct += take
} else {
if (openSegIdx !== si) {
flushModify()
openModifyFor(si)
}
alignInner(so)
openInner.retain(take, fmt, attr)
openInnerEmitted += take
}
}
so += take
rem -= take
if (so >= s.inlineLen) {
structBefore += s.structLen
si++
so = 0
}
}
} else if (delta.$textOp.check(op) || delta.$insertOp.check(op)) {
skipEmpty()
const s = segs[si]
if (s === undefined) {
flushModify()
alignA(structBefore)
a.insert(op.insert, op.format, op.attribution)
} else if (!s.isInline) {
flushModify()
alignA(structBefore + so)
a.insert(op.insert, op.format, op.attribution)
} else if (so === 0) {
// boundary preference: an insert at the start of an inline node lands in the root
flushModify()
alignA(structBefore)
a.insert(op.insert, op.format, op.attribution)
} else {
// strict interior: insert into the inline node
if (openSegIdx !== si) {
flushModify()
openModifyFor(si)
}
alignInner(so)
openInner.insert(op.insert, op.format, op.attribution)
}
// an insert does not advance the cursor (it adds content not in the original)
} else if (delta.$deleteOp.check(op)) {
let rem = op.delete
while (rem > 0) {
skipEmpty()
if (si >= segs.length) break
const s = segs[si]
if (!s.isInline) {
const k = math.min(s.inlineLen - so, rem)
flushModify()
alignA(structBefore + so)
a.delete(k)
emittedStruct += k
so += k
rem -= k
if (so >= s.inlineLen) {
structBefore += s.structLen
si++
so = 0
}
} else if (so === 0 && rem >= s.inlineLen) {
// the delete covers the whole node from its start - delete it structurally
flushModify()
alignA(structBefore)
a.delete(1)
emittedStruct += 1
rem -= s.inlineLen
structBefore += s.structLen
si++
so = 0
} else {
// strict interior delete: remove inside the inline node
const k = math.min(s.inlineLen - so, rem)
if (openSegIdx !== si) {
flushModify()
openModifyFor(si)
}
alignInner(so)
openInner.delete(k)
openInnerEmitted += k
so += k
rem -= k
if (so >= s.inlineLen) {
structBefore += s.structLen
si++
so = 0
}
}
}
} else if (delta.$modifyOp.check(op)) {
skipEmpty()
const s = segs[si]
if (s !== undefined && s.isInline) {
// modify targets an element inside the inline node
if (openSegIdx !== si) {
flushModify()
openModifyFor(si)
}
alignInner(so)
openInner.modify(delta.clone(op.value), op.format, op.attribution)
openInnerEmitted += 1
so += 1
if (so >= s.inlineLen) {
structBefore += s.structLen
si++
so = 0
}
} else if (s !== undefined) {
flushModify()
alignA(structBefore + so)
a.modify(delta.clone(op.value), op.format, op.attribution)
emittedStruct += 1
so += 1
if (so >= s.inlineLen) {
structBefore += s.structLen
si++
so = 0
}
}
}
}
flushModify()
// place the change's root marks. Pass-through / attr-key marks are structured root marks. Inner
// marks not consumed by a modify above are emitted as exact mark-only modifies for a pure cursor
// move, else collapsed to the inline node's structured position (best-effort).
for (const m of passMarks) delta.addRootMark(a, m)
if (innerBySi.size > 0) {
if (!hadChildOps) {
let cursor = emittedStruct
for (const [segIdx, marks] of [...innerBySi.entries()].sort((x, y) => x[0] - y[0])) {
if (segStart[segIdx] > cursor) { a.retain(segStart[segIdx] - cursor); cursor = segStart[segIdx] }
const mod = /** @type {any} */ (delta.create())
for (const { innerKey, mark } of marks) delta.addRootMark(mod, mark.copy(innerKey))
a.modify(mod.done(false))
cursor += 1
}
} else {
for (const [segIdx, marks] of innerBySi) {
for (const { mark } of marks) delta.addRootMark(a, mark.copy(segStart[segIdx]))
}
}
}
if (db.deleteMarks !== null) {
for (const id of db.deleteMarks) delta.deleteRootMark(a, id)
}
applySegsChange(this.segs, a, this.names)
a.done(false)
return createTransformResult(a, null)
}
}
/**
* Template for {@link InlineTransformer}. Typed loosely: the inlined output shape depends on the
* runtime `names` and document content, so the OUT side is `any`.
*
* @template {delta.DeltaConf} [IN=any]
* @extends {Template<IN, any>}
*/
export class Inline extends Template {
/**
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
* @param {Array<string|null>} names the node names to inline (`null` selects anonymous nodes)
*/
constructor ($d, names) {
super($d, /** @type {any} */ (delta.$deltaAny))
this.names = names
}
get name () { return 'lib0:inline:' + this.names.join(',') }
/**
* @return {Transformer<IN, any>}
*/
init () {
return new InlineTransformer(this.$in, this.$out, this.names)
}
}
/**
* Inline child nodes whose name is in `names` by splicing each one's children into its parent (one
* level; e.g. with `[null]`: `<p>some<>text</></p>` <-> `<p>sometext</p>`). Use `null` for
* anonymous/typeless nodes, or a node name like `'b'`. Returns a reusable {@link Inline} template
* (a `project` hole, or `.init()` for a standalone transformer).
*
* @template {delta.DeltaConf} IN
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
* @param {Array<string|null>} names
* @return {Inline<IN>}
*/
export const inline = ($d, names) => new Inline($d, names)
/**
* Convenience {@link Inline} factory that inlines anonymous ("null") child nodes
* (`<p>some<>text</></p>` <-> `<p>sometext</p>`) - i.e. `inline($d, [null])`.
*
* @template {delta.DeltaConf} IN
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
* @return {Inline<IN>}
*/
/* @__NO_SIDE_EFFECTS__ */
export const inlineNullNodes = ($d) => inline($d, [null])