@nodable/entities 1.0.0 → 1.1.0

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
@@ -4,7 +4,7 @@ Standalone, zero-dependency XML/HTML entity replacement with:
4
4
 
5
5
  - **5 entity categories** processed in a fixed, predictable order
6
6
  - **Persistent vs. input entity separation** — no state leaks between documents
7
- - **`getInstance()`** — clean per-document reset without cloning
7
+ - **`reset()`** — clean per-document reset without cloning
8
8
  - **Composable named entity groups** (HTML, currency, math, arrows, numeric refs)
9
9
  - **Security limits** — cap total expansions and expanded length per document
10
10
  - **Granular limit targeting** — apply limits to any subset of categories
@@ -52,12 +52,12 @@ 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
59
59
 
60
- Entities set at configuration time that survive across all documents. Never wiped by `getInstance()`. Set via `setExternalEntities()` or `addExternalEntity()` / `addEntity()`.
60
+ Entities set at configuration time that survive across all documents. Never wiped by `reset()`. Set via `setExternalEntities()` or `addExternalEntity()` / `addEntity()`.
61
61
 
62
62
  ```js
63
63
  const replacer = new EntityReplacer({ default: true });
@@ -68,7 +68,7 @@ replacer.replace('&brand; makes &product;');
68
68
 
69
69
  ### `input / runtime` — Per-document DOCTYPE entities
70
70
 
71
- Entities injected by the parser from the document's DOCTYPE block. Stored separately from persistent entities and **wiped on every `getInstance()` call** so they cannot leak between documents.
71
+ Entities injected by the parser from the document's DOCTYPE block. Stored separately from persistent entities and **wiped on every `reset()` call** so they cannot leak between documents.
72
72
 
73
73
  Set via `addInputEntities()`. Never call this manually — `BaseOutputBuilder` calls it automatically.
74
74
 
@@ -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
 
@@ -155,7 +155,7 @@ replacer.replace('Tom & Jerry <cartoons>');
155
155
 
156
156
  ### `setExternalEntities(map)`
157
157
 
158
- Replace the full set of **persistent** external entities. These survive across all documents and are not cleared by `getInstance()`.
158
+ Replace the full set of **persistent** external entities. These survive across all documents and are not cleared by `reset()`.
159
159
 
160
160
  ```js
161
161
  replacer.setExternalEntities({ brand: 'Acme', year: '2025' });
@@ -174,7 +174,7 @@ replacer.addExternalEntity('year', '2025');
174
174
 
175
175
  ### `addInputEntities(map)`
176
176
 
177
- Inject **input/runtime** (DOCTYPE) entities for the current document. These are stored separately from persistent entities and wiped on the next `getInstance()` call. Also resets per-document expansion counters.
177
+ Inject **input/runtime** (DOCTYPE) entities for the current document. These are stored separately from persistent entities and wiped on the next `reset()` call. Also resets per-document expansion counters.
178
178
 
179
179
  ```js
180
180
  // Called automatically by BaseOutputBuilder — no manual wiring needed.
@@ -183,7 +183,7 @@ replacer.addInputEntities(doctypeEntityMap);
183
183
 
184
184
  Values containing `&` are silently skipped. Accepts pre-built `{ regex, val }` or `{ regx, val }` objects as produced by `DocTypeReader`.
185
185
 
186
- ### `getInstance()`
186
+ ### `reset()`
187
187
 
188
188
  Reset all per-document state and return `this`.
189
189
 
@@ -200,9 +200,9 @@ The builder factory calls this when creating a new builder instance, ensuring ea
200
200
 
201
201
  ```js
202
202
  // In a builder factory:
