mirror of
https://github.com/yjs/yjs.git
synced 2026-02-24 04:01:14 +01:00
implement base AttributionManager class and use in Y.Text
This commit is contained in:
31
package-lock.json
generated
31
package-lock.json
generated
@@ -13,14 +13,14 @@
|
||||
"y-protocols": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.5",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^3.6.1",
|
||||
"jsdoc": "^3.6.7",
|
||||
"markdownlint-cli": "^0.41.0",
|
||||
"rollup": "^4.37.0",
|
||||
"standard": "^16.0.4",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript": "^5.8.3",
|
||||
"yjs": "."
|
||||
},
|
||||
"engines": {
|
||||
@@ -557,12 +557,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.19.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.15.tgz",
|
||||
"integrity": "sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==",
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
@@ -4547,16 +4548,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.9.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
||||
"version": "5.8.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.2.0"
|
||||
"node": ">=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/uc.micro": {
|
||||
@@ -4587,10 +4589,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||
"dev": true
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/uri-js": {
|
||||
"version": "4.4.1",
|
||||
|
||||
@@ -90,14 +90,14 @@
|
||||
"y-protocols": "^1.0.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.15.5",
|
||||
"@types/node": "^22.14.1",
|
||||
"concurrently": "^3.6.1",
|
||||
"jsdoc": "^3.6.7",
|
||||
"markdownlint-cli": "^0.41.0",
|
||||
"rollup": "^4.37.0",
|
||||
"standard": "^16.0.4",
|
||||
"tui-jsdoc-template": "^1.2.2",
|
||||
"typescript": "^4.9.5",
|
||||
"typescript": "^5.8.3",
|
||||
"yjs": "."
|
||||
},
|
||||
"engines": {
|
||||
|
||||
@@ -41,3 +41,4 @@ export * from './structs/ContentType.js'
|
||||
export * from './structs/Item.js'
|
||||
export * from './structs/Skip.js'
|
||||
export * from './utils/IdMap.js'
|
||||
export * from './utils/AttributionManager.js'
|
||||
|
||||
@@ -26,8 +26,7 @@ import {
|
||||
updateMarkerChanges,
|
||||
ContentType,
|
||||
warnPrematureAccess,
|
||||
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, IdMap, // eslint-disable-line
|
||||
snapshot
|
||||
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as delta from '../utils/Delta.js'
|
||||
@@ -999,85 +998,63 @@ export class YText extends AbstractType {
|
||||
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
|
||||
* attribution `{ isDeleted: true, .. }`.
|
||||
*
|
||||
* @param {IdMap<any>} [idMap]
|
||||
* @param {Doc} [prevYdoc]
|
||||
* @param {AbstractAttributionManager} am
|
||||
* @return {import('../utils/Delta.js').Delta} The Delta representation of this type.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getContent (idMap, prevYdoc) {
|
||||
getContent (am = noAttributionsManager) {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
const prevSnapshot = prevYdoc ? snapshot(prevYdoc) : null
|
||||
const d = delta.create()
|
||||
/**
|
||||
* @type {{ [key: string]: any }}
|
||||
*/
|
||||
const currentAttributes = {}
|
||||
const doc = /** @type {Doc} */ (this.doc)
|
||||
const computeContent = () => {
|
||||
let n = this._start
|
||||
while (n !== null) {
|
||||
switch (n.content.constructor) {
|
||||
case ContentString: {
|
||||
const cur = currentAttributes.get('ychange')
|
||||
if (snapshot !== undefined && !isVisible(n, snapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
|
||||
for (let item = this._start; item !== null; item = item.right) {
|
||||
const cs = am.getContent(item)
|
||||
for (let i = 0; i < cs.length; i++) {
|
||||
const { content, deleted, attrs } = cs[i]
|
||||
/**
|
||||
* @type {{ [key: string]: any }?}
|
||||
*/
|
||||
let attributions = null
|
||||
if (attrs != null) {
|
||||
attributions = {}
|
||||
attrs.forEach(attr => {
|
||||
switch (attr.name) {
|
||||
case '_insertedBy':
|
||||
case '_deletedBy':
|
||||
case '_suggestedBy': {
|
||||
const as = /** @type {any} */ (attributions)
|
||||
const ls = as[attr.name] = as[attr.name] ?? []
|
||||
ls.push(attr.val)
|
||||
break
|
||||
}
|
||||
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
|
||||
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
|
||||
packStr()
|
||||
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
|
||||
default: {
|
||||
if (attr.name[0] !== '_') {
|
||||
/** @type {any} */ (attributions)[attr.name] = attr.val
|
||||
}
|
||||
}
|
||||
} else if (cur !== undefined) {
|
||||
packStr()
|
||||
currentAttributes.delete('ychange')
|
||||
}
|
||||
str += /** @type {ContentString} */ (n.content).str
|
||||
})
|
||||
}
|
||||
switch (content.constructor) {
|
||||
case ContentString: {
|
||||
d.insert(/** @type {ContentString} */ (content).str, {}, attributions)
|
||||
break
|
||||
}
|
||||
case ContentType:
|
||||
case ContentEmbed: {
|
||||
packStr()
|
||||
/**
|
||||
* @type {Object<string,any>}
|
||||
*/
|
||||
const op = {
|
||||
insert: n.content.getContent()[0]
|
||||
}
|
||||
if (currentAttributes.size > 0) {
|
||||
const attrs = /** @type {Object<string,any>} */ ({})
|
||||
op.attributes = attrs
|
||||
currentAttributes.forEach((value, key) => {
|
||||
attrs[key] = value
|
||||
})
|
||||
}
|
||||
ops.push(op)
|
||||
d.insert(/** @type {ContentEmbed | ContentType} */ (content).getContent()[0], {}, attributions)
|
||||
break
|
||||
}
|
||||
case ContentFormat:
|
||||
if (isVisible(n, snapshot)) {
|
||||
packStr()
|
||||
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
|
||||
if (attributions != null) {
|
||||
attributions.formattedBy = (deleted ? attributions.deletedBy : attributions.insertedBy) ?? []
|
||||
delete attributions.deletedBy
|
||||
delete attributions.insertedBy
|
||||
d.useAttribution(attributions)
|
||||
}
|
||||
break
|
||||
}
|
||||
n = n.right
|
||||
}
|
||||
}
|
||||
if (prevSnapshot) {
|
||||
// snapshots are merged again after the transaction, so we need to keep the
|
||||
// transaction alive until we are done
|
||||
transact(doc, transaction => {
|
||||
if (prevSnapshot) {
|
||||
splitSnapshotAffectedStructs(transaction, prevSnapshot)
|
||||
}
|
||||
computeContent()
|
||||
}, 'cleanup')
|
||||
} else {
|
||||
computeContent()
|
||||
}
|
||||
return d.done()
|
||||
}
|
||||
|
||||
|
||||
88
src/utils/AttributionManager.js
Normal file
88
src/utils/AttributionManager.js
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
Item, AbstractContent, IdMap // eslint-disable-line
|
||||
} from '../internals.js'
|
||||
|
||||
import * as error from 'lib0/error'
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
export class AttributedContent {
|
||||
/**
|
||||
* @param {AbstractContent} content
|
||||
* @param {boolean} deleted
|
||||
* @param {Array<import('./IdMap.js').Attribution<T>> | null} attrs
|
||||
*/
|
||||
constructor (content, deleted, attrs) {
|
||||
this.content = content
|
||||
this.deleted = deleted
|
||||
this.attrs = attrs
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for associating Attributions to content / changes
|
||||
*/
|
||||
export class AbstractAttributionManager {
|
||||
/**
|
||||
* @param {Item} _item
|
||||
* @return {Array<AttributedContent<any>>}
|
||||
*/
|
||||
getContent (_item) {
|
||||
error.methodUnimplemented()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for associating Attributions to content / changes
|
||||
*
|
||||
* @implements AbstractAttributionManager
|
||||
*/
|
||||
export class TwosetAttributionManager {
|
||||
/**
|
||||
* @param {IdMap<any>} inserts
|
||||
* @param {IdMap<any>} deletes
|
||||
*/
|
||||
constructor (inserts, deletes) {
|
||||
this.inserts = inserts
|
||||
this.deletes = deletes
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Item} item
|
||||
* @return {Array<AttributedContent<any>>}
|
||||
*/
|
||||
getContent (item) {
|
||||
const deleted = item.deleted
|
||||
const slice = (deleted ? this.deletes : this.inserts).slice(item.id, item.length)
|
||||
let content = slice.length === 1 ? item.content : item.content.copy()
|
||||
let res = slice.map(s => {
|
||||
const c = content
|
||||
if (s.len < c.getLength()) {
|
||||
content = c.splice(s.len)
|
||||
}
|
||||
return new AttributedContent(c, deleted, s.attrs)
|
||||
})
|
||||
if (deleted) {
|
||||
res = res.filter(s => s.attrs != null)
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract class for associating Attributions to content / changes
|
||||
*
|
||||
* @implements AbstractAttributionManager
|
||||
*/
|
||||
export class NoAttributionsManager {
|
||||
/**
|
||||
* @param {Item} item
|
||||
* @return {Array<AttributedContent<any>>}
|
||||
*/
|
||||
getContent (item) {
|
||||
return item.deleted ? [] : [new AttributedContent(item.content, item.deleted, null)]
|
||||
}
|
||||
}
|
||||
|
||||
export const noAttributionsManager = new NoAttributionsManager()
|
||||
@@ -105,6 +105,11 @@ export class AttrRange {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
* @typedef {{ clock: number, len: number, attrs: Array<Attribution<Attrs>>? }} MaybeAttrRange
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template Attrs
|
||||
*/
|
||||
@@ -287,12 +292,18 @@ export class IdMap {
|
||||
}
|
||||
|
||||
/**
|
||||
* Return attributions for a slice of ids.
|
||||
*
|
||||
* @param {ID} id
|
||||
* @param {number} len
|
||||
* @return {Array<AttrRange<Attrs>>?}
|
||||
* @return {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
slice (id, len) {
|
||||
const dr = this.clients.get(id.client)
|
||||
/**
|
||||
* @type {Array<MaybeAttrRange<Attrs>>}
|
||||
*/
|
||||
const res = []
|
||||
if (dr) {
|
||||
/**
|
||||
* @type {Array<AttrRange<Attrs>>}
|
||||
@@ -300,7 +311,7 @@ export class IdMap {
|
||||
const ranges = dr.getIds()
|
||||
let index = findIndexInIdRanges(ranges, id.clock)
|
||||
if (index !== null) {
|
||||
const res = []
|
||||
let prev = null
|
||||
while (index < ranges.length) {
|
||||
let r = ranges[index]
|
||||
if (r.clock < id.clock) {
|
||||
@@ -310,13 +321,26 @@ export class IdMap {
|
||||
r = new AttrRange(r.clock, id.clock + len - r.clock, r.attrs)
|
||||
}
|
||||
if (r.len <= 0) break
|
||||
const prevEnd = prev != null ? prev.clock + prev.len : index
|
||||
if (prevEnd < index) {
|
||||
res.push(/** @type {MaybeAttrRange<Attrs>} */ (new AttrRange(prevEnd, index - prevEnd, /** @type {any} */ (null))))
|
||||
}
|
||||
prev = r
|
||||
res.push(r)
|
||||
index++
|
||||
}
|
||||
return res
|
||||
}
|
||||
}
|
||||
return null
|
||||
if (res.length > 0) {
|
||||
const last = res[res.length - 1]
|
||||
const end = last.clock + last.len
|
||||
if (end < id.clock + len) {
|
||||
res.push(new AttrRange(end, id.clock + len - end, []))
|
||||
}
|
||||
} else {
|
||||
res.push(new AttrRange(id.clock, len, []))
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,7 +95,9 @@ export const testRepeatMergingMultipleIdMaps = tc => {
|
||||
const mergedAttrs = merged.slice(new ID(iclient, iclock), 1)
|
||||
if (mergedAttrs) {
|
||||
mergedAttrs.forEach(a => {
|
||||
composed.add(iclient, a.clock, a.len, a.attrs)
|
||||
if (a.attrs != null) {
|
||||
composed.add(iclient, a.clock, a.len, a.attrs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user