@nodable/entities 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -52,7 +52,7 @@ replacer.replace('© 2024 — Price: £9.99');
52
52
  Entities are processed in this fixed order — not configurable:
53
53
 
54
54
  ```
55
- persistent external → input/runtime → system → default → amp
55
+ persistent input/runtime → external → system → default → amp
56
56
  ```
57
57
 
58
58
  ### `persistent external` — Caller-supplied configuration entities
@@ -136,7 +136,7 @@ const replacer = new EntityReplacer({
136
136
  applyLimitsTo: 'external', // 'external' (default) | 'all' | ['external', 'system'] | ...
137
137
 
138
138
  // Post-processing hook — fires once on the fully resolved string
139
- postCheck: null, // (resolved: string, original: string) => string
139
+ postCheck: resolved => resolved, // (resolved: string, original: string) => string
140
140
  });
141
141
  ```
142
142
 
@@ -469,7 +469,9 @@ evp.addInputEntities({ company: 'Nodable' }); // called by BaseOutputBuilder
469
469
  const result: string = evp.parse('<©&brand;');
470
470
  ```
471
471
 
472
+ ## Note
472
473
 
474
+ This library silently skip numeric entities which are out range. For example `�` is skipped.
473
475
 
474
476
  ## License
475
477
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodable/entities",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Replace XML, HTML, External entites with security controls",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -140,22 +140,22 @@ export default class EntityReplacer {
140
140
  constructor(options = {}) {
141
141
  // Immutable config resolved at construction
142
142
  this._defaultTable = resolveTable(options.default, DEFAULT_XML_ENTITIES, true);
143
- this._systemTable = resolveTable(options.system, null, false);
144
- this._ampEnabled = options.amp !== false && options.amp !== null;
143
+ this._systemTable = resolveTable(options.system, null, false);
144
+ this._ampEnabled = options.amp !== false && options.amp !== null;
145
145
 
146
146
  this._maxTotalExpansions = options.maxTotalExpansions || 0;
147
- this._maxExpandedLength = options.maxExpandedLength || 0;
148
- this._applyLimitsTo = resolveApplyLimitsTo(options.applyLimitsTo ?? 'external');
149
- this._postCheck = typeof options.postCheck === 'function' ? options.postCheck : null;
147
+ this._maxExpandedLength = options.maxExpandedLength || 0;
148
+ this._applyLimitsTo = resolveApplyLimitsTo(options.applyLimitsTo ?? 'external');
149
+ this._postCheck = typeof options.postCheck === 'function' ? options.postCheck : r => r;
150
150
 
151
151
  // Pre-computed category limit flags
152
152
  this._limitExternal = this._applyLimitsTo === 'all' || (this._applyLimitsTo instanceof Set && this._applyLimitsTo.has('external'));
153
- this._limitSystem = this._applyLimitsTo === 'all' || (this._applyLimitsTo instanceof Set && this._applyLimitsTo.has('system'));
154
- this._limitDefault = this._applyLimitsTo === 'all' || (this._applyLimitsTo instanceof Set && this._applyLimitsTo.has('default'));
153
+ this._limitSystem = this._applyLimitsTo === 'all' || (this._applyLimitsTo instanceof Set && this._applyLimitsTo.has('system'));
154
+ this._limitDefault = this._applyLimitsTo === 'all' || (this._applyLimitsTo instanceof Set && this._applyLimitsTo.has('default'));
155
155
 
156
156
  // Frozen immutable entry arrays
157
157
  this._defaultEntries = this._defaultTable ? Object.entries(this._defaultTable) : [];
158
- this._systemEntries = this._systemTable ? Object.entries(this._systemTable) : [];
158
+ this._systemEntries = this._systemTable ? Object.entries(this._systemTable) : [];
159
159
 
160
160
  // Persistent external entities — survive across documents
161
161
  /** @type {Array<[string, {regex: RegExp, val: string}]>} */
@@ -167,7 +167,7 @@ export default class EntityReplacer {
167
167
 
168
168
  // Per-document counters — reset in getInstance()
169
169
  this._totalExpansions = 0;
170
- this._expandedLength = 0;
170
+ this._expandedLength = 0;
171
171
  }
172
172
 
173
173
  // -------------------------------------------------------------------------
@@ -215,8 +215,8 @@ export default class EntityReplacer {
215
215
  */
216
216
  addInputEntities(map) {
217
217
  this._totalExpansions = 0;
218
- this._expandedLength = 0;
219
- this._inputEntries = buildEntries(map);
218
+ this._expandedLength = 0;
219
+ this._inputEntries = buildEntries(map);
220
220
  }
221
221
 
222
222
  // -------------------------------------------------------------------------
@@ -233,9 +233,9 @@ export default class EntityReplacer {
233
233
  * @returns {EntityReplacer} `this`, after reset
234
234
  */
235
235
  getInstance() {
236
- this._inputEntries = [];
236
+ this._inputEntries = [];
237
237
  this._totalExpansions = 0;
238
- this._expandedLength = 0;
238
+ this._expandedLength = 0;
239
239
  return this;
240
240
  }
241
241
 
@@ -263,6 +263,7 @@ export default class EntityReplacer {
263
263
 
264
264
  const original = str;
265
265
 
266
+
266
267
  // 1. Persistent external entities
267
268
  if (this._persistentEntries.length > 0) {
268
269
  str = this._applyEntries(str, this._persistentEntries, this._limitExternal);
@@ -273,25 +274,23 @@ export default class EntityReplacer {
273
274
  str = this._applyEntries(str, this._inputEntries, this._limitExternal);
274
275
  }
275
276
 
276
- // 3. System (named groups)
277
- if (this._systemEntries.length > 0 && str.indexOf('&') !== -1) {
278
- str = this._applyEntries(str, this._systemEntries, this._limitSystem);
279
- }
280
-
281
- // 4. Default XML entities (lt / gt / apos / quot)
277
+ // 3. Default XML entities (lt / gt / apos / quot)
282
278
  if (this._defaultEntries.length > 0 && str.indexOf('&') !== -1) {
283
279
  str = this._applyEntries(str, this._defaultEntries, this._limitDefault);
284
280
  }
285
281
 
282
+ // 4. System (named groups)
283
+ if (this._systemEntries.length > 0 && str.indexOf('&') !== -1) {
284
+ str = this._applyEntries(str, this._systemEntries, this._limitSystem);
285
+ }
286
+
286
287
  // 5. &amp; — always last
287
288
  if (this._ampEnabled && str.indexOf('&') !== -1) {
288
289
  str = str.replace(AMP_ENTITY.regex, AMP_ENTITY.val);
289
290
  }
290
291
 
291
292
  // 6. postCheck
292
- if (this._postCheck !== null && str !== original) {
293
- str = this._postCheck(str, original);
294
- }
293
+ str = this._postCheck(str, original);
295
294
 
296
295
  return str;
297
296
  }
@@ -302,8 +301,8 @@ export default class EntityReplacer {
302
301
 
303
302
  _applyEntries(str, entries, track) {
304
303
  const limitExpansions = track && this._maxTotalExpansions > 0;
305
- const limitLength = track && this._maxExpandedLength > 0;
306
- const trackAny = limitExpansions || limitLength;
304
+ const limitLength = track && this._maxExpandedLength > 0;
305
+ const trackAny = limitExpansions || limitLength;
307
306
 
308
307
  for (let i = 0; i < entries.length; i++) {
309
308
  if (str.indexOf('&') === -1) break;
package/src/groups.js CHANGED
@@ -28,6 +28,7 @@ export const COMMON_HTML = {
28
28
  frac12: { regex: /&(frac12|#0*189|#x0*[Bb][Dd]);/g, val: '\u00bd' },
29
29
  frac14: { regex: /&(frac14|#0*188|#x0*[Bb][Cc]);/g, val: '\u00bc' },
30
30
  frac34: { regex: /&(frac34|#0*190|#x0*[Bb][Ee]);/g, val: '\u00be' },
31
+ inr: { regex: /&(inr|#0*8377);/g, val: "₹" },
31
32
  };
32
33
 
33
34
  /**
@@ -90,10 +91,20 @@ export const ARROW_ENTITIES = {
90
91
  export const NUMERIC_ENTITIES = {
91
92
  num_dec: {
92
93
  regex: /&#0*([0-9]{1,7});/g,
93
- val: (_, s) => String.fromCodePoint(Number.parseInt(s, 10)),
94
+ val: (_, s) => fromCodePoint(s, 10, "&#"),
94
95
  },
95
96
  num_hex: {
96
97
  regex: /&#x0*([0-9a-fA-F]{1,6});/g,
97
- val: (_, s) => String.fromCodePoint(Number.parseInt(s, 16)),
98
+ val: (_, s) => fromCodePoint(s, 16, "&#x"),
98
99
  },
99
- };
100
+ };
101
+
102
+ function fromCodePoint(str, base, prefix) {
103
+ const codePoint = Number.parseInt(str, base);
104
+
105
+ if (codePoint >= 0 && codePoint <= 0x10FFFF) {
106
+ return String.fromCodePoint(codePoint);
107
+ } else {
108
+ return prefix + str + ";";
109
+ }
110
+ }