@@ -83,31 +83,74 @@ exports.numToString = (num) => num.toString(36).toLowerCase();
8383
8484/**
8585 * An operation to apply to a shared document.
86- *
87- * @typedef {object } Op
88- * @property {('+'|'-'|'='|'') } opcode - The operation's operator:
89- * - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
90- * document.
91- * - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
92- * document.
93- * - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
94- * the document. The inserted characters come from the changeset's character bank.
95- * - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
96- * operation.
97- * @property {number } chars - The number of characters to keep, insert, or delete.
98- * @property {number } lines - The number of characters among the `chars` characters that are
99- * newlines. If non-zero, the last character must be a newline.
100- * @property {string } attribs - Identifiers of attributes to apply to the text, represented as a
101- * repeated (zero or more) sequence of asterisk followed by a non-negative base-36 (lower-case)
102- * integer. For example, '*2*1o' indicates that attributes 2 and 60 apply to the text affected
103- * by the operation. The identifiers come from the document's attribute pool. This is the empty
104- * string for remove ('-') operations. For keep ('=') operations, the attributes are merged with
105- * the base text's existing attributes:
106- * - A keep op attribute with a non-empty value replaces an existing base text attribute that
107- * has the same key.
108- * - A keep op attribute with an empty value is interpreted as an instruction to remove an
109- * existing base text attribute that has the same key, if one exists.
11086 */
87+ class Op {
88+ /**
89+ * @param {(''|'='|'+'|'-') } [opcode=''] - Initial value of the `opcode` property.
90+ */
91+ constructor ( opcode = '' ) {
92+ /**
93+ * The operation's operator:
94+ * - '=': Keep the next `chars` characters (containing `lines` newlines) from the base
95+ * document.
96+ * - '-': Remove the next `chars` characters (containing `lines` newlines) from the base
97+ * document.
98+ * - '+': Insert `chars` characters (containing `lines` newlines) at the current position in
99+ * the document. The inserted characters come from the changeset's character bank.
100+ * - '' (empty string): Invalid operator used in some contexts to signifiy the lack of an
101+ * operation.
102+ *
103+ * @type {(''|'='|'+'|'-') }
104+ * @public
105+ */
106+ this . opcode = opcode ;
107+
108+ /**
109+ * The number of characters to keep, insert, or delete.
110+ *
111+ * @type {number }
112+ * @public
113+ */
114+ this . chars = 0 ;
115+
116+ /**
117+ * The number of characters among the `chars` characters that are newlines. If non-zero, the
118+ * last character must be a newline.
119+ *
120+ * @type {number }
121+ * @public
122+ */
123+ this . lines = 0 ;
124+
125+ /**
126+ * Identifiers of attributes to apply to the text, represented as a repeated (zero or more)
127+ * sequence of asterisk followed by a non-negative base-36 (lower-case) integer. For example,
128+ * '*2*1o' indicates that attributes 2 and 60 apply to the text affected by the operation. The
129+ * identifiers come from the document's attribute pool.
130+ *
131+ * For keep ('=') operations, the attributes are merged with the base text's existing
132+ * attributes:
133+ * - A keep op attribute with a non-empty value replaces an existing base text attribute that
134+ * has the same key.
135+ * - A keep op attribute with an empty value is interpreted as an instruction to remove an
136+ * existing base text attribute that has the same key, if one exists.
137+ *
138+ * This is the empty string for remove ('-') operations.
139+ *
140+ * @type {string }
141+ * @public
142+ */
143+ this . attribs = '' ;
144+ }
145+
146+ toString ( ) {
147+ if ( ! this . opcode ) throw new TypeError ( 'null op' ) ;
148+ if ( typeof this . attribs !== 'string' ) throw new TypeError ( 'attribs must be a string' ) ;
149+ const l = this . lines ? `|${ exports . numToString ( this . lines ) } ` : '' ;
150+ return this . attribs + l + this . opcode + exports . numToString ( this . chars ) ;
151+ }
152+ }
153+ exports . Op = Op ;
111154
112155/**
113156 * Describes changes to apply to a document. Does not include the attribute pool or the original
@@ -166,8 +209,7 @@ exports.opIterator = (opsStr) => {
166209 } ;
167210 let regexResult = nextRegexMatch ( ) ;
168211
169- const next = ( optOp ) => {
170- const op = optOp || exports . newOp ( ) ;
212+ const next = ( op = new Op ( ) ) => {
171213 if ( regexResult [ 0 ] ) {
172214 op . attribs = regexResult [ 1 ] ;
173215 op . lines = exports . parseNum ( regexResult [ 2 ] || '0' ) ;
@@ -203,15 +245,14 @@ const clearOp = (op) => {
203245/**
204246 * Creates a new Op object
205247 *
248+ * @deprecated Use the `Op` class instead.
206249 * @param {('+'|'-'|'='|'') } [optOpcode=''] - The operation's operator.
207250 * @returns {Op }
208251 */
209- exports . newOp = ( optOpcode ) => ( {
210- opcode : ( optOpcode || '' ) ,
211- chars : 0 ,
212- lines : 0 ,
213- attribs : '' ,
214- } ) ;
252+ exports . newOp = ( optOpcode ) => {
253+ padutils . warnWithStack ( 'Changeset.newOp() is deprecated; use the Changeset.Op class instead' ) ;
254+ return new Op ( optOpcode ) ;
255+ } ;
215256
216257/**
217258 * Copies op1 to op2
@@ -220,7 +261,7 @@ exports.newOp = (optOpcode) => ({
220261 * @param {Op } [op2] - dest Op. If not given, a new Op is used.
221262 * @returns {Op } `op2`
222263 */
223- const copyOp = ( op1 , op2 = exports . newOp ( ) ) => Object . assign ( op2 , op1 ) ;
264+ const copyOp = ( op1 , op2 = new Op ( ) ) => Object . assign ( op2 , op1 ) ;
224265
225266/**
226267 * Serializes a sequence of Ops.
@@ -257,7 +298,7 @@ const copyOp = (op1, op2 = exports.newOp()) => Object.assign(op2, op1);
257298 * @returns {Generator<Op> }
258299 */
259300const opsFromText = function * ( opcode , text , attribs = '' , pool = null ) {
260- const op = exports . newOp ( opcode ) ;
301+ const op = new Op ( opcode ) ;
261302 op . attribs = typeof attribs === 'string'
262303 ? attribs : new AttributeMap ( pool ) . update ( attribs || [ ] , opcode === '+' ) . toString ( ) ;
263304 const lastNewlinePos = text . lastIndexOf ( '\n' ) ;
@@ -447,7 +488,7 @@ exports.smartOpAssembler = () => {
447488 */
448489exports . mergingOpAssembler = ( ) => {
449490 const assem = exports . opAssembler ( ) ;
450- const bufOp = exports . newOp ( ) ;
491+ const bufOp = new Op ( ) ;
451492
452493 // If we get, for example, insertions [xxx\n,yyy], those don't merge,
453494 // but if we get [xxx\n,yyy,zzz\n], that merges to [xxx\nyyyzzz\n].
@@ -523,12 +564,8 @@ exports.opAssembler = () => {
523564 * @param {Op } op - Operation to add. Ownership remains with the caller.
524565 */
525566 const append = ( op ) => {
526- if ( ! op . opcode ) throw new TypeError ( 'null op' ) ;
527- if ( typeof op . attribs !== 'string' ) throw new TypeError ( 'attribs must be a string' ) ;
528- serialized += op . attribs ;
529- if ( op . lines ) serialized += `|${ exports . numToString ( op . lines ) } ` ;
530- serialized += op . opcode ;
531- serialized += exports . numToString ( op . chars ) ;
567+ assert ( op instanceof Op , 'argument must be an instance of Op' ) ;
568+ serialized += op . toString ( ) ;
532569 } ;
533570
534571 const toString = ( ) => serialized ;
@@ -972,8 +1009,8 @@ const applyZip = (in1, in2, func) => {
9721009 const iter1 = exports . opIterator ( in1 ) ;
9731010 const iter2 = exports . opIterator ( in2 ) ;
9741011 const assem = exports . smartOpAssembler ( ) ;
975- const op1 = exports . newOp ( ) ;
976- const op2 = exports . newOp ( ) ;
1012+ const op1 = new Op ( ) ;
1013+ const op2 = new Op ( ) ;
9771014 while ( op1 . opcode || iter1 . hasNext ( ) || op2 . opcode || iter2 . hasNext ( ) ) {
9781015 if ( ( ! op1 . opcode ) && iter1 . hasNext ( ) ) iter1 . next ( op1 ) ;
9791016 if ( ( ! op2 . opcode ) && iter2 . hasNext ( ) ) iter2 . next ( op2 ) ;
@@ -1148,7 +1185,7 @@ exports.composeAttributes = (att1, att2, resultIsMutation, pool) => {
11481185 * @returns {Op } The result of applying `csOp` to `attOp`.
11491186 */
11501187const slicerZipperFunc = ( attOp , csOp , pool ) => {
1151- const opOut = exports . newOp ( ) ;
1188+ const opOut = new Op ( ) ;
11521189 if ( ! attOp . opcode ) {
11531190 copyOp ( csOp , opOut ) ;
11541191 csOp . opcode = '' ;
@@ -1231,7 +1268,7 @@ exports.mutateAttributionLines = (cs, lines, pool) => {
12311268 const line = mut . removeLines ( 1 ) ;
12321269 lineIter = exports . opIterator ( line ) ;
12331270 }
1234- if ( ! lineIter || ! lineIter . hasNext ( ) ) return exports . newOp ( ) ;
1271+ if ( ! lineIter || ! lineIter . hasNext ( ) ) return new Op ( ) ;
12351272 return lineIter . next ( ) ;
12361273 } ;
12371274 let lineAssem = null ;
@@ -1248,8 +1285,8 @@ exports.mutateAttributionLines = (cs, lines, pool) => {
12481285 lineAssem = null ;
12491286 } ;
12501287
1251- let csOp = exports . newOp ( ) ;
1252- let attOp = exports . newOp ( ) ;
1288+ let csOp = new Op ( ) ;
1289+ let attOp = new Op ( ) ;
12531290 while ( csOp . opcode || csIter . hasNext ( ) || attOp . opcode || isNextMutOp ( ) ) {
12541291 if ( ! csOp . opcode && csIter . hasNext ( ) ) csOp = csIter . next ( ) ;
12551292 if ( ( ! csOp . opcode ) && ( ! attOp . opcode ) && ( ! lineAssem ) && ( ! ( lineIter && lineIter . hasNext ( ) ) ) ) {
@@ -1826,7 +1863,7 @@ exports.attribsAttributeValue = (attribs, key, pool) => {
18261863 */
18271864exports . builder = ( oldLen ) => {
18281865 const assem = exports . smartOpAssembler ( ) ;
1829- const o = exports . newOp ( ) ;
1866+ const o = new Op ( ) ;
18301867 const charBank = exports . stringAssembler ( ) ;
18311868
18321869 const self = {
@@ -1930,8 +1967,8 @@ exports.makeAttribsString = (opcode, attribs, pool) => {
19301967exports . subattribution = ( astr , start , optEnd ) => {
19311968 const iter = exports . opIterator ( astr ) ;
19321969 const assem = exports . smartOpAssembler ( ) ;
1933- let attOp = exports . newOp ( ) ;
1934- const csOp = exports . newOp ( ) ;
1970+ let attOp = new Op ( ) ;
1971+ const csOp = new Op ( ) ;
19351972
19361973 const doCsOp = ( ) => {
19371974 if ( ! csOp . chars ) return ;
@@ -1994,7 +2031,7 @@ exports.inverse = (cs, lines, alines, pool) => {
19942031 let curChar = 0 ;
19952032 let curLineOpIter = null ;
19962033 let curLineOpIterLine ;
1997- let curLineNextOp = exports . newOp ( '+' ) ;
2034+ let curLineNextOp = new Op ( '+' ) ;
19982035
19992036 const unpacked = exports . unpack ( cs ) ;
20002037 const csIter = exports . opIterator ( unpacked . ops ) ;
@@ -2025,7 +2062,7 @@ exports.inverse = (cs, lines, alines, pool) => {
20252062 curLineOpIter = exports . opIterator ( alinesGet ( curLine ) ) ;
20262063 }
20272064 if ( ! curLineNextOp . chars ) {
2028- curLineNextOp = curLineOpIter . hasNext ( ) ? curLineOpIter . next ( ) : exports . newOp ( ) ;
2065+ curLineNextOp = curLineOpIter . hasNext ( ) ? curLineOpIter . next ( ) : new Op ( ) ;
20292066 }
20302067 const charsToUse = Math . min ( numChars , curLineNextOp . chars ) ;
20312068 func ( charsToUse , curLineNextOp . attribs , charsToUse === curLineNextOp . chars &&
@@ -2135,7 +2172,7 @@ exports.follow = (cs1, cs2, reverseInsertOrder, pool) => {
21352172 const hasInsertFirst = exports . attributeTester ( [ 'insertorder' , 'first' ] , pool ) ;
21362173
21372174 const newOps = applyZip ( unpacked1 . ops , unpacked2 . ops , ( op1 , op2 ) => {
2138- const opOut = exports . newOp ( ) ;
2175+ const opOut = new Op ( ) ;
21392176 if ( op1 . opcode === '+' || op2 . opcode === '+' ) {
21402177 let whichToDo ;
21412178 if ( op2 . opcode !== '+' ) {
0 commit comments