Skip to content

Commit 6cf2055

Browse files
committed
Changeset: New API to simplify attribute processing
1 parent 982d8ad commit 6cf2055

File tree

6 files changed

+756
-0
lines changed

6 files changed

+756
-0
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
# 1.9.0 (not yet released)
2+
3+
### Notable enhancements
4+
5+
#### For plugin authors
6+
7+
* New APIs for processing attributes: `ep_etherpad-lite/static/js/attributes`
8+
(low-level API) and `ep_etherpad-lite/static/js/AttributeMap` (high-level
9+
API).
10+
111
# 1.8.15
212

313
### Security fixes

src/node/utils/tar.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
, "broadcast_revisions.js"
5555
, "socketio.js"
5656
, "AttributeManager.js"
57+
, "AttributeMap.js"
58+
, "attributes.js"
5759
, "ChangesetUtils.js"
5860
]
5961
, "ace2_inner.js": [
@@ -71,6 +73,8 @@
7173
, "linestylefilter.js"
7274
, "domline.js"
7375
, "AttributeManager.js"
76+
, "AttributeMap.js"
77+
, "attributes.js"
7478
, "scroll.js"
7579
, "caretPosition.js"
7680
, "pad_utils.js"

src/static/js/AttributeMap.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
'use strict';
2+
3+
const attributes = require('./attributes');
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+
* Convenience class to convert an Op's attribute string to/from a Map of key, value pairs.
22+
*/
23+
class AttributeMap extends Map {
24+
/**
25+
* Converts an attribute string into an AttributeMap.
26+
*
27+
* @param {AttributeString} str - The attribute string to convert into an AttributeMap.
28+
* @param {AttributePool} pool - Attribute pool.
29+
* @returns {AttributeMap}
30+
*/
31+
static fromString(str, pool) {
32+
return new AttributeMap(pool).updateFromString(str);
33+
}
34+
35+
/**
36+
* @param {AttributePool} pool - Attribute pool.
37+
*/
38+
constructor(pool) {
39+
super();
40+
/** @public */
41+
this.pool = pool;
42+
}
43+
44+
/**
45+
* @param {string} k - Attribute name.
46+
* @param {string} v - Attribute value.
47+
* @returns {AttributeMap} `this` (for chaining).
48+
*/
49+
set(k, v) {
50+
k = k == null ? '' : String(k);
51+
v = v == null ? '' : String(v);
52+
this.pool.putAttrib([k, v]);
53+
return super.set(k, v);
54+
}
55+
56+
toString() {
57+
return attributes.attribsToString(attributes.sort([...this]), this.pool);
58+
}
59+
60+
/**
61+
* @param {Iterable<Attribute>} entries - [key, value] pairs to insert into this map.
62+
* @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the
63+
* key is removed from this map (if present).
64+
* @returns {AttributeMap} `this` (for chaining).
65+
*/
66+
update(entries, emptyValueIsDelete = false) {
67+
for (let [k, v] of entries) {
68+
k = k == null ? '' : String(k);
69+
v = v == null ? '' : String(v);
70+
if (!v && emptyValueIsDelete) {
71+
this.delete(k);
72+
} else {
73+
this.set(k, v);
74+
}
75+
}
76+
return this;
77+
}
78+
79+
/**
80+
* @param {AttributeString} str - The attribute string identifying the attributes to insert into
81+
* this map.
82+
* @param {boolean} [emptyValueIsDelete] - If true and an entry's value is the empty string, the
83+
* key is removed from this map (if present).
84+
* @returns {AttributeMap} `this` (for chaining).
85+
*/
86+
updateFromString(str, emptyValueIsDelete = false) {
87+
return this.update(attributes.attribsFromString(str, this.pool), emptyValueIsDelete);
88+
}
89+
}
90+
91+
module.exports = AttributeMap;

src/static/js/attributes.js

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
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

Comments
 (0)