mirror of
https://github.com/ether/etherpad-lite.git
synced 2025-04-25 18:06:15 -04:00
Changeset: New API to simplify attribute processing
This commit is contained in:
parent
982d8ad0f2
commit
6cf2055199
6 changed files with 756 additions and 0 deletions
91
src/static/js/AttributeMap.js
Normal file
91
src/static/js/AttributeMap.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
'use strict';
|
||||
|
||||
const attributes = require('./attributes');
|
||||
|
||||
/**
|
||||
* A `[key, value]` pair of strings describing a text attribute.
|
||||
*
|
||||
* @typedef {[string, string]} Attribute
|
||||
*/
|
||||
|
||||
/**
|
||||
* A concatenated sequence of zero or more attribute identifiers, each one represented by an
|
||||
* asterisk followed by a base-36 encoded attribute number.
|
||||
*
|
||||
* Examples: '', '*0', '*3*j*z*1q'
|
||||
*
|
||||
* @typedef {string} AttributeString
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
|
||||
*/
|
||||
class AttributeMap extends Map {
|
||||
/**
|
||||
* Converts an attribute string into an AttributeMap.
|
||||
*
|
||||
* @param {AttributeString} str - The attribute string to convert into an AttributeMap.
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @returns {AttributeMap}
|
||||
*/
|
||||
static fromString(str, pool) {
|
||||
return new AttributeMap(pool).updateFromString(str);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
*/
|
||||
constructor(pool) {
|
||||
super();
|
||||
/** @public */
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} k - Attribute name.
|
||||
* @param {string} v - Attribute value.
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
set(k, v) {
|
||||
k = k == null ? '' : String(k);
|
||||
v = v == null ? '' : String(v);
|
||||
this.pool.putAttrib([k, v]);
|
||||
return super.set(k, v);
|
||||
}
|
||||
|
||||
toString() {
|
||||
return attributes.attribsToString(attributes.sort([...this]), this.pool);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Iterable<Attribute>} entries - [key, value] pairs to insert into this map.
|
||||
* @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the
|
||||
* key is removed from this map (if present).
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
update(entries, emptyValueIsDelete = false) {
|
||||
for (let [k, v] of entries) {
|
||||
k = k == null ? '' : String(k);
|
||||
v = v == null ? '' : String(v);
|
||||
if (!v && emptyValueIsDelete) {
|
||||
this.delete(k);
|
||||
} else {
|
||||
this.set(k, v);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {AttributeString} str - The attribute string identifying the attributes to insert into
|
||||
* this map.
|
||||
* @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the
|
||||
* key is removed from this map (if present).
|
||||
* @returns {AttributeMap} `this` (for chaining).
|
||||
*/
|
||||
updateFromString(str, emptyValueIsDelete = false) {
|
||||
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = AttributeMap;
|
130
src/static/js/attributes.js
Normal file
130
src/static/js/attributes.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
'use strict';
|
||||
|
||||
// Low-level utilities for manipulating attribute strings. For a high-level API, see AttributeMap.
|
||||
|
||||
/**
|
||||
* A `[key, value]` pair of strings describing a text attribute.
|
||||
*
|
||||
* @typedef {[string, string]} Attribute
|
||||
*/
|
||||
|
||||
/**
|
||||
* A concatenated sequence of zero or more attribute identifiers, each one represented by an
|
||||
* asterisk followed by a base-36 encoded attribute number.
|
||||
*
|
||||
* Examples: '', '*0', '*3*j*z*1q'
|
||||
*
|
||||
* @typedef {string} AttributeString
|
||||
*/
|
||||
|
||||
/**
|
||||
* Converts an attribute string into a sequence of attribute identifier numbers.
|
||||
*
|
||||
* WARNING: This only works on attribute strings. It does NOT work on serialized operations or
|
||||
* changesets.
|
||||
*
|
||||
* @param {AttributeString} str - Attribute string.
|
||||
* @yields {number} The attribute numbers (to look up in the associated pool), in the order they
|
||||
* appear in `str`.
|
||||
* @returns {Generator<number>}
|
||||
*/
|
||||
exports.decodeAttribString = function* (str) {
|
||||
const re = /\*([0-9a-z]+)|./gy;
|
||||
let match;
|
||||
while ((match = re.exec(str)) != null) {
|
||||
const [m, n] = match;
|
||||
if (n == null) throw new Error(`invalid character in attribute string: ${m}`);
|
||||
yield Number.parseInt(n, 36);
|
||||
}
|
||||
};
|
||||
|
||||
const checkAttribNum = (n) => {
|
||||
if (typeof n !== 'number') throw new TypeError(`not a number: ${n}`);
|
||||
if (n < 0) throw new Error(`attribute number is negative: ${n}`);
|
||||
if (n !== Math.trunc(n)) throw new Error(`attribute number is not an integer: ${n}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inverse of `decodeAttribString`.
|
||||
*
|
||||
* @param {Iterable<number>} attribNums - Sequence of attribute numbers.
|
||||
* @returns {AttributeString}
|
||||
*/
|
||||
exports.encodeAttribString = (attribNums) => {
|
||||
let str = '';
|
||||
for (const n of attribNums) {
|
||||
checkAttribNum(n);
|
||||
str += `*${n.toString(36).toLowerCase()}`;
|
||||
}
|
||||
return str;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts a sequence of attribute numbers into a sequence of attributes.
|
||||
*
|
||||
* @param {Iterable<number>} attribNums - Attribute numbers to look up in the pool.
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @yields {Attribute} The identified attributes, in the same order as `attribNums`.
|
||||
* @returns {Generator<Attribute>}
|
||||
*/
|
||||
exports.attribsFromNums = function* (attribNums, pool) {
|
||||
for (const n of attribNums) {
|
||||
checkAttribNum(n);
|
||||
const attrib = pool.getAttrib(n);
|
||||
if (attrib == null) throw new Error(`attribute ${n} does not exist in pool`);
|
||||
yield attrib;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Inverse of `attribsFromNums`.
|
||||
*
|
||||
* @param {Iterable<Attribute>} attribs - Attributes. Any attributes not already in `pool` are
|
||||
* inserted into `pool`. No checking is performed to ensure that the attributes are in the
|
||||
* canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if
|
||||
* required.)
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @yields {number} The attribute number of each attribute in `attribs`, in order.
|
||||
* @returns {Generator<number>}
|
||||
*/
|
||||
exports.attribsToNums = function* (attribs, pool) {
|
||||
for (const attrib of attribs) yield pool.putAttrib(attrib);
|
||||
};
|
||||
|
||||
/**
|
||||
* Convenience function that is equivalent to `attribsFromNums(decodeAttribString(str), pool)`.
|
||||
*
|
||||
* WARNING: This only works on attribute strings. It does NOT work on serialized operations or
|
||||
* changesets.
|
||||
*
|
||||
* @param {AttributeString} str - Attribute string.
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @yields {Attribute} The attributes identified in `str`, in order.
|
||||
* @returns {Generator<Attribute>}
|
||||
*/
|
||||
exports.attribsFromString = function* (str, pool) {
|
||||
yield* exports.attribsFromNums(exports.decodeAttribString(str), pool);
|
||||
};
|
||||
|
||||
/**
|
||||
* Inverse of `attribsFromString`.
|
||||
*
|
||||
* @param {Iterable<Attribute>} attribs - Attributes. The attributes to insert into the pool (if
|
||||
* necessary) and encode. No checking is performed to ensure that the attributes are in the
|
||||
* canonical order and that there are no duplicate keys. (Use an AttributeMap and/or `sort()` if
|
||||
* required.)
|
||||
* @param {AttributePool} pool - Attribute pool.
|
||||
* @returns {AttributeString}
|
||||
*/
|
||||
exports.attribsToString =
|
||||
(attribs, pool) => exports.encodeAttribString(exports.attribsToNums(attribs, pool));
|
||||
|
||||
/**
|
||||
* Sorts the attributes in canonical order. The order of entries with the same attribute name is
|
||||
* unspecified.
|
||||
*
|
||||
* @param {Attribute[]} attribs - Attributes to sort in place.
|
||||
* @returns {Attribute[]} `attribs` (for chaining).
|
||||
*/
|
||||
exports.sort =
|
||||
(attribs) => attribs.sort(([keyA], [keyB]) => (keyA > keyB ? 1 : 0) - (keyA < keyB ? 1 : 0));
|
Loading…
Add table
Add a link
Reference in a new issue