| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- // @ts-check
- const { containsNonBmpChars } = require('../util')
- const {
- ApplyError,
- InvalidInsertionError,
- UnprocessableError,
- } = require('../errors')
- /** @typedef {{ result: string, inputCursor: number}} ApplyContext */
- /** @typedef {{ length: number, inputCursor: number, readonly inputLength: number}} LengthApplyContext */
- class ScanOp {
- constructor() {
- if (this.constructor === ScanOp) {
- throw new Error('Cannot instantiate abstract class')
- }
- }
- /**
- * Applies an operation to a string
- * @param {string} input
- * @param {ApplyContext} current
- * @returns {ApplyContext}
- */
- apply(input, current) {
- throw new Error('abstract method')
- }
- /**
- * Applies an operation to a length
- * @param {LengthApplyContext} current
- * @returns {LengthApplyContext}
- */
- applyToLength(current) {
- throw new Error('abstract method')
- }
- toJSON() {
- throw new Error('abstract method')
- }
- /**
- * @param {object} raw
- * @returns {ScanOp}
- */
- static fromJSON(raw) {
- if (isRetain(raw)) {
- return RetainOp.fromJSON(raw)
- } else if (isInsert(raw)) {
- return InsertOp.fromJSON(raw)
- } else if (isRemove(raw)) {
- return RemoveOp.fromJSON(raw)
- }
- throw new UnprocessableError(`Invalid ScanOp ${JSON.stringify(raw)}`)
- }
- /**
- * Tests whether two ScanOps are equal
- * @param {ScanOp} _other
- * @returns {boolean}
- */
- equals(_other) {
- return false
- }
- /**
- * Tests whether two ScanOps can be merged into a single operation
- * @param {ScanOp} other
- * @returns
- */
- canMergeWith(other) {
- return false
- }
- /**
- * Merge two ScanOps into a single operation
- * @param {ScanOp} _other
- * @returns {void}
- */
- mergeWith(_other) {
- throw new Error('abstract method')
- }
- toString() {
- 'ScanOp'
- }
- }
- class InsertOp extends ScanOp {
- constructor(insertion) {
- super()
- if (typeof insertion !== 'string') {
- throw new InvalidInsertionError('insertion must be a string')
- }
- if (containsNonBmpChars(insertion)) {
- throw new InvalidInsertionError('insertion contains non-BMP characters')
- }
- this.insertion = insertion
- }
- /**
- *
- * @param {{i: string} | string} op
- * @returns {InsertOp}
- */
- static fromJSON(op) {
- if (typeof op === 'string') {
- return new InsertOp(op)
- }
- // It must be an object with an 'i' property.
- if (typeof op.i !== 'string') {
- throw new InvalidInsertionError(
- 'insert operation must have a string property'
- )
- }
- return new InsertOp(op.i)
- }
- /**
- * @inheritdoc
- * @param {string} input
- * @param {ApplyContext} current
- * @returns {ApplyContext}
- * */
- apply(input, current) {
- if (containsNonBmpChars(this.insertion)) {
- throw new InvalidInsertionError(input, this.toJSON())
- }
- current.result += this.insertion
- return current
- }
- /**
- * @inheritdoc
- * @param {LengthApplyContext} current
- * @returns {LengthApplyContext}
- */
- applyToLength(current) {
- current.length += this.insertion.length
- return current
- }
- /** @inheritdoc */
- equals(other) {
- if (!(other instanceof InsertOp)) {
- return false
- }
- return this.insertion === other.insertion
- }
- canMergeWith(other) {
- return other instanceof InsertOp
- }
- mergeWith(other) {
- if (!(other instanceof InsertOp)) {
- throw new Error('Cannot merge with incompatible operation')
- }
- this.insertion += other.insertion
- }
- toJSON() {
- // TODO: Once we add metadata to the operation, generate an object rather
- // than the compact representation.
- return this.insertion
- }
- toString() {
- return `insert '${this.insertion}'`
- }
- }
- class RetainOp extends ScanOp {
- constructor(length) {
- super()
- if (length < 0) {
- throw new Error('length must be non-negative')
- }
- this.length = length
- }
- /**
- * @inheritdoc
- * @param {string} input
- * @param {ApplyContext} current
- * @returns {ApplyContext}
- * */
- apply(input, current) {
- if (current.inputCursor + this.length > input.length) {
- throw new ApplyError(
- "Operation can't retain more chars than are left in the string.",
- this.toJSON(),
- input
- )
- }
- current.result += input.slice(
- current.inputCursor,
- current.inputCursor + this.length
- )
- current.inputCursor += this.length
- return current
- }
- /**
- * @inheritdoc
- * @param {LengthApplyContext} current
- * @returns {LengthApplyContext}
- */
- applyToLength(current) {
- if (current.inputCursor + this.length > current.inputLength) {
- throw new ApplyError(
- "Operation can't retain more chars than are left in the string.",
- this.toJSON(),
- current.inputLength
- )
- }
- current.length += this.length
- current.inputCursor += this.length
- return current
- }
- /**
- *
- * @param {number | {r: number}} op
- * @returns
- */
- static fromJSON(op) {
- if (typeof op === 'number') {
- return new RetainOp(op)
- }
- // It must be an object with a 'r' property.
- if (typeof op.r !== 'number') {
- throw new Error('retain operation must have a number property')
- }
- return new RetainOp(op.r)
- }
- /** @inheritdoc */
- equals(other) {
- if (!(other instanceof RetainOp)) {
- return false
- }
- return this.length === other.length
- }
- canMergeWith(other) {
- return other instanceof RetainOp
- }
- mergeWith(other) {
- if (!(other instanceof RetainOp)) {
- throw new Error('Cannot merge with incompatible operation')
- }
- this.length += other.length
- }
- toJSON() {
- // TODO: Once we add metadata to the operation, generate an object rather
- // than the compact representation.
- return this.length
- }
- toString() {
- return `retain ${this.length}`
- }
- }
- class RemoveOp extends ScanOp {
- constructor(length) {
- super()
- if (length < 0) {
- throw new Error('length must be non-negative')
- }
- this.length = length
- }
- /**
- * @inheritdoc
- * @param {string} _input
- * @param {ApplyContext} current
- * @returns {ApplyContext}
- */
- apply(_input, current) {
- current.inputCursor += this.length
- return current
- }
- /**
- * @inheritdoc
- * @param {LengthApplyContext} current
- * @returns {LengthApplyContext}
- */
- applyToLength(current) {
- current.inputCursor += this.length
- return current
- }
- /**
- *
- * @param {number} op
- * @returns {RemoveOp}
- */
- static fromJSON(op) {
- if (typeof op !== 'number' || op > 0) {
- throw new Error('delete operation must be a negative number')
- }
- return new RemoveOp(-op)
- }
- /** @inheritdoc */
- equals(other) {
- if (!(other instanceof RemoveOp)) {
- return false
- }
- return this.length === other.length
- }
- canMergeWith(other) {
- return other instanceof RemoveOp
- }
- mergeWith(other) {
- if (!(other instanceof RemoveOp)) {
- throw new Error('Cannot merge with incompatible operation')
- }
- this.length += other.length
- }
- toJSON() {
- return -this.length
- }
- toString() {
- return `remove ${this.length}`
- }
- }
- function isRetain(op) {
- return (
- (typeof op === 'number' && op > 0) ||
- (typeof op === 'object' && typeof op.r === 'number' && op.r > 0)
- )
- }
- function isInsert(op) {
- return (
- typeof op === 'string' ||
- (typeof op === 'object' && typeof op.i === 'string')
- )
- }
- function isRemove(op) {
- return typeof op === 'number' && op < 0
- }
- module.exports = {
- ScanOp,
- InsertOp,
- RetainOp,
- RemoveOp,
- isRetain,
- isInsert,
- isRemove,
- }
|