203
- getInstance() {
203
+ reset() {
204
204
  const builder = new MyBuilder(this.config);
205
- builder.entityParser = this.entityVP.getInstance();
205
+ builder.entityParser = this.entityVP.reset();
206
206
  return builder;
207
207
  }
208
208
  ```
@@ -215,12 +215,12 @@ A key design goal is that entities from one document never bleed into the next.
215
215
 
216
216
  ```
217
217
  Document 1 parse:
218
- factory.getInstance() → evp.getInstance() [clears input, resets counters]
218
+ factory.reset() → evp.reset() [clears input, resets counters]
219
219
  builder sees DOCTYPE → evp.addInputEntities({ version: '1.0' })
220
220
  builder processes values → evp.parse('&brand; v&version;') → 'Acme v1.0'
221
221
 
222
222
  Document 2 parse (no DOCTYPE):
223
- factory.getInstance() → evp.getInstance() [clears &version;, resets counters]
223
+ factory.reset() → evp.reset() [clears &version;, resets counters]
224
224
  no DOCTYPE → addInputEntities() not called
225
225
  builder processes values → evp.parse('&brand; v&version;') → 'Acme v&version;'
226
226
  ↑ persistent &brand; works
@@ -300,16 +300,14 @@ postCheck: (resolved) =>
300
300
 
301
301
  ---
302
302
 
303
- ## `EntitiesValueParser` — flex-xml-parser adapter
304
-
305
- `EntitiesValueParser` wraps `EntityReplacer` and implements the `ValueParser` interface used by `@nodable/flexible-xml-parser`.
303
+ ## Integration with — flex-xml-parser adapter
306
304
 
307
305
  ### Setup
308
306
 
309
307
  ```js
310
- import { EntitiesValueParser, COMMON_HTML } from '@nodable/entities';
308
+ import EntityReplacer, { COMMON_HTML } from '@nodable/entities';
311
309
 
312
- const evp = new EntitiesValueParser({
310
+ const evp = new EntityReplacer({
313
311
  system: COMMON_HTML,
314
312
  maxTotalExpansions: 500,
315
313
  });
@@ -329,7 +327,7 @@ parser.parse(xml);
329
327
  All `EntityReplacerOptions` are accepted, plus one extra:
330
328
 
331
329
  ```js
332
- new EntitiesValueParser({
330
+ new EntityReplacer({
333
331
  // All EntityReplacer options...
334
332
  default: true,
335
333
  system: COMMON_HTML,
@@ -341,48 +339,20 @@ new EntitiesValueParser({
341
339
  })
342
340
  ```
343
341
 
344
- ### `setExternalEntities(map)`
345
-
346
- Replace the full persistent entity map. These entities survive across all documents.
347
-
348
- ```js
349
- evp.setExternalEntities({ brand: 'Acme', copy: '©' });
350
- ```
351
-
352
- ### `addEntity(key, value)`
353
-
354
- Append a single persistent external entity. Previously registered entities are preserved.
355
-
356
- ```js
357
- evp.addEntity('copy', '©');
358
- evp.addEntity('trade', '™');
359
- evp.addEntity('year', '2024');
360
- ```
361
-
362
- Throws if `key` contains `&` or `;`, or if `value` contains `&`.
363
-
364
- ### `getInstance()` — called by builder factory
342
+ ### `reset()` — called by builder factory
365
343
 
366
344
  Reset per-document state (input entities + counters) and return `this`. The builder factory calls this each time it creates a new builder instance.
367
345
 
368
346
  ```js
369
- // In your CompactObjBuilderFactory.getInstance():
370
- getInstance() {
347
+ // In your CompactObjBuilderFactory.reset():
348
+ reset() {
371
349
  const builder = new CompactObjBuilder(this._config);
372
350
  // Reset EVP for the new document:
373
- builder.entityParser = this._entityVP.getInstance();
351
+ builder.entityParser = this._entityVP.reset();
374
352
  return builder;
375
353
  }
376
354
  ```
377
355
 
378
- ### `addInputEntities(entities)` — called automatically
379
-
380
- Receives the DOCTYPE entity map from `BaseOutputBuilder` once per parse. Resets per-document expansion counters. Accepts both plain string values and `{ regx, val }` objects from `DocTypeReader`.
381
-
382
- ### `parse(val, context?)`
383
-
384
- Implements the `ValueParser` interface. `context` is accepted but ignored. Returns non-string input unchanged.
385
-
386
356
  ---
387
357
 
388
358
  ## Custom Entity Tables
@@ -421,7 +391,7 @@ const replacer = new EntityReplacer({
421
391
  | Numeric refs with leading zeros | ✅ | ✅ |
422
392
  | DOCTYPE / external entity injection | ❌ | ✅ |
423
393
  | Persistent vs. input entity separation | ❌ | ✅ |
424
- | Per-document reset via `getInstance()` | ❌ | ✅ |
394
+ | Per-document reset via `reset()` | ❌ | ✅ |
425
395
  | Expansion count limit | ❌ | ✅ |
426
396
  | Expanded length limit | ❌ | ✅ |
427
397
  | `applyLimitsTo` granularity | ❌ | ✅ |
@@ -437,11 +407,9 @@ Full TypeScript declarations are included via `index.d.ts`. No `@types/` package
437
407
 
438
408
  ```ts
439
409
  import EntityReplacer, {
440
- EntitiesValueParser,
441
410
  COMMON_HTML,
442
411
  EntityTable,
443
412
  EntityReplacerOptions,
444
- EntitiesValueParserOptions,
445
413
  } from '@nodable/entities';
446
414
 
447
415
  // EntityReplacer
@@ -454,22 +422,13 @@ const opts: EntityReplacerOptions = {
454
422
  };
455
423
  const replacer = new EntityReplacer(opts);
456
424
  replacer.setExternalEntities({ brand: 'Acme' });
457
- replacer.getInstance(); // reset for new document
425
+ replacer.reset(); // reset for new document
458
426
  replacer.addInputEntities({ version: '1.0' }); // from DOCTYPE
459
-
460
- // EntitiesValueParser
461
- const evpOpts: EntitiesValueParserOptions = {
462
- system: COMMON_HTML,
463
- entities: { brand: 'Acme' },
464
- };
465
- const evp = new EntitiesValueParser(evpOpts);
466
- evp.addEntity('copy', '©');
467
- evp.getInstance(); // called by builder factory
468
- evp.addInputEntities({ company: 'Nodable' }); // called by BaseOutputBuilder
469
- const result: string = evp.parse('<©&brand;');
470
427
  ```
471
428
 
429
+ ## Note
472
430
 
431
+ This library silently skip numeric entities which are out range. For example `�` is skipped.
473
432
 
474
433
  ## License
475
434
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nodable/entities",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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
  // -------------------------------------------------------------------------
@@ -230,13 +230,11 @@ export default class EntityReplacer {
230
230
  * The builder factory calls this each time it creates a new builder instance
231
231
  * so DOCTYPE entities from a previous document are never carried over.
232
232
  *
233
- * @returns {EntityReplacer} `this`, after reset
234
233
  */
235
- getInstance() {
236
- this._inputEntries = [];
234
+ reset() {
235
+ this._inputEntries = [];
237
236
  this._totalExpansions = 0;
238
- this._expandedLength = 0;
239
- return this;
237
+ this._expandedLength = 0;
240
238
  }
241
239
 
242
240
  // -------------------------------------------------------------------------
@@ -263,6 +261,7 @@ export default class EntityReplacer {
263
261
 
264
262
  const original = str;
265
263
 
264
+
266
265
  // 1. Persistent external entities
267
266
  if (this._persistentEntries.length > 0) {
268
267
  str = this._applyEntries(str, this._persistentEntries, this._limitExternal);
@@ -273,37 +272,44 @@ export default class EntityReplacer {
273
272
  str = this._applyEntries(str, this._inputEntries, this._limitExternal);
274
273
  }
275
274
 
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)
275
+ // 3. Default XML entities (lt / gt / apos / quot)
282
276
  if (this._defaultEntries.length > 0 && str.indexOf('&') !== -1) {
283
277
  str = this._applyEntries(str, this._defaultEntries, this._limitDefault);
284
278
  }
285
279
 
280
+ // 4. System (named groups)
281
+ if (this._systemEntries.length > 0 && str.indexOf('&') !== -1) {
282
+ str = this._applyEntries(str, this._systemEntries, this._limitSystem);
283
+ }
284
+
286
285
  // 5. &amp; — always last
287
286
  if (this._ampEnabled && str.indexOf('&') !== -1) {
288
287
  str = str.replace(AMP_ENTITY.regex, AMP_ENTITY.val);
289
288
  }
290
289
 
291
290
  // 6. postCheck
292
- if (this._postCheck !== null && str !== original) {
293
- str = this._postCheck(str, original);
294
- }
291
+ str = this._postCheck(str, original);
295
292
 
296
293
  return str;
297
294
  }
298
295
 
296
+
297
+ /**
298
+ *
299
+ * @param {string} val
300
+ * @returns
301
+ */
302
+ parse(val) {
303
+ return this.replace(val);
304
+ }
299
305
  // -------------------------------------------------------------------------
300
306
  // Private helpers
301
307
  // -------------------------------------------------------------------------
302
308
 
303
309
  _applyEntries(str, entries, track) {
304
310
  const limitExpansions = track && this._maxTotalExpansions > 0;
305
- const limitLength = track && this._maxExpandedLength > 0;
306
- const trackAny = limitExpansions || limitLength;
311
+ const limitLength = track && this._maxExpandedLength > 0;
312
+ const trackAny = limitExpansions || limitLength;
307
313
 
308
314
  for (let i = 0; i < entries.length; i++) {
309
315
  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
+ }
package/src/index.d.ts CHANGED
@@ -212,9 +212,8 @@ export default class EntityReplacer {
212
212
  * The builder factory calls this when creating a new builder instance,
213
213
  * ensuring each document starts clean regardless of whether it has a DOCTYPE.
214
214
  *
215
- * @returns `this` — for convenient chaining in factory code
216
215
  */
217
- getInstance(): this;
216
+ reset(): this;
218
217
 
219
218
  // -------------------------------------------------------------------------
220
219
  // Primary API
@@ -225,26 +224,18 @@ export default class EntityReplacer {
225
224
  * Returns `str` unchanged if it contains no `&` character (fast path).
226
225
  */
227
226
  replace(str: string): string;
227
+
228
+ /**
229
+ * wrapper on replace()
230
+ */
231
+ parse(str: string): string;
228
232
  }
229
233
 
230
234
  // ---------------------------------------------------------------------------
231
235
  // EntitiesValueParser
232
236
  // ---------------------------------------------------------------------------
233
237
 
234
- /**
235
- * Options accepted by `EntitiesValueParser` — a superset of `EntityReplacerOptions`.
236
- */
237
- export interface EntitiesValueParserOptions extends EntityReplacerOptions {
238
- /**
239
- * Initial persistent external entity map loaded at construction time.
240
- * Values must not contain `&` (to prevent recursive expansion).
241
- * Equivalent to calling `setExternalEntities()` after construction.
242
- *
243
- * @example
244
- * new EntitiesValueParser({ entities: { copy: '©', trade: '™' } })
245
- */
246
- entities?: Record<string, string>;
247
- }
238
+
248
239
 
249
240
  /**
250
241
  * Raw DOCTYPE entity map shape as produced by `DocTypeReader`.
@@ -270,112 +261,6 @@ export interface ValueParserContext {
270
261
  isLeafNode?: boolean;
271
262
  }
272
263
 
273
- /**
274
- * `EntitiesValueParser` — value-parser adapter that wraps `EntityReplacer`
275
- * for use with `@nodable/flexible-xml-parser`.
276
- *
277
- * ## Setup
278
- *
279
- * ```ts
280
- * import { EntitiesValueParser, COMMON_HTML } from '@nodable/entities';
281
- *
282
- * const evp = new EntitiesValueParser({ system: COMMON_HTML });
283
- *
284
- * // Persistent entities — never wiped between documents:
285
- * evp.setExternalEntities({ brand: 'Acme', product: 'Widget' });
286
- *
287
- * // Register with the builder factory:
288
- * builder.registerValueParser('entity', evp);
289
- *
290
- * const parser = new XMLParser({ OutputBuilder: builder });
291
- * parser.parse(xml);
292
- * ```
293
- *
294
- * ## Lifecycle (called automatically by the builder / parser)
295
- *
296
- * | Caller | Method | When |
297
- * |-----------------|----------------------|-------------------------------------------|
298
- * | Builder factory | `getInstance()` | Before each `parse()` call |
299
- * | Builder | `addInputEntities()` | After DOCTYPE is read (if present) |
300
- * | Builder | `parse(val)` | For each text / attribute value |
301
- */
302
- export class EntitiesValueParser {
303
- constructor(options?: EntitiesValueParserOptions);
304
-
305
- // -------------------------------------------------------------------------
306
- // Persistent external entity registration
307
- // -------------------------------------------------------------------------
308
-
309
- /**
310
- * Replace the full set of persistent external entities.
311
- *
312
- * These survive across all documents and are **not** cleared by
313
- * `getInstance()`. Call this once after construction (or at any time to
314
- * swap the entire persistent entity map).
315
- *
316
- * @throws if any value contains `&`
317
- */
318
- setExternalEntities(map: Record<string, string>): void;
319
-
320
- /**
321
- * Append a single persistent external entity.
322
- *
323
- * Provide the bare name without `&` and `;` — e.g. `'copy'` for `&copy;`.
324
- * Existing persistent entities are preserved.
325
- *
326
- * @throws if `key` contains `&` or `;`
327
- * @throws if `value` is not a string or contains `&`
328
- */
329
- addEntity(key: string, value: string): void;
330
-
331
- // -------------------------------------------------------------------------
332
- // Builder factory integration
333
- // -------------------------------------------------------------------------
334
-
335
- /**
336
- * Reset per-document state and return `this`.
337
- *
338
- * Clears input/runtime entities (DOCTYPE) and resets expansion counters.
339
- * Does **not** clear persistent external entities.
340
- *
341
- * The builder factory calls this when creating a new builder instance.
342
- *
343
- * @returns `this`
344
- */
345
- getInstance(): this;
346
-
347
- // -------------------------------------------------------------------------
348
- // DOCTYPE integration — called automatically by BaseOutputBuilder
349
- // -------------------------------------------------------------------------
350
-
351
- /**
352
- * Receive DOCTYPE entities for the current document.
353
- *
354
- * Called automatically by `BaseOutputBuilder`. Stores entities separately
355
- * from persistent entities so they are wiped on the next `getInstance()`.
356
- * Also resets per-document expansion counters.
357
- *
358
- * Accepts both plain string values and `{ regx, val }` / `{ regex, val }`
359
- * objects as produced by `DocTypeReader`.
360
- */
361
- addInputEntities(entities: DocTypeEntityMap): void;
362
-
363
- // -------------------------------------------------------------------------
364
- // ValueParser interface
365
- // -------------------------------------------------------------------------
366
-
367
- /**
368
- * Replace entity references in `val`.
369
- *
370
- * Implements the `ValueParser` interface. The `context` argument is
371
- * accepted but ignored — replacement is applied uniformly to all values.
372
- *
373
- * Returns non-string input unchanged.
374
- */
375
- parse(val: string, context?: ValueParserContext): string;
376
- parse(val: unknown, context?: ValueParserContext): unknown;
377
- }
378
-
379
264
  // ---------------------------------------------------------------------------
380
265
  // Named entity group exports
381
266
  // ---------------------------------------------------------------------------
package/src/index.js CHANGED
@@ -17,7 +17,6 @@
17
17
 
18
18
  export { default } from './EntityReplacer.js';
19
19
  export { DEFAULT_XML_ENTITIES, AMP_ENTITY } from './EntityReplacer.js';
20
- export { default as EntitiesValueParser } from './EntitiesValueParser.js';
21
20
  export {
22
21
  COMMON_HTML,
23
22
  CURRENCY_ENTITIES,
@@ -1,152 +0,0 @@
1
- import EntityReplacer from './EntityReplacer.js';
2
-
3
- /**
4
- * EntitiesValueParser — value-parser adapter that wraps `EntityReplacer`.
5
- *
6
- * Register an instance under the key `'entity'` on a `@nodable/flexible-xml-parser`
7
- * output builder factory to enable entity expansion for all parsed text values.
8
- *
9
- * ## Lifecycle
10
- *
11
- * 1. **Construction** — supply configuration and optional persistent entities.
12
- * 2. **`setExternalEntities(map)`** — (re)set the full persistent entity map.
13
- * Or use `addEntity(key, value)` to add one at a time.
14
- * 3. **`getInstance()`** — builder factory calls this when creating a new builder
15
- * instance. Resets input entities and per-document counters. Returns `this`.
16
- * 4. **`addInputEntities(map)`** — builder calls this if the document has a
17
- * DOCTYPE block. Stores entities for *this document only*.
18
- * 5. **`parse(val)`** — called by the builder for each text value.
19
- *
20
- * ```js
21
- * const evp = new EntitiesValueParser({ system: COMMON_HTML });
22
- * evp.setExternalEntities({ brand: 'Acme' });
23
- * builder.registerValueParser('entity', evp);
24
- * ```
25
- *
26
- * -------------------------------------------------------------------------
27
- * Constructor options (all optional)
28
- * -------------------------------------------------------------------------
29
- *
30
- * `default` — `true` (default) | `false`/`null` | custom EntityTable
31
- * `system` — `false` (default) | `true` for COMMON_HTML | EntityTable
32
- * `amp` — `true` (default) | `false`/`null`
33
- * `maxTotalExpansions` — max entity refs expanded per document (0 = unlimited)
34
- * `maxExpandedLength` — max characters added by expansion per document (0 = unlimited)
35
- * `applyLimitsTo` — which categories count toward limits (default: `'external'`)
36
- * `postCheck` — `(resolved, original) => string` hook
37
- * `entities` — initial persistent entity map, e.g. `{ copy: '©' }`
38
- */
39
- export default class EntitiesValueParser {
40
- constructor(options = {}) {
41
- this._replacer = new EntityReplacer(options);
42
-
43
- // Load any entities provided inline at construction time as persistent entities
44
- if (options.entities && typeof options.entities === 'object') {
45
- const init = {};
46
- for (const [key, val] of Object.entries(options.entities)) {
47
- this._validateEntityArgs(key, val);
48
- init[key] = val;
49
- }
50
- this._replacer.setExternalEntities(init);
51
- }
52
- }
53
-
54
- // -------------------------------------------------------------------------
55
- // Persistent external entity registration
56
- // -------------------------------------------------------------------------
57
-
58
- /**
59
- * Replace the full set of persistent external entities.
60
- * These survive across documents and are never wiped by `getInstance()`.
61
- *
62
- * @param {Record<string, string>} map — e.g. `{ copy: '©', brand: 'Acme' }`
63
- */
64
- setExternalEntities(map) {
65
- for (const [key, val] of Object.entries(map)) {
66
- this._validateEntityArgs(key, val);
67
- }
68
- this._replacer.setExternalEntities(map);
69
- }
70
-
71
- /**
72
- * Add (or replace) a single persistent external entity.
73
- * Existing persistent entities are preserved.
74
- *
75
- * @param {string} key — bare name without `&` / `;`, e.g. `'copy'`
76
- * @param {string} value — replacement string, e.g. `'©'`
77
- */
78
- addEntity(key, value) {
79
- this._validateEntityArgs(key, value);
80
- this._replacer.addExternalEntity(key, value);
81
- }
82
-
83
- // -------------------------------------------------------------------------
84
- // Builder factory integration
85
- // -------------------------------------------------------------------------
86
-
87
- /**
88
- * Reset per-document state (input entities + expansion counters) and return `this`.
89
- *
90
- * The builder factory calls this when creating a new builder instance so that
91
- * DOCTYPE entities from a previous document are never carried over.
92
- *
93
- * @returns {EntitiesValueParser} `this`
94
- */
95
- getInstance() {
96
- this._replacer.getInstance();
97
- return this;
98
- }
99
-
100
- // -------------------------------------------------------------------------
101
- // DOCTYPE integration — called by BaseOutputBuilder
102
- // -------------------------------------------------------------------------
103
-
104
- /**
105
- * Receive DOCTYPE entities from the output builder.
106
- *
107
- * These are stored separately from persistent entities and wiped on the next
108
- * `getInstance()` call. Resets per-document expansion counters.
109
- *
110
- * @param {Record<string, string | { regx: RegExp, val: string | Function }>} entities
111
- * Raw entity map from `DocTypeReader` — values may be plain strings or
112
- * `{ regx, val }` objects (note: `regx`, not `regex`, matching the reader's output).
113
- */
114
- addInputEntities(entities) {
115
- this._replacer.addInputEntities(entities);
116
- }
117
-
118
- // -------------------------------------------------------------------------
119
- // ValueParser interface
120
- // -------------------------------------------------------------------------
121
-
122
- /**
123
- * Replace entity references in `val`.
124
- *
125
- * @param {string} val
126
- * @param {object} [_context]
127
- * @returns {string}
128
- */
129
- parse(val, _context) {
130
- if (typeof val !== 'string') return val;
131
- return this._replacer.replace(val);
132
- }
133
-
134
- // -------------------------------------------------------------------------
135
- // Private helpers
136
- // -------------------------------------------------------------------------
137
-
138
- _validateEntityArgs(key, value) {
139
- if (typeof key !== 'string' || key.includes('&') || key.includes(';')) {
140
- throw new Error(
141
- `[EntitiesValueParser] Entity key must not contain '&' or ';'. ` +
142
- `Use 'copy' for '&copy;', got: ${JSON.stringify(key)}`
143
- );
144
- }
145
- if (typeof value !== 'string' || value.includes('&')) {
146
- throw new Error(
147
- `[EntitiesValueParser] Entity value must be a plain string that does not ` +
148
- `contain '&', got: ${JSON.stringify(value)}`
149
- );
150
- }
151
- }
152
- }