From 120ba2f56be87640fe8eff09918eef55a609f56f Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 10:13:32 +0200 Subject: [PATCH 1/9] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20add=20block=20tag=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/overlay/types.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/overlay/types.ts diff --git a/src/json-crdt-extensions/peritext/overlay/types.ts b/src/json-crdt-extensions/peritext/overlay/types.ts new file mode 100644 index 0000000000..09bccc97bd --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/types.ts @@ -0,0 +1,13 @@ +export type BlockTag = [ + /** + * Developer specified type of the block. For example, 'title', 'paragraph', + * 'image', etc. For performance reasons, it is better to use a number to + * represent the type. + */ + type: number | number[], + + /** + * Any custom attributes that the developer wants to add to the block. + */ + attr?: undefined | unknown, +]; From 7c31a6fadb84c391a7ede68dd094bb8dd63bd0bd Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 10:18:46 +0200 Subject: [PATCH 2/9] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20implement=20overlay=20"ref"=20concept?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/refs.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/overlay/refs.ts diff --git a/src/json-crdt-extensions/peritext/overlay/refs.ts b/src/json-crdt-extensions/peritext/overlay/refs.ts new file mode 100644 index 0000000000..d8a5eb8217 --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/refs.ts @@ -0,0 +1,21 @@ +import type {SplitSlice} from '../slice/SplitSlice'; +import type {Slice} from '../slice/types'; + +/** + * On overlay "ref" is a reference from the {@link Overlay} to a {@link Slice}. + * In case of a *marker* slice, the reference is to the slice itself. In case of + * a regular annotation slice, two references are needed: one to the start slice + * and one to the end slice. + */ +export type OverlayRef = + | SplitSlice // Ref to a *marker* slice + | OverlayRefSliceStart // Ref to the start of an annotation slice + | OverlayRefSliceEnd; // Ref to the end of an annotation slice + +export class OverlayRefSliceStart { + constructor(public readonly slice: Slice) {} +} + +export class OverlayRefSliceEnd { + constructor(public readonly slice: Slice) {} +} From ca037e6a0d3a3a94511755c06c4d8a350a339649 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 10:59:31 +0200 Subject: [PATCH 3/9] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20add=20OverlayPoint=20implementation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/OverlayPoint.ts | 147 ++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts new file mode 100644 index 0000000000..5d5962a514 --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -0,0 +1,147 @@ +import {Point} from '../rga/Point'; +import {compare} from '../../../json-crdt-patch/clock'; +import {OverlayRef, OverlayRefSliceEnd, OverlayRefSliceStart} from './refs'; +import {printTree} from 'sonic-forest/lib/print/printTree'; +import type {HeadlessNode} from 'sonic-forest/lib/types'; +import type {Printable} from '../../../util/print/types'; +import type {Slice} from '../slice/types'; + +export class OverlayPoint extends Point implements Printable, HeadlessNode { + /** + * Sorted list of references to rich-text constructs. + */ + public readonly refs: OverlayRef[] = []; + + /** + * Sorted list of layers, contain the interval from this point to the next one. + */ + public readonly layers: Slice[] = []; + + /** + * Collapsed slices. + * + * @todo Rename to `markers`? + */ + public readonly points: Slice[] = []; + + /** Hash of text contents until the next {@link OverlayPoint}. */ + public hash: number = 0; + + public removeSlice(slice: Slice): void { + const refs = this.refs; + const length = refs.length; + for (let i = 0; i < length; i++) { + const ref = refs[i]; + if ( + ref === slice || + (ref instanceof OverlayRefSliceStart && ref.slice === slice) || + (ref instanceof OverlayRefSliceEnd && ref.slice === slice) + ) { + refs.splice(i, 1); + break; + } + } + this.removeLayer(slice); + this.removePoint(slice); + } + + /** + * Inserts a slice to the list of layers which contains the area from this + * point to the next one. + * @param slice Slice to add to the layer list. + */ + public addLayer(slice: Slice): void { + const layers = this.layers; + const length = layers.length; + if (!length) { + layers.push(slice); + return; + } + // We attempt to insert from the end of the list, as it is the most likely. + const lastSlice = layers[length - 1]; + const sliceId = slice.id; + if (compare(lastSlice.id, sliceId) < 0) { + layers.push(slice); + return; + } + for (let i = length - 2; i >= 0; i--) { + const currSlice = layers[i]; + if (compare(currSlice.id, sliceId) < 0) { + layers.splice(i + 1, 0, slice); + return; + } + } + layers.unshift(slice); + } + + public removeLayer(slice: Slice): void { + const layers = this.layers; + const length = layers.length; + for (let i = 0; i < length; i++) { + if (layers[i] === slice) { + layers.splice(i, 1); + return; + } + } + } + + public addPoint(slice: Slice): void { + const points = this.points; + const length = points.length; + if (!length) { + points.push(slice); + return; + } + // We attempt to insert from the end of the list, as it is the most likely. + const lastSlice = points[length - 1]; + const sliceId = slice.id; + if (compare(lastSlice.id, sliceId) < 0) { + points.push(slice); + return; + } + for (let i = length - 2; i >= 0; i--) { + const currSlice = points[i]; + if (compare(currSlice.id, sliceId) < 0) { + points.splice(i + 1, 0, slice); + return; + } + } + points.unshift(slice); + } + + public removePoint(slice: Slice): void { + const points = this.points; + const length = points.length; + for (let i = 0; i < length; i++) { + if (points[i] === slice) { + points.splice(i, 1); + return; + } + } + } + + // ---------------------------------------------------------------- Printable + + public toStringName(tab: string, lite?: boolean): string { + return super.toString(tab, lite); + } + + public toString(tab: string = '', lite?: boolean): string { + const refs = lite ? '' : `, refs = ${this.refs.length}`; + const header = this.toStringName(tab, lite) + refs; + if (lite) return header; + return ( + header + + printTree( + tab, + this.layers.map((slice) => (tab) => slice.toString(tab)), + ) + ); + } + + // ------------------------------------------------------------- HeadlessNode + + public p: OverlayPoint | undefined = undefined; + public l: OverlayPoint | undefined = undefined; + public r: OverlayPoint | undefined = undefined; +} From fb6191eebecc04cde352142682d62a4451013578 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 10:59:57 +0200 Subject: [PATCH 4/9] =?UTF-8?q?ci:=20=F0=9F=8E=A1=20do=20not=20install=20t?= =?UTF-8?q?ypedoc=20by=20default?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- yarn.lock | 83 +++------------------------------------------------- 2 files changed, 5 insertions(+), 81 deletions(-) diff --git a/package.json b/package.json index 2db3f0f6e1..1cc9560c38 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,7 @@ "demo:json-patch": "ts-node src/json-patch/__demos__/json-patch.ts", "demo:json-pointer": "ts-node src/json-pointer/__demos__/json-pointer.ts", "coverage": "yarn test --collectCoverage", - "typedoc": "typedoc", + "typedoc": "npx typedoc@0.25.13", "build:pages": "rimraf gh-pages && mkdir -p gh-pages && cp -r typedocs/* gh-pages && cp -r coverage gh-pages/coverage", "deploy:pages": "gh-pages -d gh-pages", "publish-coverage-and-typedocs": "yarn typedoc && yarn coverage && yarn build:pages && yarn deploy:pages", @@ -164,7 +164,6 @@ "tslib": "^2.6.2", "tslint": "^6.1.3", "tslint-config-common": "^1.6.2", - "typedoc": "^0.25.13", "typescript": "^5.4.4", "webpack": "^5.91.0", "webpack-cli": "^5.1.4", diff --git a/yarn.lock b/yarn.lock index 39dff418a6..77fdfd2825 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1278,11 +1278,6 @@ ansi-regex@^6.0.1: resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== -ansi-sequence-parser@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz#e0aa1cdcbc8f8bb0b5bca625aac41f5f056973cf" - integrity sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg== - ansi-styles@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" @@ -3471,11 +3466,6 @@ json5@^2.2.3: resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== -jsonc-parser@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.1.tgz#031904571ccf929d7670ee8c547545081cb37f1a" - integrity sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA== - jsonfile@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb" @@ -3602,11 +3592,6 @@ lru-cache@^6.0.0: resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.0.3.tgz#b40014d7d2d16d94130b87297a04a1f24874ae7c" integrity sha512-B7gr+F6MkqB3uzINHXNctGieGsRTMwIBgxkp0yq/5BwcuDzD4A8wQpHQW6vDAm1uKSLQghmRdD9sKqf2vJ1cEg== -lunr@^2.3.9: - version "2.3.9" - resolved "https://registry.yarnpkg.com/lunr/-/lunr-2.3.9.tgz#18b123142832337dd6e964df1a5a7707b25d35e1" - integrity sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow== - make-dir@^3.0.2: version "3.1.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" @@ -3633,11 +3618,6 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -marked@^4.3.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3" - integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A== - mdn-data@2.0.14: version "2.0.14" resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" @@ -3719,7 +3699,7 @@ minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimatch@^9.0.1, minimatch@^9.0.3: +minimatch@^9.0.1: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== @@ -4552,16 +4532,6 @@ shell-quote@^1.8.1: resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== -shiki@^0.14.7: - version "0.14.7" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-0.14.7.tgz#c3c9e1853e9737845f1d2ef81b31bcfb07056d4e" - integrity sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg== - dependencies: - ansi-sequence-parser "^1.1.0" - jsonc-parser "^3.2.0" - vscode-oniguruma "^1.7.0" - vscode-textmate "^8.0.0" - side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -4735,16 +4705,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -4776,14 +4737,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -5066,16 +5020,6 @@ type-is@~1.6.18: media-typer "0.3.0" mime-types "~2.1.24" -typedoc@^0.25.13: - version "0.25.13" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.25.13.tgz#9a98819e3b2d155a6d78589b46fa4c03768f0922" - integrity sha512-pQqiwiJ+Z4pigfOnnysObszLiU3mVLWAExSPf+Mu06G/qsc3wzbuM56SZQvONhHLncLUhYzOVkjFFpFfL5AzhQ== - dependencies: - lunr "^2.3.9" - marked "^4.3.0" - minimatch "^9.0.3" - shiki "^0.14.7" - typescript@^5.4.4: version "5.4.4" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.4.tgz#eb2471e7b0a5f1377523700a21669dce30c2d952" @@ -5170,16 +5114,6 @@ vary@~1.1.2: resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== -vscode-oniguruma@^1.7.0: - version "1.7.0" - resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b" - integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA== - -vscode-textmate@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/vscode-textmate/-/vscode-textmate-8.0.0.tgz#2c7a3b1163ef0441097e0b5d6389cd5504b59e5d" - integrity sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg== - walker@^1.0.8: version "1.0.8" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" @@ -5339,16 +5273,7 @@ wildcard@^2.0.0: resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From 70748ac60fc15b8f35e5ed5361011c873d46753b Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 14:25:22 +0200 Subject: [PATCH 5/9] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20improve=20overlay=20point=20layer=20insertion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/Peritext.ts | 1 + .../peritext/overlay/OverlayPoint.ts | 42 ++++-- .../overlay/__tests__/OverlayPoint.spec.ts | 121 ++++++++++++++++++ 3 files changed, 155 insertions(+), 9 deletions(-) create mode 100644 src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts diff --git a/src/json-crdt-extensions/peritext/Peritext.ts b/src/json-crdt-extensions/peritext/Peritext.ts index 2005c7e81b..d9fde4c150 100644 --- a/src/json-crdt-extensions/peritext/Peritext.ts +++ b/src/json-crdt-extensions/peritext/Peritext.ts @@ -49,6 +49,7 @@ export class Peritext implements Printable { * * @param pos Position of the character in the text. * @param anchor Whether the point should attach before or after a character. + * Defaults to "before". * @returns The point. */ public pointAt(pos: number, anchor: Anchor = Anchor.Before): Point { diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index 5d5962a514..a5b88165df 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -6,19 +6,27 @@ import type {HeadlessNode} from 'sonic-forest/lib/types'; import type {Printable} from '../../../util/print/types'; import type {Slice} from '../slice/types'; +/** + * A {@link Point} which is indexed in the {@link Overlay} tree. Represents + * sparse locations in the string of the places where annotation slices start, + * end, or are broken down by other intersecting slices. + */ export class OverlayPoint extends Point implements Printable, HeadlessNode { /** - * Sorted list of references to rich-text constructs. + * Sorted list of all references to rich-text constructs. */ public readonly refs: OverlayRef[] = []; /** - * Sorted list of layers, contain the interval from this point to the next one. + * Sorted list of layers, contains the interval from this point to the next + * one. A *layer* is a part of a slice from the current point to the next one. + * This interval can contain many layers, as the slices can be overlap. */ public readonly layers: Slice[] = []; /** - * Collapsed slices. + * Collapsed slices - markers/block splits, which represent a single point in + * the text, even if the start and end of the slice are different. * * @todo Rename to `markers`? */ @@ -45,9 +53,14 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { this.removePoint(slice); } + // ------------------------------------------------------------------- layers + /** * Inserts a slice to the list of layers which contains the area from this - * point to the next one. + * point until the next one. The operation is idempotent, so inserting the + * same slice twice will not change the state of the point. The layers are + * sorted by the slice ID. + * * @param slice Slice to add to the layer list. */ public addLayer(slice: Slice): void { @@ -57,23 +70,32 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { layers.push(slice); return; } - // We attempt to insert from the end of the list, as it is the most likely. + // We attempt to insert from the end of the list, as it is the most likely + // scenario. And `.push()` is more efficient than `.unshift()`. const lastSlice = layers[length - 1]; const sliceId = slice.id; - if (compare(lastSlice.id, sliceId) < 0) { + const cmp = compare(lastSlice.id, sliceId); + if (cmp < 0) { layers.push(slice); return; - } + } else if (!cmp) return; for (let i = length - 2; i >= 0; i--) { const currSlice = layers[i]; - if (compare(currSlice.id, sliceId) < 0) { + const cmp = compare(currSlice.id, sliceId); + if (cmp < 0) { layers.splice(i + 1, 0, slice); return; - } + } else if (!cmp) return; } layers.unshift(slice); } + /** + * Removes a slice from the list of layers, which start from this overlay + * point. + * + * @param slice Slice to remove from the layer list. + */ public removeLayer(slice: Slice): void { const layers = this.layers; const length = layers.length; @@ -85,6 +107,8 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { } } + // ------------------------------------------------------------------ markers + public addPoint(slice: Slice): void { const points = this.points; const length = points.length; diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts new file mode 100644 index 0000000000..5bcf3db538 --- /dev/null +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts @@ -0,0 +1,121 @@ +import {Point} from "../../rga/Point"; +import {setup} from "../../slice/__tests__/setup"; +import {OverlayPoint} from "../OverlayPoint"; + +const setupOverlayPoint = () => { + const deps = setup(); + const getPoint = (point: Point) => { + return new OverlayPoint(deps.peritext.str, point.id, point.anchor); + }; + return { + ...deps, + getPoint, + }; +}; + +describe('layers', () => { + test('can add a layer', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); + const point = getPoint(slice.start); + expect(point.layers.length).toBe(0); + point.addLayer(slice); + expect(point.layers.length).toBe(1); + expect(point.layers[0]).toBe(slice); + }); + + test('inserting same slice twice is a no-op', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); + const point = getPoint(slice.start); + expect(point.layers.length).toBe(0); + point.addLayer(slice); + point.addLayer(slice); + point.addLayer(slice); + expect(point.layers.length).toBe(1); + expect(point.layers[0]).toBe(slice); + }); + + test('can add two layers with the same start position', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); + const point = getPoint(slice1.start); + expect(point.layers.length).toBe(0); + point.addLayer(slice1); + expect(point.layers.length).toBe(1); + point.addLayer(slice2); + point.addLayer(slice2); + expect(point.layers.length).toBe(2); + expect(point.layers[0]).toBe(slice1); + expect(point.layers[1]).toBe(slice2); + }); + + test('orders slices by their ID', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); + const point = getPoint(slice1.start); + point.addLayer(slice2); + point.addLayer(slice1); + expect(point.layers[0]).toBe(slice1); + expect(point.layers[1]).toBe(slice2); + }); + + test('can add tree layers and sort them correctly', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), ''); + const point = getPoint(slice1.start); + point.addLayer(slice3); + point.addLayer(slice3); + point.addLayer(slice2); + point.addLayer(slice3); + point.addLayer(slice1); + point.addLayer(slice3); + point.addLayer(slice3); + expect(point.layers.length).toBe(3); + expect(point.layers[0]).toBe(slice1); + expect(point.layers[1]).toBe(slice2); + expect(point.layers[2]).toBe(slice3); + }); + + test('can add tree layers by appending them', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), ''); + const point = getPoint(slice1.start); + point.addLayer(slice1); + point.addLayer(slice2); + point.addLayer(slice3); + expect(point.layers[0]).toBe(slice1); + expect(point.layers[1]).toBe(slice2); + expect(point.layers[2]).toBe(slice3); + }); + + test('can remove layers', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice1 = peritext.slices.insOverwrite(peritext.rangeAt(5, 5), ''); + const slice2 = peritext.slices.insOverwrite(peritext.rangeAt(5, 3), ''); + const slice3 = peritext.slices.insOverwrite(peritext.rangeAt(2, 10), ''); + const point = getPoint(slice1.start); + point.addLayer(slice2); + point.addLayer(slice1); + point.addLayer(slice1); + point.addLayer(slice1); + point.addLayer(slice3); + expect(point.layers[0]).toBe(slice1); + expect(point.layers[1]).toBe(slice2); + expect(point.layers[2]).toBe(slice3); + point.removeLayer(slice2); + expect(point.layers[0]).toBe(slice1); + expect(point.layers[1]).toBe(slice3); + point.removeLayer(slice1); + expect(point.layers[0]).toBe(slice3); + point.removeLayer(slice1); + point.removeLayer(slice3); + expect(point.layers.length).toBe(0); + }); +}); From 323a98e8d327d81c65e7a56682fb70e55fdbd4b7 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 14:25:42 +0200 Subject: [PATCH 6/9] =?UTF-8?q?style(json-crdt-extensions):=20=F0=9F=92=84?= =?UTF-8?q?=20run=20Prettier?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts | 2 +- .../peritext/overlay/__tests__/OverlayPoint.spec.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index a5b88165df..ff712c3065 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -27,7 +27,7 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { /** * Collapsed slices - markers/block splits, which represent a single point in * the text, even if the start and end of the slice are different. - * + * * @todo Rename to `markers`? */ public readonly points: Slice[] = []; diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts index 5bcf3db538..cac62190f4 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts @@ -1,6 +1,6 @@ -import {Point} from "../../rga/Point"; -import {setup} from "../../slice/__tests__/setup"; -import {OverlayPoint} from "../OverlayPoint"; +import {Point} from '../../rga/Point'; +import {setup} from '../../slice/__tests__/setup'; +import {OverlayPoint} from '../OverlayPoint'; const setupOverlayPoint = () => { const deps = setup(); From 7aea094eaefc5a8ce6a0691e4a189b3a96718ef6 Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 15:27:28 +0200 Subject: [PATCH 7/9] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20improve=20OverlayPoint=20marker=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/OverlayPoint.ts | 50 +++++--- .../overlay/__tests__/OverlayPoint.spec.ts | 113 ++++++++++++++++++ 2 files changed, 147 insertions(+), 16 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index ff712c3065..4c8a93c411 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -25,14 +25,15 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { public readonly layers: Slice[] = []; /** - * Collapsed slices - markers/block splits, which represent a single point in - * the text, even if the start and end of the slice are different. - * - * @todo Rename to `markers`? + * Collapsed slices - markers (block splits), which represent a single point + * in the text, even if the start and end of the slice are different. */ - public readonly points: Slice[] = []; + public readonly markers: Slice[] = []; - /** Hash of text contents until the next {@link OverlayPoint}. */ + /** + * Hash of text contents until the next {@link OverlayPoint}. This field is + * modified by the {@link Overlay} tree. + */ public hash: number = 0; public removeSlice(slice: Slice): void { @@ -50,7 +51,7 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { } } this.removeLayer(slice); - this.removePoint(slice); + this.removeMarker(slice); } // ------------------------------------------------------------------- layers @@ -109,32 +110,49 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { // ------------------------------------------------------------------ markers - public addPoint(slice: Slice): void { - const points = this.points; + /** + * Inserts a slice to the list of markers which represent a single point in + * the text, even if the start and end of the slice are different. The + * operation is idempotent, so inserting the same slice twice will not change + * the state of the point. The markers are sorted by the slice ID. + * + * @param slice Slice to add to the marker list. + */ + public addMarker(slice: Slice): void { + const points = this.markers; const length = points.length; if (!length) { points.push(slice); return; } - // We attempt to insert from the end of the list, as it is the most likely. + // We attempt to insert from the end of the list, as it is the most likely + // scenario. And `.push()` is more efficient than `.unshift()`. const lastSlice = points[length - 1]; const sliceId = slice.id; - if (compare(lastSlice.id, sliceId) < 0) { + const cmp = compare(lastSlice.id, sliceId); + if (cmp < 0) { points.push(slice); return; - } + } else if (!cmp) return; for (let i = length - 2; i >= 0; i--) { const currSlice = points[i]; - if (compare(currSlice.id, sliceId) < 0) { + const cmp = compare(currSlice.id, sliceId); + if (cmp < 0) { points.splice(i + 1, 0, slice); return; - } + } else if (!cmp) return; } points.unshift(slice); } - public removePoint(slice: Slice): void { - const points = this.points; + /** + * Removes a slice from the list of markers, which represent a single point in + * the text, even if the start and end of the slice are different. + * + * @param slice Slice to remove from the marker list. + */ + public removeMarker(slice: Slice): void { + const points = this.markers; const length = points.length; for (let i = 0; i < length; i++) { if (points[i] === slice) { diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts index cac62190f4..1242087848 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts @@ -119,3 +119,116 @@ describe('layers', () => { expect(point.layers.length).toBe(0); }); }); + +describe('markers', () => { + test('can add a marker', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const point = getPoint(marker.start); + expect(point.markers.length).toBe(0); + point.addMarker(marker); + expect(point.markers.length).toBe(1); + expect(point.markers[0]).toBe(marker); + }); + + test('inserting same marker twice is a no-op', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const point = getPoint(marker.start); + expect(point.markers.length).toBe(0); + point.addMarker(marker); + point.addMarker(marker); + point.addMarker(marker); + point.addMarker(marker); + expect(point.markers.length).toBe(1); + expect(point.markers[0]).toBe(marker); + }); + + test('can add two markers with the same start position', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker1 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const marker2 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const point = getPoint(marker1.start); + expect(point.markers.length).toBe(0); + point.addMarker(marker1); + expect(point.markers.length).toBe(1); + point.addMarker(marker2); + point.addMarker(marker2); + expect(point.markers.length).toBe(2); + expect(point.markers[0]).toBe(marker1); + expect(point.markers[1]).toBe(marker2); + }); + + test('orders markers by their ID', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker1 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const marker2 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const point = getPoint(marker1.start); + point.addMarker(marker2); + point.addMarker(marker1); + point.addMarker(marker2); + point.addMarker(marker1); + point.addMarker(marker2); + point.addMarker(marker1); + expect(point.markers[0]).toBe(marker1); + expect(point.markers[1]).toBe(marker2); + }); + + test('can add tree markers and sort them correctly', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker1 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const marker2 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const marker3 = peritext.slices.insSplit(peritext.rangeAt(5, 0), '

