@nodable/entities
Advanced tools
+1
-1
| { | ||
| "name": "@nodable/entities", | ||
| "version": "1.0.1", | ||
| "version": "1.1.0", | ||
| "description": "Replace XML, HTML, External entites with security controls", | ||
@@ -5,0 +5,0 @@ "main": "./src/index.js", |
+20
-63
@@ -7,3 +7,3 @@ # `@nodable/entities` | ||
| - **Persistent vs. input entity separation** — no state leaks between documents | ||
| - **`getInstance()`** — clean per-document reset without cloning | ||
| - **`reset()`** — clean per-document reset without cloning | ||
| - **Composable named entity groups** (HTML, currency, math, arrows, numeric refs) | ||
@@ -61,3 +61,3 @@ - **Security limits** — cap total expansions and expanded length per document | ||
| Entities set at configuration time that survive across all documents. Never wiped by `getInstance()`. Set via `setExternalEntities()` or `addExternalEntity()` / `addEntity()`. | ||
| Entities set at configuration time that survive across all documents. Never wiped by `reset()`. Set via `setExternalEntities()` or `addExternalEntity()` / `addEntity()`. | ||
@@ -73,3 +73,3 @@ ```js | ||
| 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. | ||
| 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. | ||
@@ -161,3 +161,3 @@ Set via `addInputEntities()`. Never call this manually — `BaseOutputBuilder` calls it automatically. | ||
| Replace the full set of **persistent** external entities. These survive across all documents and are not cleared by `getInstance()`. | ||
| Replace the full set of **persistent** external entities. These survive across all documents and are not cleared by `reset()`. | ||
@@ -181,3 +181,3 @@ ```js | ||
| 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. | ||
| 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. | ||
@@ -191,3 +191,3 @@ ```js | ||
| ### `getInstance()` | ||
| ### `reset()` | ||
@@ -209,5 +209,5 @@ Reset all per-document state and return `this`. | ||
| // In a builder factory: | ||
| getInstance() { | ||
| reset() { | ||
| const builder = new MyBuilder(this.config); | ||
| builder.entityParser = this.entityVP.getInstance(); | ||
| builder.entityParser = this.entityVP.reset(); | ||
| return builder; | ||
@@ -225,3 +225,3 @@ } | ||
| Document 1 parse: | ||
| factory.getInstance() → evp.getInstance() [clears input, resets counters] | ||
| factory.reset() → evp.reset() [clears input, resets counters] | ||
| builder sees DOCTYPE → evp.addInputEntities({ version: '1.0' }) | ||
@@ -231,3 +231,3 @@ builder processes values → evp.parse('&brand; v&version;') → 'Acme v1.0' | ||
| Document 2 parse (no DOCTYPE): | ||
| factory.getInstance() → evp.getInstance() [clears &version;, resets counters] | ||
| factory.reset() → evp.reset() [clears &version;, resets counters] | ||
| no DOCTYPE → addInputEntities() not called | ||
@@ -312,12 +312,10 @@ builder processes values → evp.parse('&brand; v&version;') → 'Acme v&version;' | ||
| ## `EntitiesValueParser` — flex-xml-parser adapter | ||
| ## Integration with — flex-xml-parser adapter | ||
| `EntitiesValueParser` wraps `EntityReplacer` and implements the `ValueParser` interface used by `@nodable/flexible-xml-parser`. | ||
| ### Setup | ||
| ```js | ||
| import { EntitiesValueParser, COMMON_HTML } from '@nodable/entities'; | ||
| import EntityReplacer, { COMMON_HTML } from '@nodable/entities'; | ||
| const evp = new EntitiesValueParser({ | ||
| const evp = new EntityReplacer({ | ||
| system: COMMON_HTML, | ||
@@ -342,3 +340,3 @@ maxTotalExpansions: 500, | ||
| ```js | ||
| new EntitiesValueParser({ | ||
| new EntityReplacer({ | ||
| // All EntityReplacer options... | ||
@@ -355,32 +353,12 @@ default: true, | ||
| ### `setExternalEntities(map)` | ||
| ### `reset()` — called by builder factory | ||
| Replace the full persistent entity map. These entities survive across all documents. | ||
| ```js | ||
| evp.setExternalEntities({ brand: 'Acme', copy: '©' }); | ||
| ``` | ||
| ### `addEntity(key, value)` | ||
| Append a single persistent external entity. Previously registered entities are preserved. | ||
| ```js | ||
| evp.addEntity('copy', '©'); | ||
| evp.addEntity('trade', '™'); | ||
| evp.addEntity('year', '2024'); | ||
| ``` | ||
| Throws if `key` contains `&` or `;`, or if `value` contains `&`. | ||
| ### `getInstance()` — called by builder factory | ||
| Reset per-document state (input entities + counters) and return `this`. The builder factory calls this each time it creates a new builder instance. | ||
| ```js | ||
| // In your CompactObjBuilderFactory.getInstance(): | ||
| getInstance() { | ||
| // In your CompactObjBuilderFactory.reset(): | ||
| reset() { | ||
| const builder = new CompactObjBuilder(this._config); | ||
| // Reset EVP for the new document: | ||
| builder.entityParser = this._entityVP.getInstance(); | ||
| builder.entityParser = this._entityVP.reset(); | ||
| return builder; | ||
@@ -390,10 +368,2 @@ } | ||
| ### `addInputEntities(entities)` — called automatically | ||
| 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`. | ||
| ### `parse(val, context?)` | ||
| Implements the `ValueParser` interface. `context` is accepted but ignored. Returns non-string input unchanged. | ||
| --- | ||
@@ -437,3 +407,3 @@ | ||
| | Persistent vs. input entity separation | ❌ | ✅ | | ||
| | Per-document reset via `getInstance()` | ❌ | ✅ | | ||
| | Per-document reset via `reset()` | ❌ | ✅ | | ||
| | Expansion count limit | ❌ | ✅ | | ||
@@ -454,7 +424,5 @@ | Expanded length limit | ❌ | ✅ | | ||
| import EntityReplacer, { | ||
| EntitiesValueParser, | ||
| COMMON_HTML, | ||
| EntityTable, | ||
| EntityReplacerOptions, | ||
| EntitiesValueParserOptions, | ||
| } from '@nodable/entities'; | ||
@@ -472,15 +440,4 @@ | ||
| replacer.setExternalEntities({ brand: 'Acme' }); | ||
| replacer.getInstance(); // reset for new document | ||
| replacer.reset(); // reset for new document | ||
| replacer.addInputEntities({ version: '1.0' }); // from DOCTYPE | ||
| // EntitiesValueParser | ||
| const evpOpts: EntitiesValueParserOptions = { | ||
| system: COMMON_HTML, | ||
| entities: { brand: 'Acme' }, | ||
| }; | ||
| const evp = new EntitiesValueParser(evpOpts); | ||
| evp.addEntity('copy', '©'); | ||
| evp.getInstance(); // called by builder factory | ||
| evp.addInputEntities({ company: 'Nodable' }); // called by BaseOutputBuilder | ||
| const result: string = evp.parse('<©&brand;'); | ||
| ``` | ||
@@ -487,0 +444,0 @@ |
@@ -233,9 +233,7 @@ // --------------------------------------------------------------------------- | ||
| * | ||
| * @returns {EntityReplacer} `this`, after reset | ||
| */ | ||
| getInstance() { | ||
| reset() { | ||
| this._inputEntries = []; | ||
| this._totalExpansions = 0; | ||
| this._expandedLength = 0; | ||
| return this; | ||
| } | ||
@@ -299,2 +297,11 @@ | ||
| /** | ||
| * | ||
| * @param {string} val | ||
| * @returns | ||
| */ | ||
| parse(val) { | ||
| return this.replace(val); | ||
| } | ||
| // ------------------------------------------------------------------------- | ||
@@ -301,0 +308,0 @@ // Private helpers |
+7
-122
@@ -215,5 +215,4 @@ // --------------------------------------------------------------------------- | ||
| * | ||
| * @returns `this` — for convenient chaining in factory code | ||
| */ | ||
| getInstance(): this; | ||
| reset(): this; | ||
@@ -229,2 +228,7 @@ // ------------------------------------------------------------------------- | ||
| replace(str: string): string; | ||
| /** | ||
| * wrapper on replace() | ||
| */ | ||
| parse(str: string): string; | ||
| } | ||
@@ -236,17 +240,4 @@ | ||
| /** | ||
| * Options accepted by `EntitiesValueParser` — a superset of `EntityReplacerOptions`. | ||
| */ | ||
| export interface EntitiesValueParserOptions extends EntityReplacerOptions { | ||
| /** | ||
| * Initial persistent external entity map loaded at construction time. | ||
| * Values must not contain `&` (to prevent recursive expansion). | ||
| * Equivalent to calling `setExternalEntities()` after construction. | ||
| * | ||
| * @example | ||
| * new EntitiesValueParser({ entities: { copy: '©', trade: '™' } }) | ||
| */ | ||
| entities?: Record<string, string>; | ||
| } | ||
| /** | ||
@@ -276,108 +267,2 @@ * Raw DOCTYPE entity map shape as produced by `DocTypeReader`. | ||
| /** | ||
| * `EntitiesValueParser` — value-parser adapter that wraps `EntityReplacer` | ||
| * for use with `@nodable/flexible-xml-parser`. | ||
| * | ||
| * ## Setup | ||
| * | ||
| * ```ts | ||
| * import { EntitiesValueParser, COMMON_HTML } from '@nodable/entities'; | ||
| * | ||
| * const evp = new EntitiesValueParser({ system: COMMON_HTML }); | ||
| * | ||
| * // Persistent entities — never wiped between documents: | ||
| * evp.setExternalEntities({ brand: 'Acme', product: 'Widget' }); | ||
| * | ||
| * // Register with the builder factory: | ||
| * builder.registerValueParser('entity', evp); | ||
| * | ||
| * const parser = new XMLParser({ OutputBuilder: builder }); | ||
| * parser.parse(xml); | ||
| * ``` | ||
| * | ||
| * ## Lifecycle (called automatically by the builder / parser) | ||
| * | ||
| * | Caller | Method | When | | ||
| * |-----------------|----------------------|-------------------------------------------| | ||
| * | Builder factory | `getInstance()` | Before each `parse()` call | | ||
| * | Builder | `addInputEntities()` | After DOCTYPE is read (if present) | | ||
| * | Builder | `parse(val)` | For each text / attribute value | | ||
| */ | ||
| export class EntitiesValueParser { | ||
| constructor(options?: EntitiesValueParserOptions); | ||
| // ------------------------------------------------------------------------- | ||
| // Persistent external entity registration | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Replace the full set of persistent external entities. | ||
| * | ||
| * These survive across all documents and are **not** cleared by | ||
| * `getInstance()`. Call this once after construction (or at any time to | ||
| * swap the entire persistent entity map). | ||
| * | ||
| * @throws if any value contains `&` | ||
| */ | ||
| setExternalEntities(map: Record<string, string>): void; | ||
| /** | ||
| * Append a single persistent external entity. | ||
| * | ||
| * Provide the bare name without `&` and `;` — e.g. `'copy'` for `©`. | ||
| * Existing persistent entities are preserved. | ||
| * | ||
| * @throws if `key` contains `&` or `;` | ||
| * @throws if `value` is not a string or contains `&` | ||
| */ | ||
| addEntity(key: string, value: string): void; | ||
| // ------------------------------------------------------------------------- | ||
| // Builder factory integration | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Reset per-document state and return `this`. | ||
| * | ||
| * Clears input/runtime entities (DOCTYPE) and resets expansion counters. | ||
| * Does **not** clear persistent external entities. | ||
| * | ||
| * The builder factory calls this when creating a new builder instance. | ||
| * | ||
| * @returns `this` | ||
| */ | ||
| getInstance(): this; | ||
| // ------------------------------------------------------------------------- | ||
| // DOCTYPE integration — called automatically by BaseOutputBuilder | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Receive DOCTYPE entities for the current document. | ||
| * | ||
| * Called automatically by `BaseOutputBuilder`. Stores entities separately | ||
| * from persistent entities so they are wiped on the next `getInstance()`. | ||
| * Also resets per-document expansion counters. | ||
| * | ||
| * Accepts both plain string values and `{ regx, val }` / `{ regex, val }` | ||
| * objects as produced by `DocTypeReader`. | ||
| */ | ||
| addInputEntities(entities: DocTypeEntityMap): void; | ||
| // ------------------------------------------------------------------------- | ||
| // ValueParser interface | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Replace entity references in `val`. | ||
| * | ||
| * Implements the `ValueParser` interface. The `context` argument is | ||
| * accepted but ignored — replacement is applied uniformly to all values. | ||
| * | ||
| * Returns non-string input unchanged. | ||
| */ | ||
| parse(val: string, context?: ValueParserContext): string; | ||
| parse(val: unknown, context?: ValueParserContext): unknown; | ||
| } | ||
| // --------------------------------------------------------------------------- | ||
@@ -384,0 +269,0 @@ // Named entity group exports |
+0
-1
@@ -20,3 +20,2 @@ /** | ||
| export { DEFAULT_XML_ENTITIES, AMP_ENTITY } from './EntityReplacer.js'; | ||
| export { default as EntitiesValueParser } from './EntitiesValueParser.js'; | ||
| export { | ||
@@ -23,0 +22,0 @@ COMMON_HTML, |
| import EntityReplacer from './EntityReplacer.js'; | ||
| /** | ||
| * EntitiesValueParser — value-parser adapter that wraps `EntityReplacer`. | ||
| * | ||
| * Register an instance under the key `'entity'` on a `@nodable/flexible-xml-parser` | ||
| * output builder factory to enable entity expansion for all parsed text values. | ||
| * | ||
| * ## Lifecycle | ||
| * | ||
| * 1. **Construction** — supply configuration and optional persistent entities. | ||
| * 2. **`setExternalEntities(map)`** — (re)set the full persistent entity map. | ||
| * Or use `addEntity(key, value)` to add one at a time. | ||
| * 3. **`getInstance()`** — builder factory calls this when creating a new builder | ||
| * instance. Resets input entities and per-document counters. Returns `this`. | ||
| * 4. **`addInputEntities(map)`** — builder calls this if the document has a | ||
| * DOCTYPE block. Stores entities for *this document only*. | ||
| * 5. **`parse(val)`** — called by the builder for each text value. | ||
| * | ||
| * ```js | ||
| * const evp = new EntitiesValueParser({ system: COMMON_HTML }); | ||
| * evp.setExternalEntities({ brand: 'Acme' }); | ||
| * builder.registerValueParser('entity', evp); | ||
| * ``` | ||
| * | ||
| * ------------------------------------------------------------------------- | ||
| * Constructor options (all optional) | ||
| * ------------------------------------------------------------------------- | ||
| * | ||
| * `default` — `true` (default) | `false`/`null` | custom EntityTable | ||
| * `system` — `false` (default) | `true` for COMMON_HTML | EntityTable | ||
| * `amp` — `true` (default) | `false`/`null` | ||
| * `maxTotalExpansions` — max entity refs expanded per document (0 = unlimited) | ||
| * `maxExpandedLength` — max characters added by expansion per document (0 = unlimited) | ||
| * `applyLimitsTo` — which categories count toward limits (default: `'external'`) | ||
| * `postCheck` — `(resolved, original) => string` hook | ||
| * `entities` — initial persistent entity map, e.g. `{ copy: '©' }` | ||
| */ | ||
| export default class EntitiesValueParser { | ||
| constructor(options = {}) { | ||
| this._replacer = new EntityReplacer(options); | ||
| // Load any entities provided inline at construction time as persistent entities | ||
| if (options.entities && typeof options.entities === 'object') { | ||
| const init = {}; | ||
| for (const [key, val] of Object.entries(options.entities)) { | ||
| this._validateEntityArgs(key, val); | ||
| init[key] = val; | ||
| } | ||
| this._replacer.setExternalEntities(init); | ||
| } | ||
| } | ||
| // ------------------------------------------------------------------------- | ||
| // Persistent external entity registration | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Replace the full set of persistent external entities. | ||
| * These survive across documents and are never wiped by `getInstance()`. | ||
| * | ||
| * @param {Record<string, string>} map — e.g. `{ copy: '©', brand: 'Acme' }` | ||
| */ | ||
| setExternalEntities(map) { | ||
| for (const [key, val] of Object.entries(map)) { | ||
| this._validateEntityArgs(key, val); | ||
| } | ||
| this._replacer.setExternalEntities(map); | ||
| } | ||
| /** | ||
| * Add (or replace) a single persistent external entity. | ||
| * Existing persistent entities are preserved. | ||
| * | ||
| * @param {string} key — bare name without `&` / `;`, e.g. `'copy'` | ||
| * @param {string} value — replacement string, e.g. `'©'` | ||
| */ | ||
| addEntity(key, value) { | ||
| this._validateEntityArgs(key, value); | ||
| this._replacer.addExternalEntity(key, value); | ||
| } | ||
| // ------------------------------------------------------------------------- | ||
| // Builder factory integration | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Reset per-document state (input entities + expansion counters) and return `this`. | ||
| * | ||
| * The builder factory calls this when creating a new builder instance so that | ||
| * DOCTYPE entities from a previous document are never carried over. | ||
| * | ||
| * @returns {EntitiesValueParser} `this` | ||
| */ | ||
| getInstance() { | ||
| this._replacer.getInstance(); | ||
| return this; | ||
| } | ||
| // ------------------------------------------------------------------------- | ||
| // DOCTYPE integration — called by BaseOutputBuilder | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Receive DOCTYPE entities from the output builder. | ||
| * | ||
| * These are stored separately from persistent entities and wiped on the next | ||
| * `getInstance()` call. Resets per-document expansion counters. | ||
| * | ||
| * @param {Record<string, string | { regx: RegExp, val: string | Function }>} entities | ||
| * Raw entity map from `DocTypeReader` — values may be plain strings or | ||
| * `{ regx, val }` objects (note: `regx`, not `regex`, matching the reader's output). | ||
| */ | ||
| addInputEntities(entities) { | ||
| this._replacer.addInputEntities(entities); | ||
| } | ||
| // ------------------------------------------------------------------------- | ||
| // ValueParser interface | ||
| // ------------------------------------------------------------------------- | ||
| /** | ||
| * Replace entity references in `val`. | ||
| * | ||
| * @param {string} val | ||
| * @param {object} [_context] | ||
| * @returns {string} | ||
| */ | ||
| parse(val, _context) { | ||
| if (typeof val !== 'string') return val; | ||
| return this._replacer.replace(val); | ||
| } | ||
| // ------------------------------------------------------------------------- | ||
| // Private helpers | ||
| // ------------------------------------------------------------------------- | ||
| _validateEntityArgs(key, value) { | ||
| if (typeof key !== 'string' || key.includes('&') || key.includes(';')) { | ||
| throw new Error( | ||
| `[EntitiesValueParser] Entity key must not contain '&' or ';'. ` + | ||
| `Use 'copy' for '©', got: ${JSON.stringify(key)}` | ||
| ); | ||
| } | ||
| if (typeof value !== 'string' || value.includes('&')) { | ||
| throw new Error( | ||
| `[EntitiesValueParser] Entity value must be a plain string that does not ` + | ||
| `contain '&', got: ${JSON.stringify(value)}` | ||
| ); | ||
| } | ||
| } | ||
| } |
44085
-21.43%6
-14.29%736
-24.67%436
-8.98%