@y/y
Version:
Shared Editing Library
231 lines (205 loc) • 6.92 kB
JavaScript
/**
* Meta API for describing Yjs documents
*/
import * as decoding from 'lib0/decoding'
import * as s from 'lib0/schema'
import {
$idSet,
$idMapAny,
createIdSetFromIdMap,
createDeleteSetFromStructStore,
createInsertSetFromStructStore,
diffIdMap,
mergeIdMaps,
createIdSet,
createIdMap,
mergeIdSets,
diffIdSet,
createIdMapFromIdSet,
writeIdSet,
readIdSet,
writeIdMap,
readIdMap,
intersectMaps,
intersectSets,
filterIdMap
} from './ids.js'
import { IdSetEncoderV2 } from './UpdateEncoder.js'
import { IdSetDecoderV2 } from './UpdateDecoder.js'
/**
* @param {IdSet} inserts
* @param {IdSet} deletes
* @return {ContentIds}
*/
export const createContentIds = (inserts = createIdSet(), deletes = createIdSet()) => ({ inserts, deletes })
/**
* @param {ContentMap} contentMap
*/
export const createContentIdsFromContentMap = contentMap => createContentIds(
createIdSetFromIdMap(contentMap.inserts),
createIdSetFromIdMap(contentMap.deletes)
)
/**
* @param {import('./Doc.js').Doc} ydoc
* @param {boolean} insertsContainDeletes - whether the returned `inserts` set includes content that
* the doc has already deleted. `true` yields a full structural snapshot (every inserted id);
* `false` yields only the doc's currently-visible content (`inserts − deletes`), which is what you
* want when the snapshot defines what should render.
*/
export const createContentIdsFromDoc = (ydoc, insertsContainDeletes) => createContentIds(
createInsertSetFromStructStore(ydoc.store, !insertsContainDeletes),
createDeleteSetFromStructStore(ydoc.store)
)
/**
* @param {import('./Doc.js').Doc} ydocPrev
* @param {import('./Doc.js').Doc} ydocNext
*/
export const createContentIdsFromDocDiff = (ydocPrev, ydocNext) =>
excludeContentIds(createContentIdsFromDoc(ydocPrev, true), createContentIdsFromDoc(ydocNext, true))
/**
* @param {ContentIds} content
* @param {ContentIds} excludeContent
*/
export const excludeContentIds = (content, excludeContent) =>
createContentIds(diffIdSet(content.inserts, excludeContent.inserts), diffIdSet(content.deletes, excludeContent.deletes))
/**
* @param {ContentMap} content
* @param {ContentIds | ContentMap} excludeContent
*/
export const excludeContentMap = (content, excludeContent) => createContentMap(
diffIdMap(content.inserts, excludeContent.inserts),
diffIdMap(content.deletes, excludeContent.deletes)
)
/**
* @param {Array<ContentMap>} contents
*/
export const mergeContentMaps = contents => createContentMap(
mergeIdMaps(contents.map(c => c.inserts)),
mergeIdMaps(contents.map(c => c.deletes))
)
/**
* @param {Array<ContentIds>} contents
*/
export const mergeContentIds = contents => createContentIds(
mergeIdSets(contents.map(c => c.inserts)),
mergeIdSets(contents.map(c => c.deletes))
)
/**
* @param {IdMap<any>} [inserts]
* @param {IdMap<any>} [deletes]
* @return {ContentMap}
*/
export const createContentMap = (inserts = createIdMap(), deletes = createIdMap()) => ({ inserts, deletes })
/**
* @param {ContentIds} contentIds
* @param {Array<ContentAttribute<any>>} insertAttrs
* @param {Array<ContentAttribute<any>>} [deleteAttrs]
*/
export const createContentMapFromContentIds = (contentIds, insertAttrs, deleteAttrs = insertAttrs) => createContentMap(
createIdMapFromIdSet(contentIds.inserts, insertAttrs),
createIdMapFromIdSet(contentIds.deletes, deleteAttrs)
)
/**
* @param {import('./UpdateEncoder.js').IdSetEncoder} encoder
* @param {ContentIds} contentIds
*/
export const writeContentIds = (encoder, contentIds) => {
writeIdSet(encoder, contentIds.inserts)
writeIdSet(encoder, contentIds.deletes)
}
/**
* @param {ContentIds} contentIds
*/
export const encodeContentIds = contentIds => {
const encoder = new IdSetEncoderV2()
writeContentIds(encoder, contentIds)
return encoder.toUint8Array()
}
/**
* @todo this encoding needs to be heavily optimized for production
*
* @param {import('./UpdateDecoder.js').IdSetDecoder} decoder
* @return {ContentIds}
*/
export const readContentIds = decoder => createContentIds(
readIdSet(decoder),
readIdSet(decoder)
)
/**
* @param {Uint8Array<any>} buf
*/
export const decodeContentIds = buf => readContentIds(new IdSetDecoderV2(decoding.createDecoder(buf)))
/**
* @todo this encoding needs to be heavily optimized for production
*
* @param {import('./UpdateEncoder.js').IdSetEncoder} encoder
* @param {ContentMap} contentMap
*/
export const writeContentMap = (encoder, contentMap) => {
writeIdMap(encoder, contentMap.inserts)
writeIdMap(encoder, contentMap.deletes)
}
/**
* @todo this encoding needs to be heavily optimized for production
*
* @param {import('./UpdateDecoder.js').IdSetDecoder} decoder
* @return {ContentMap} contentMap
*/
export const readContentMap = (decoder) => createContentMap(
readIdMap(decoder),
readIdMap(decoder)
)
/**
* @param {ContentMap} contentMap
*/
export const encodeContentMap = contentMap => {
const encoder = new IdSetEncoderV2()
writeContentMap(encoder, contentMap)
return encoder.toUint8Array()
}
/**
* @param {ContentMap} mapA
* @param {ContentMap|ContentIds} mapB
*/
export const intersectContentMap = (mapA, mapB) => createContentMap(
intersectMaps(mapA.inserts, mapB.inserts),
intersectMaps(mapA.deletes, mapB.deletes)
)
/**
* @param {ContentIds} setA
* @param {ContentIds|ContentMap} setB
*/
export const intersectContentIds = (setA, setB) => createContentIds(
intersectSets(setA.inserts, setB.inserts),
intersectSets(setA.deletes, setB.deletes)
)
/**
* @param {Uint8Array<any>} buf
*/
export const decodeContentMap = buf => readContentMap(new IdSetDecoderV2(decoding.createDecoder(buf)))
/**
* @todo filter by array of content instead
* @param {ContentMap} contentMap
* @param {(c:Array<ContentAttribute<any>>)=>boolean} insertPredicate
* @param {(c:Array<ContentAttribute<any>>)=>boolean} deletePredicate
*/
export const filterContentMap = (contentMap, insertPredicate, deletePredicate) => createContentMap(filterIdMap(contentMap.inserts, insertPredicate), filterIdMap(contentMap.deletes, deletePredicate))
/**
* Schema of {@link ContentIds} - the shape produced by {@link createContentIds}.
*
* Structural: a `ContentIds` is a plain object literal, so there is no prototype to tag. The
* *nominal* {@link $idSet} checks on the two fields are what discriminate it from a
* {@link ContentMap} in both directions.
*
* @type {s.Schema<ContentIds>}
*/
export const $contentIds = s.$object({ inserts: $idSet, deletes: $idSet })
/**
* Schema of {@link ContentMap} - the shape produced by {@link createContentMap}.
*
* See {@link $contentIds}. Use {@link $idMap} on the individual fields to additionally constrain
* the mapped-value type; `ContentMap` itself hardcodes `IdMap<any>`.
*
* @type {s.Schema<ContentMap>}
*/
export const $contentMap = s.$object({ inserts: $idMapAny, deletes: $idMapAny })