'); + const point = getPoint(marker1.start); + point.addMarker(marker3); + point.addMarker(marker3); + point.addMarker(marker2); + point.addMarker(marker2); + point.addMarker(marker3); + point.addMarker(marker1); + point.addMarker(marker3); + point.addMarker(marker3); + expect(point.markers.length).toBe(3); + expect(point.markers[0]).toBe(marker1); + expect(point.markers[1]).toBe(marker2); + expect(point.markers[2]).toBe(marker3); + }); + + test('can add tree markers by appending them', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const point = getPoint(marker2.start); + point.addMarker(marker1); + point.addMarker(marker2); + point.addMarker(marker3); + expect(point.markers[0]).toBe(marker1); + expect(point.markers[1]).toBe(marker2); + expect(point.markers[2]).toBe(marker3); + }); + + test('can remove markers', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const point = getPoint(marker1.start); + point.addMarker(marker2); + point.addMarker(marker1); + point.addMarker(marker1); + point.addMarker(marker1); + point.addMarker(marker3); + expect(point.markers[0]).toBe(marker1); + expect(point.markers[1]).toBe(marker2); + expect(point.markers[2]).toBe(marker3); + point.removeMarker(marker2); + expect(point.markers[0]).toBe(marker1); + expect(point.markers[1]).toBe(marker3); + point.removeMarker(marker1); + expect(point.markers[0]).toBe(marker3); + point.removeMarker(marker1); + point.removeMarker(marker3); + expect(point.markers.length).toBe(0); + }); +}); From 8a237760a999987c5e00baaa40ccc1110faeb5cf Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 15:59:48 +0200 Subject: [PATCH 8/9] =?UTF-8?q?feat(json-crdt-extensions):=20=F0=9F=8E=B8?= =?UTF-8?q?=20improve=20OverlayPoint=20ref=20operations?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/OverlayPoint.ts | 114 +++++++++++------- .../overlay/__tests__/OverlayPoint.spec.ts | 88 +++++++++++++- 2 files changed, 150 insertions(+), 52 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index 4c8a93c411..c9f6e6bf8c 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -2,6 +2,7 @@ import {Point} from '../rga/Point'; import {compare} from '../../../json-crdt-patch/clock'; import {OverlayRef, OverlayRefSliceEnd, OverlayRefSliceStart} from './refs'; import {printTree} from 'sonic-forest/lib/print/printTree'; +import type {SplitSlice} from '../slice/SplitSlice'; import type {HeadlessNode} from 'sonic-forest/lib/types'; import type {Printable} from '../../../util/print/types'; import type {Slice} from '../slice/types'; @@ -13,9 +14,12 @@ import type {Slice} from '../slice/types'; */ export class OverlayPoint extends Point implements Printable, HeadlessNode { /** - * Sorted list of all references to rich-text constructs. + * Hash of text contents until the next {@link OverlayPoint}. This field is + * modified by the {@link Overlay} tree. */ - public readonly refs: OverlayRef[] = []; + public hash: number = 0; + + // ------------------------------------------------------------------- layers /** * Sorted list of layers, contains the interval from this point to the next @@ -24,38 +28,6 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { */ public readonly layers: Slice[] = []; - /** - * Collapsed slices - markers (block splits), which represent a single point - * in the text, even if the start and end of the slice are different. - */ - public readonly markers: Slice[] = []; - - /** - * Hash of text contents until the next {@link OverlayPoint}. This field is - * modified by the {@link Overlay} tree. - */ - public hash: number = 0; - - public removeSlice(slice: Slice): void { - const refs = this.refs; - const length = refs.length; - for (let i = 0; i < length; i++) { - const ref = refs[i]; - if ( - ref === slice || - (ref instanceof OverlayRefSliceStart && ref.slice === slice) || - (ref instanceof OverlayRefSliceEnd && ref.slice === slice) - ) { - refs.splice(i, 1); - break; - } - } - this.removeLayer(slice); - this.removeMarker(slice); - } - - // ------------------------------------------------------------------- layers - /** * Inserts a slice to the list of layers which contains the area from this * point until the next one. The operation is idempotent, so inserting the @@ -110,6 +82,13 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { // ------------------------------------------------------------------ markers + /** + * Collapsed slices - markers (block splits), which represent a single point + * in the text, even if the start and end of the slice are different. + * @deprecated This field might happen to be not necessary. + */ + public readonly markers: Slice[] = []; + /** * Inserts a slice to the list of markers which represent a single point in * the text, even if the start and end of the slice are different. The @@ -117,32 +96,34 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { * the state of the point. The markers are sorted by the slice ID. * * @param slice Slice to add to the marker list. + * @deprecated This method might happen to be not necessary. */ public addMarker(slice: Slice): void { - const points = this.markers; - const length = points.length; + /** @deprecated */ + const markers = this.markers; + const length = markers.length; if (!length) { - points.push(slice); + markers.push(slice); return; } // We attempt to insert from the end of the list, as it is the most likely // scenario. And `.push()` is more efficient than `.unshift()`. - const lastSlice = points[length - 1]; + const lastSlice = markers[length - 1]; const sliceId = slice.id; const cmp = compare(lastSlice.id, sliceId); if (cmp < 0) { - points.push(slice); + markers.push(slice); return; } else if (!cmp) return; for (let i = length - 2; i >= 0; i--) { - const currSlice = points[i]; + const currSlice = markers[i]; const cmp = compare(currSlice.id, sliceId); if (cmp < 0) { - points.splice(i + 1, 0, slice); + markers.splice(i + 1, 0, slice); return; } else if (!cmp) return; } - points.unshift(slice); + markers.unshift(slice); } /** @@ -150,18 +131,59 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { * the text, even if the start and end of the slice are different. * * @param slice Slice to remove from the marker list. + * @deprecated This method might happen to be not necessary. */ public removeMarker(slice: Slice): void { - const points = this.markers; - const length = points.length; + /** @deprecated */ + const markers = this.markers; + const length = markers.length; for (let i = 0; i < length; i++) { - if (points[i] === slice) { - points.splice(i, 1); + if (markers[i] === slice) { + markers.splice(i, 1); return; } } } + // --------------------------------------------------------------------- refs + + /** + * Sorted list of all references to rich-text constructs. + */ + public readonly refs: OverlayRef[] = []; + + public addMarkerRef(slice: SplitSlice): void { + this.refs.push(slice); + this.addMarker(slice); + } + + public addLayerStartRef(slice: Slice): void { + this.refs.push(new OverlayRefSliceStart(slice)); + this.addLayer(slice); + } + + public addLayerEndRef(slice: Slice): void { + this.refs.push(new OverlayRefSliceEnd(slice)); + } + + public removeRef(slice: Slice): void { + const refs = this.refs; + const length = refs.length; + for (let i = 0; i < length; i++) { + const ref = refs[i]; + if ( + ref === slice || + (ref instanceof OverlayRefSliceStart && ref.slice === slice) || + (ref instanceof OverlayRefSliceEnd && ref.slice === slice) + ) { + refs.splice(i, 1); + break; + } + } + this.removeLayer(slice); + this.removeMarker(slice); + } + // ---------------------------------------------------------------- Printable public toStringName(tab: string, lite?: boolean): string { diff --git a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts index 1242087848..629e972b04 100644 --- a/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts +++ b/src/json-crdt-extensions/peritext/overlay/__tests__/OverlayPoint.spec.ts @@ -1,6 +1,7 @@ import {Point} from '../../rga/Point'; import {setup} from '../../slice/__tests__/setup'; import {OverlayPoint} from '../OverlayPoint'; +import {OverlayRefSliceEnd, OverlayRefSliceStart} from '../refs'; const setupOverlayPoint = () => { const deps = setup(); @@ -196,9 +197,9 @@ describe('markers', () => { test('can add tree markers by appending them', () => { const {peritext, getPoint} = setupOverlayPoint(); - const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); - const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); - const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 1), '

