lib0
Version:
isomorphic utility functions
125 lines (115 loc) • 4 kB
JavaScript
import * as delta from '../delta.js'
import * as s from '../../schema.js'
import { Transformer, Template, createTransformResult, attrsShapeOf } from './core.js'
/**
* @template {string} AttrName
* @template {delta.DeltaConf} IN
* @typedef {import('./core.js').ResolveOut<{ name: 'lib0:value', attrs: { value: IN extends { attrs: { [K in AttrName]: infer V } } ? V : never }}>} ApplyAttr
*/
/**
* @param {delta.DeltaBuilderAny} outDelta
* @param {string|number} from
* @param {string|number} to
* @param {delta.DeltaAny} inDelta
*/
const attrTransformHelper = (outDelta, from, to, inDelta) => {
const attrOp = inDelta.attrs[from]
if (attrOp != null) {
// a clone retargeted to `to` (the clone skips the cached fingerprint, which covers the key)
const c = attrOp.clone(to)
// `attrs` is a mapped type over fixed conf keys, so the dynamic-key write needs the cast
const oattrs = /** @type {any} */ (outDelta.attrs)
oattrs[to] = c
}
// Move a mark on the projected attribute (`from` -> `to`); drop root marks on any other attribute or
// content (only this one attribute exists on the other side). A mark *inside* the attribute value
// rides on the cloned attr op above, and a mark *delete* (id-keyed) rides verbatim via mergeRootMarks.
delta.mergeRootMarks(outDelta, inDelta, k => k === from ? to : null)
// flag conservatively: the direct attr assignment above bypasses the builder, so a mark inside the
// cloned attribute value would otherwise not set the flag (marksToPositions self-corrects a false +ve)
outDelta.maybeHasMarks ||= inDelta.maybeHasMarks
return outDelta
}
/**
* Projects a single node attribute into a `lib0:value` node's `value` attribute (and back).
*
* @template {string} AttrName
* @template {delta.DeltaConf} [IN=any]
* @extends {Template<IN, ApplyAttr<AttrName, IN>>}
*/
export class Attr extends Template {
/**
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
* @param {AttrName} attrName
*/
constructor ($d, attrName) {
const m = attrsShapeOf($d)
const $val = (m && m[attrName]) || s.$any
super($d, /** @type {any} */ (delta.$delta('lib0:value', { attrs: { value: $val } })))
this.attrName = attrName
}
get name () { return 'lib0:attr:' + this.attrName }
/**
* @return {Transformer<IN, ApplyAttr<AttrName, IN>>}
*/
init () {
return /** @type {any} */ (new AttrTransformer(this.$in, this.$out, this.attrName))
}
}
/**
* @template {delta.DeltaConf} A
* @template {delta.DeltaConf} B
* @extends {Transformer<A,B>}
*/
export class AttrTransformer extends Transformer {
/**
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $in
* @param {import('../../schema.js').Schema<delta.Delta<any>>} $out
* @param {string} attrName
*/
constructor ($in, $out, attrName) {
super($in, $out)
this.attrName = /** @type {keyof delta.DeltaConfGetAttrs<A> & (string|number)} */ (attrName)
}
/**
* @param {delta.DeltaBuilder<A>} d
* @return {import('./core.js').TransformResultAny}
*/
applyA (d) {
return createTransformResult(
null,
attrTransformHelper(
delta.create('lib0:value'),
this.attrName,
'value',
d
)
)
}
/**
* @param {delta.DeltaBuilder<B>} d
* @return {import('./core.js').TransformResultAny}
*/
applyB (d) {
return createTransformResult(
attrTransformHelper(
delta.create(),
'value',
this.attrName,
d
),
null
)
}
}
/**
* Project a single node attribute `attrName` into a `lib0:value` carrier (and back). Returns a
* reusable {@link Attr} template (a `project` hole, or `.init()` for a standalone transformer).
*
* @template {string} AttrName
* @template {delta.DeltaConf} IN
* @param {import('../../schema.js').Schema<delta.Delta<IN>>} $d
* @param {AttrName} attrName
* @return {Attr<AttrName, IN>}
*/
export const attr = ($d, attrName) => new Attr($d, attrName)