|
| 1 | +'use strict'; |
| 2 | + |
| 3 | +// Low-level utilities for manipulating attribute strings. For a high-level API, see AttributeMap. |
| 4 | + |
| 5 | +/** |
| 6 | + * A `[key, value]` pair of strings describing a text attribute. |
| 7 | + * |
| 8 | + * @typedef {[string, string]} Attribute |
| 9 | + */ |
| 10 | + |
| 11 | +/** |
| 12 | + * A concatenated sequence of zero or more attribute identifiers, each one represented by an |
| 13 | + * asterisk followed by a base-36 encoded attribute number. |
| 14 | + * |
| 15 | + * Examples: '', '*0', '*3*j*z*1q' |
| 16 | + * |
| 17 | + * @typedef {string} AttributeString |
| 18 | + */ |
| 19 | + |
| 20 | +/** |
| 21 | + * Converts an attribute string into a sequence of attribute identifier numbers. |
| 22 | + * |
| 23 | + * WARNING: This only works on attribute strings. It does NOT work on serialized operations or |
| 24 | + * changesets. |
| 25 | + * |
| 26 | + * @param {AttributeString} str - Attribute string. |
| 27 | + * @yields {number} The attribute numbers (to look up in the associated pool), in the order they |
| 28 | + * appear in `str`. |
| 29 | + * @returns {Generator<number>} |
| 30 | + */ |
| 31 | +exports.decodeAttribString = function* (str) { |
| 32 | + const re = /\*([0-9a-z]+)|./gy; |
| 33 | + let match; |
| 34 | + while ((match = re.exec(str)) != null) { |
| 35 | + const [m, n] = match; |
| 36 | + if (n == null) throw new Error(`invalid character in attribute string: ${m}`); |
| 37 | + yield Number.parseInt(n, 36); |
| 38 | + } |
| 39 | +}; |
| 40 | + |
| 41 | +const checkAttribNum = (n) => { |
| 42 | + if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`); |
| 43 | + if (n < 0) throw new Error(`attribute number is negative: ${n}`); |
| 44 | + if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`); |
| 45 | +}; |
| 46 | + |
| 47 | +/** |
| 48 | + * Inverse of `decodeAttribString`. |
| 49 | + * |
| 50 | + * @param {Iterable<number>} attribNums - Sequence of attribute numbers. |
| 51 | + * @returns {AttributeString} |
| 52 | + */ |
| 53 | +exports.encodeAttribString = (attribNums) => { |
| 54 | + let str = ''; |
| 55 | + for (const n of attribNums) { |
| 56 | + checkAttribNum(n); |
| 57 | + str += `*${n.toString(36).toLowerCase()}`; |
| 58 | + } |
| 59 | + return str; |
| 60 | +}; |
| 61 | + |
| 62 | +/** |
| 63 | + * Converts a sequence of attribute numbers into a sequence of attributes. |
| 64 | + * |
| 65 | + * @param {Iterable<number>} attribNums - Attribute numbers to look up in the pool. |
| 66 | + * @param {AttributePool} pool - Attribute pool. |
| 67 | + * @yields {Attribute} The identified attributes, in the same order as `attribNums`. |
| 68 | + * @returns {Generator<Attribute>} |
| 69 | + */ |
| 70 | +exports.attribsFromNums = function* (attribNums, pool) { |
| 71 | + for (const n of attribNums) { |
| 72 | + checkAttribNum(n); |
| 73 | + const attrib = pool.getAttrib(n); |
| 74 | + if (attrib == null) throw new Error(`attribute ${n} does not exist in pool`); |
| 75 | + yield attrib; |
| 76 | + } |
| 77 | +}; |
| 78 | + |
| 79 | +/** |
| 80 | + * Inverse of `attribsFromNums`. |
| 81 | + * |
| 82 | + * @param {Iterable<Attribute>} attribs - Attributes. Any attributes not already in `pool` are |
| 83 | + * inserted into `pool`. No checking is performed to ensure that the attributes are in the |
| 84 | + * canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if |
| 85 | + * required.) |
| 86 | + * @param {AttributePool} pool - Attribute pool. |
| 87 | + * @yields {number} The attribute number of each attribute in `attribs`, in order. |
| 88 | + * @returns {Generator<number>} |
| 89 | + */ |
| 90 | +exports.attribsToNums = function* (attribs, pool) { |
| 91 | + for (const attrib of attribs) yield pool.putAttrib(attrib); |
| 92 | +}; |
| 93 | + |
| 94 | +/** |
| 95 | + * Convenience function that is equivalent to `attribsFromNums(decodeAttribString(str), pool)`. |
| 96 | + * |
| 97 | + * WARNING: This only works on attribute strings. It does NOT work on serialized operations or |
| 98 | + * changesets. |
| 99 | + * |
| 100 | + * @param {AttributeString} str - Attribute string. |
| 101 | + * @param {AttributePool} pool - Attribute pool. |
| 102 | + * @yields {Attribute} The attributes identified in `str`, in order. |
| 103 | + * @returns {Generator<Attribute>} |
| 104 | + */ |
| 105 | +exports.attribsFromString = function* (str, pool) { |
| 106 | + yield* exports.attribsFromNums(exports.decodeAttribString(str), pool); |
| 107 | +}; |
| 108 | + |
| 109 | +/** |
| 110 | + * Inverse of `attribsFromString`. |
| 111 | + * |
| 112 | + * @param {Iterable<Attribute>} attribs - Attributes. The attributes to insert into the pool (if |
| 113 | + * necessary) and encode. No checking is performed to ensure that the attributes are in the |
| 114 | + * canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if |
| 115 | + * required.) |
| 116 | + * @param {AttributePool} pool - Attribute pool. |
| 117 | + * @returns {AttributeString} |
| 118 | + */ |
| 119 | +exports.attribsToString = |
| 120 | + (attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool)); |
| 121 | + |
| 122 | +/** |
| 123 | + * Sorts the attributes in canonical order. The order of entries with the same attribute name is |
| 124 | + * unspecified. |
| 125 | + * |
| 126 | + * @param {Attribute[]} attribs - Attributes to sort in place. |
| 127 | + * @returns {Attribute[]} `attribs` (for chaining). |
| 128 | + */ |
| 129 | +exports.sort = |
| 130 | + (attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0)); |
0 commit comments