'); + const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 2), '

'); + const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 3), '

'); const point = getPoint(marker2.start); point.addMarker(marker1); point.addMarker(marker2); @@ -210,9 +211,9 @@ describe('markers', () => { test('can remove markers', () => { const {peritext, getPoint} = setupOverlayPoint(); - const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); - const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); - const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 0), '

'); + const marker1 = peritext.slices.insSplit(peritext.rangeAt(6, 1), '

'); + const marker2 = peritext.slices.insSplit(peritext.rangeAt(6, 1), '

'); + const marker3 = peritext.slices.insSplit(peritext.rangeAt(6, 2), '

'); const point = getPoint(marker1.start); point.addMarker(marker2); point.addMarker(marker1); @@ -232,3 +233,78 @@ describe('markers', () => { expect(point.markers.length).toBe(0); }); }); + +describe('refs', () => { + test('can add marker ref', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker = peritext.slices.insSplit(peritext.rangeAt(10, 1), '

'); + const point = getPoint(marker.start); + expect(point.markers.length).toBe(0); + expect(point.refs.length).toBe(0); + point.addMarkerRef(marker); + expect(point.markers.length).toBe(1); + expect(point.refs.length).toBe(1); + expect(point.markers[0]).toBe(marker); + expect(point.refs[0]).toBe(marker); + }); + + test('can add layer ref (start)', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice = peritext.slices.insErase(peritext.rangeAt(0, 4), 123); + const point = getPoint(slice.start); + expect(point.layers.length).toBe(0); + expect(point.refs.length).toBe(0); + point.addLayerStartRef(slice); + expect(point.layers.length).toBe(1); + expect(point.refs.length).toBe(1); + expect(point.layers[0]).toBe(slice); + expect((point.refs[0] as OverlayRefSliceStart).slice).toBe(slice); + }); + + test('can add layer ref (end)', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const slice = peritext.slices.insErase(peritext.rangeAt(0, 4), 123); + const point = getPoint(slice.end); + expect(point.layers.length).toBe(0); + expect(point.refs.length).toBe(0); + point.addLayerEndRef(slice); + expect(point.layers.length).toBe(0); + expect(point.refs.length).toBe(1); + expect((point.refs[0] as OverlayRefSliceEnd).slice).toBe(slice); + }); + + test('can add marker and layer start', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker = peritext.slices.insSplit(peritext.rangeAt(10, 1), '

'); + const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123); + const point = getPoint(slice.end); + expect(point.layers.length).toBe(0); + expect(point.markers.length).toBe(0); + expect(point.refs.length).toBe(0); + point.addMarkerRef(marker); + point.addLayerStartRef(slice); + expect(point.layers.length).toBe(1); + expect(point.markers.length).toBe(1); + expect(point.refs.length).toBe(2); + }); + + test('can remove marker and layer', () => { + const {peritext, getPoint} = setupOverlayPoint(); + const marker = peritext.slices.insSplit(peritext.rangeAt(10, 1), '

'); + const slice = peritext.slices.insErase(peritext.rangeAt(10, 4), 123); + const point = getPoint(slice.end); + point.addMarkerRef(marker); + point.addLayerStartRef(slice); + expect(point.layers.length).toBe(1); + expect(point.markers.length).toBe(1); + expect(point.refs.length).toBe(2); + point.removeRef(slice); + expect(point.layers.length).toBe(0); + expect(point.markers.length).toBe(1); + expect(point.refs.length).toBe(1); + point.removeRef(marker); + expect(point.layers.length).toBe(0); + expect(point.markers.length).toBe(0); + expect(point.refs.length).toBe(0); + }); +}); From faf466f71f3a1c8d68702f58181cd85b11981c4b Mon Sep 17 00:00:00 2001 From: streamich Date: Fri, 26 Apr 2024 16:05:58 +0200 Subject: [PATCH 9/9] =?UTF-8?q?perf(json-crdt-extensions):=20=E2=9A=A1?= =?UTF-8?q?=EF=B8=8F=20remove=20immediately=20from=20the=20right=20bucket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../peritext/overlay/OverlayPoint.ts | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts index c9f6e6bf8c..04fc26575d 100644 --- a/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts +++ b/src/json-crdt-extensions/peritext/overlay/OverlayPoint.ts @@ -152,36 +152,60 @@ export class OverlayPoint extends Point implements Printable, HeadlessNode { */ public readonly refs: OverlayRef[] = []; + /** + * Insert a reference to a marker. + * + * @param slice A marker (split slice). + */ public addMarkerRef(slice: SplitSlice): void { this.refs.push(slice); this.addMarker(slice); } + /** + * Insert a layer that starts at this point. + * + * @param slice A slice that starts at this point. + */ public addLayerStartRef(slice: Slice): void { this.refs.push(new OverlayRefSliceStart(slice)); this.addLayer(slice); } + /** + * Insert a layer that ends at this point. + * + * @param slice A slice that ends at this point. + */ public addLayerEndRef(slice: Slice): void { this.refs.push(new OverlayRefSliceEnd(slice)); } + /** + * Removes a reference to a marker or a slice, and remove the corresponding + * layer or marker. + * + * @param slice A slice to remove the reference to. + */ public removeRef(slice: Slice): void { const refs = this.refs; const length = refs.length; for (let i = 0; i < length; i++) { const ref = refs[i]; + if (ref === slice) { + refs.splice(i, 1); + this.removeMarker(slice); + return; + } if ( - ref === slice || (ref instanceof OverlayRefSliceStart && ref.slice === slice) || (ref instanceof OverlayRefSliceEnd && ref.slice === slice) ) { refs.splice(i, 1); - break; + this.removeLayer(slice); + return; } } - this.removeLayer(slice); - this.removeMarker(slice); } // ---------------------------------------------------------------- Printable