@altano/html-cdnify 5.0.0 → 6.0.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
@@ -2,19 +2,32 @@
2
2
 
3
3
  ![logo](./assets/logo.png)
4
4
 
5
- [![npm](https://badgen.net/npm/v/@altano/html-cdnify)](https://www.npmjs.com/package/@altano/html-cdnify) ![Typed with TypeScript](https://badgen.net/npm/types/@altano/html-cdnify) ![ESM only](https://badgen.net/badge/module/esm%20only?icon=js)
6
-
7
- Transform the relative URLs in your HTML markup (e.g. scripts, stylesheets, images) to use your CDN URL.
8
-
9
- - Uses a real HTML parser, [trumpet](https://www.npmjs.com/package/trumpet), not regular expressions.
10
- - Doesn't require changes to your HTML, is a purely post-process transformation.
5
+ [![npm][npm-badge]][npm-url]
6
+ ![Typed with TypeScript][types-badge]
7
+ ![ESM only][esm-badge]
8
+
9
+ [npm-badge]: https://badgen.net/npm/v/@altano/html-cdnify
10
+ [npm-url]: https://www.npmjs.com/package/@altano/html-cdnify
11
+ [types-badge]: https://badgen.net/npm/types/@altano/html-cdnify
12
+ [esm-badge]: https://badgen.net/badge/module/esm%20only?icon=js
13
+
14
+ Transform the relative URLs in your HTML markup (e.g. scripts, stylesheets,
15
+ images) to use your CDN URL.
16
+
17
+ - Uses a real HTML parser, [parse5](https://www.npmjs.com/package/parse5), not
18
+ regular expressions.
19
+ - Doesn't require changes to your HTML, is a purely post-process
20
+ transformation.
11
21
  - Supports relative URLs in your HTML.
12
- - Will perform minimal modifications to your HTML, only modifying the element that is having an attribute cdnified.
13
- - Like [grunt-cdnify](https://www.npmjs.com/package/grunt-cdnify) but usable with or without grunt.
14
- - Like [cdnify](https://www.npmjs.com/package/cdnify) but you don't have to mark up your HTML with magic attributes.
22
+ - Will perform minimal modifications to your HTML, only modifying the element
23
+ that is having an attribute cdnified.
24
+ - Like [grunt-cdnify](https://www.npmjs.com/package/grunt-cdnify) but usable
25
+ with or without grunt.
26
+ - Like [cdnify](https://www.npmjs.com/package/cdnify) but you don't have to
27
+ mark up your HTML with magic attributes.
15
28
  - This library's scope is limited to HTML and is not meant to process CSS.
16
29
 
17
- # Simplest usage
30
+ ## Simplest usage
18
31
 
19
32
  ```js
20
33
  import { cdnify } from "@altano/html-cdnify");
@@ -30,42 +43,49 @@ cdnify({
30
43
  // <img src="//cdn.com/face.png">
31
44
  ```
32
45
 
33
- # What gets cdnified
34
-
35
- Any URLs where only the path (and after) is specified which are found in the following locations will be cdnified:
36
-
37
- <img data-src="____">
38
- <img src="____">
39
- <img srcset="____">
40
- <video poster="____">
41
- <script src="____">
42
- <source src="____">
43
- <link rel="apple-touch-icon" href="____">
44
- <link rel="icon" href="____">
45
- <link rel="shortcut icon" href="____">
46
- <link rel="stylesheet" href="____">
46
+ ## What gets cdnified
47
+
48
+ Any URLs where only the path (and after) is specified which are found in the
49
+ following locations will be cdnified:
50
+
51
+ ```text
52
+ <img data-src="____">
53
+ <img src="____">
54
+ <img srcset="____">
55
+ <video poster="____">
56
+ <script src="____">
57
+ <source src="____">
58
+ <link rel="apple-touch-icon" href="____">
59
+ <link rel="icon" href="____">
60
+ <link rel="shortcut icon" href="____">
61
+ <link rel="stylesheet" href="____">
62
+ ```
47
63
 
48
- ## Will be cdnified
64
+ ### Will be cdnified
49
65
 
50
66
  - `<img src="/logo.png">`
51
67
  - `<script src="jquery.js"></script>`
52
68
  - `<link rel="stylesheet" href="main.css"></script>`
53
69
 
54
- ## Won't be cdnified
70
+ ### Won't be cdnified
55
71
 
56
- | HTMLElement | Reason not cdnified |
57
- | ------------------------------------- | ------------------------------------------- |
58
- | `<img src="http://foo.com/logo.png">` | Absolute URL specified |
59
- | `<img src="//foo.com/logo.png">` | Scheme-relative/agnostic URL specified |
60
- | `<img custom="/logo.png">` | "custom" attribute not among those cdnified |
72
+ | HTMLElement | Reason not cdnified |
73
+ | ------------------------------------- | ---------------------- |
74
+ | `<img src="http://foo.com/logo.png">` | Absolute URL |
75
+ | `<img src="//foo.com/logo.png">` | Scheme-relative URL |
76
+ | `<img custom="/logo.png">` | Attribute not cdnified |
61
77
 
62
- Adding the `data-cdn-ignore` attribute to any element will cause the element to be skipped during cdnification, and the `data-cdn-ignore` attribute will be removed to clean up your HTML.
78
+ Adding the `data-cdn-ignore` attribute to any element will cause the element to
79
+ be skipped during cdnification, and the `data-cdn-ignore` attribute will be
80
+ removed to clean up your HTML.
63
81
 
64
- # More Examples
82
+ ## More Examples
65
83
 
66
- ## Using `bufferPath` to deal with subdirectories
84
+ ### Using `bufferPath` to deal with subdirectories
67
85
 
68
- You can specify `bufferPath`, the relative path to the buffer being processed. If this isn't specified, all resources with relative paths will be assumed to be at the root of the domain before being cdnified.
86
+ You can specify `bufferPath`, the relative path to the buffer being processed.
87
+ If this isn't specified, all resources with relative paths will be assumed to
88
+ be at the root of the domain before being cdnified.
69
89
 
70
90
  ```js
71
91
  import { cdnify } from "@altano/html-cdnify");
@@ -99,7 +119,9 @@ _Without_ `bufferPath`, the output would be:
99
119
  // <img src="//cdn.com/images/logo.png">
100
120
  ```
101
121
 
102
- If your CDN has a path specified, it is assumed that both relative URLs AND root-relative URLs in your HTML are relative to the CDN directory. In other words, the CDN subdirectory will always be in the final URL. For example:
122
+ If your CDN has a path specified, it is assumed that both relative URLs AND
123
+ root-relative URLs in your HTML are relative to the CDN directory. In other
124
+ words, the CDN subdirectory will always be in the final URL. For example:
103
125
 
104
126
  ```js
105
127
  import { cdnify } from "@altano/html-cdnify");
@@ -120,15 +142,20 @@ outputPromise.then((output) => console.log(output));
120
142
 
121
143
  // Output:
122
144
  // <img src="//cdn.com/sub/directory/in/cdn/is/always/present/article/figure1.png">
123
- // <img src="//cdn.com/sub/directory/in/cdn/is/always/present/article/articleSubDirectory/figure2.png">
145
+ // <img src="//cdn.com/sub/.../article/articleSubDirectory/figure2.png">
124
146
  // <img src="//cdn.com/sub/directory/in/cdn/is/always/present/images/logo.png">
125
147
  ```
126
148
 
127
- It should usually be the expectation that a CDN subdir is always present in the cdnified result. If you need to specify a CDN subdirectory AND you need to sometimes escape URLs in your HTML to the root of the CDN domain, you'll have to write a custom transformFunction.
149
+ It should usually be the expectation that a CDN subdir is always present in the
150
+ cdnified result. If you need to specify a CDN subdirectory AND you need to
151
+ sometimes escape URLs in your HTML to the root of the CDN domain, you'll have
152
+ to write a custom transformFunction.
128
153
 
129
- ## Using the underlying stream instead of a Promise
154
+ ### Using the underlying stream instead of a Promise
130
155
 
131
- If you are dealing with large files or network IO and would like to use the underlying NodeJS Transform Stream rather than buffering all of the input and output, feel free:
156
+ If you are dealing with large files or network IO and would like to use the
157
+ underlying NodeJS Transform Stream rather than buffering all of the input and
158
+ output, feel free:
132
159
 
133
160
  ```js
134
161
  import streamifier from "streamifier";
@@ -154,7 +181,7 @@ outputStream.pipe(process.stdout);
154
181
  // <img src="http://cdn.com/face3.png">
155
182
  ```
156
183
 
157
- ## Specifying a transform defintion to add `<custom-element src="____">` to the list of attributes to be cdnified
184
+ ### Adding `<custom-element src="____">` via a transform definition
158
185
 
159
186
  ```js
160
187
  import { cdnify } from "@altano/html-cdnify");
@@ -176,7 +203,7 @@ outputPromise.then((output) => console.log(output));
176
203
  // <custom-element src="//cdn.com/cdn/face7.png">
177
204
  ```
178
205
 
179
- ## Specifying a custom transform function
206
+ ### Specifying a custom transform function
180
207
 
181
208
  ```js
182
209
  import { cdnify } from "@altano/html-cdnify");
@@ -195,7 +222,7 @@ outputPromise.then((output) => console.log(output));
195
222
  // <img src="//cdn.com/subdir/logo.png">
196
223
  ```
197
224
 
198
- ## Specifying a custom transform function that delegates to the default
225
+ ### Specifying a custom transform function that delegates to the default
199
226
 
200
227
  If your assets are on different CDNs, you might need to write something like:
201
228
 
@@ -210,7 +237,11 @@ var outputPromise = cdnify({
210
237
  } else {
211
238
  var customCdnBaseUrl = "//assetcdn.com";
212
239
  }
213
- return CDNTransformer.defaultTransformFunction(customCdnBaseUrl, oldUrl, bufferPath);
240
+ return CDNTransformer.defaultTransformFunction(
241
+ customCdnBaseUrl,
242
+ oldUrl,
243
+ bufferPath,
244
+ );
214
245
  },
215
246
  buffer: `<img src="logo.png"><script src="main.js"></script>`,
216
247
  }).then((buffer) => buffer.toString());
@@ -221,9 +252,11 @@ outputPromise.then((output) => console.log(output));
221
252
  // <img src="//imagecdn.com/logo.png"><script src="//assetcdn.com/main.js"></script>
222
253
  ```
223
254
 
224
- ## Specifying a custom attributeParser
255
+ ### Specifying a custom attributeParser
225
256
 
226
- A custom attributeParser is like a custom transformFunction, but is scoped to just one selector rather than being applied to every attribute transformation. For example, to uppercase only PNG image names before cdnifying:
257
+ A custom attributeParser is like a custom transformFunction, but is scoped to
258
+ just one selector rather than being applied to every attribute transformation.
259
+ For example, to uppercase only PNG image names before cdnifying:
227
260
 
228
261
  ```js
229
262
  import { cdnify } from "@altano/html-cdnify");
@@ -234,7 +267,8 @@ var outputPromise = cdnify({
234
267
  {
235
268
  selector: `img[src$="png"]`,
236
269
  attribute: "src",
237
- attributeParser: (oldAttribute, transformFunction) => transformFunction(oldAttribute.toUpperCase()),
270
+ attributeParser: (oldAttribute, transformFunction) =>
271
+ transformFunction(oldAttribute.toUpperCase()),
238
272
  },
239
273
  ],
240
274
  buffer: `<img src="/logo.gif"><img src="/logo.png">`,
@@ -246,7 +280,7 @@ outputPromise.then((output) => console.log(output));
246
280
  // <img src="//cdn.com/logo.gif"><img src="//cdn.com/LOGO.PNG">
247
281
  ```
248
282
 
249
- ## Overriding an existing transform
283
+ ### Overriding an existing transform
250
284
 
251
285
  The default transformDefinitions array is:
252
286
 
@@ -301,7 +335,9 @@ The default transformDefinitions array is:
301
335
  ];
302
336
  ```
303
337
 
304
- If you'd like to ovverride any of these, specify the same selector and attribute but a different attributeParser. For example:
338
+ Note that any `transformDefinitions` you pass are **merged with** these
339
+ defaults rather than replacing them. To override one, specify the same selector
340
+ and attribute but a different attributeParser. For example:
305
341
 
306
342
  ```js
307
343
  import { cdnify } from "@altano/html-cdnify");
@@ -326,9 +362,9 @@ outputPromise.then((output) => console.log(output));
326
362
  // <img src="logo.gif">
327
363
  ```
328
364
 
329
- # API
365
+ ## API
330
366
 
331
- ## Promise-based API (recommended for simplicity)
367
+ ### Promise-based API (recommended for simplicity)
332
368
 
333
369
  ```js
334
370
  import { cdnify } from "@altano/html-cdnify");
@@ -336,17 +372,21 @@ import { cdnify } from "@altano/html-cdnify");
336
372
  cdnify({ ... Options ...}) => Promise<Buffer>
337
373
  ```
338
374
 
339
- ### Options
375
+ #### Promise-based options
340
376
 
341
- Same options as stream-based API (see below), plus:
377
+ Same options as the stream-based API (see below), plus:
342
378
 
343
- | Property | Type | Optional | Description |
344
- | -------- | ---------------- | ---------------------------------------- | ----------- |
345
- | buffer | Buffer or string | The content that is going to be cdnified |
379
+ | Property | Type | Required | Description |
380
+ | -------- | ------------------ | -------- | ------------------ |
381
+ | `buffer` | `Buffer \| string` | yes | Content to cdnify. |
346
382
 
347
- ## Stream-based API
383
+ ### Stream-based API
348
384
 
349
- use the stream-based API when you are dealing with large files or network IO and would like to use the underlying NodeJS Transform Stream rather than buffering all of the input and output. The Promisebased API is simpler but the input/output must be completely buffered, which is obviously fine most of the time.
385
+ Use the stream-based API when you are dealing with large files or network IO
386
+ and would like to use the underlying NodeJS Transform Stream rather than
387
+ buffering all of the input and output. The Promise-based API is simpler but the
388
+ input/output must be completely buffered, which is obviously fine most of the
389
+ time.
350
390
 
351
391
  ```js
352
392
  import { CDNTransformer } from "@altano/html-cdnify");
@@ -355,45 +395,82 @@ var transformer = new CDNTransformer({ ... Options ... }) => CDNTransformer
355
395
  transformer.stream => NodeJS TransformStream
356
396
  ```
357
397
 
358
- ### Options
398
+ #### Stream-based options
359
399
 
360
- | Property | Type | Optional | Description |
361
- | -------------------- | -------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
362
- | cdnUrl | string | | The absolute (or scheme-relative/agnostic) URL to your CDN. |
363
- | bufferPath | string | Y | The relative path to the buffer being processed. If this isn't specified, all resources with relative paths will be assumed to be at the root of the domain before being cdnified. |
364
- | transformDefinitions | TransformDefinition | Y | See below... |
365
- | transformFunction | CDNTransformFunction | Y | See below... |
400
+ | Property | Type | Required |
401
+ | ---------------------- | ----------------------- | -------- |
402
+ | `cdnUrl` | `string` | yes |
403
+ | `bufferPath` | `string` | no |
404
+ | `transformDefinitions` | `TransformDefinition[]` | no |
405
+ | `transformFunction` | `CDNTransformFunction` | no |
366
406
 
367
- ### TransformDefinition
407
+ - **`cdnUrl`** — the absolute (or scheme-relative/agnostic) URL to your CDN.
408
+ - **`bufferPath`** — the relative path to the buffer being processed. If this
409
+ isn't specified, all resources with relative paths will be assumed to be at
410
+ the root of the domain before being cdnified.
411
+ - **`transformDefinitions`** — see below.
412
+ - **`transformFunction`** — see below.
368
413
 
369
- ```
414
+ #### TransformDefinition
415
+
416
+ ```ts
370
417
  interface TransformDefinition {
371
418
  selector: string;
372
419
  attribute: string;
373
- attributeParser?: (oldAttribute: string, transformFunction: TransformFunction) => string;
420
+ attributeParser?: (
421
+ oldAttribute: string,
422
+ transformFunction: TransformFunction,
423
+ ) => string;
374
424
  }
375
425
  ```
376
426
 
377
427
  The default attributeParser when one isn't specified is:
378
428
 
379
- ```
429
+ ```js
380
430
  HtmlAttributeStreamTransformerOptions.attributeParsers = {
381
- default: (attr, transformFunction) => transformFunction(attr)
431
+ default: (attr, transformFunction) => transformFunction(attr),
382
432
  };
383
433
  ```
384
434
 
385
- ### CDNTransformFunction
435
+ ##### Selector support
386
436
 
387
- ```
437
+ Selectors are matched against each start tag on its own, as the document
438
+ streams past. That means **only element-local selectors work** — a tag name
439
+ plus any number of attribute predicates:
440
+
441
+ | Selector | Works | Kind |
442
+ | ---------------------------- | ----- | ------------------ |
443
+ | `img` | yes | tag name |
444
+ | `*[src]` | yes | universal |
445
+ | `img[src]` | yes | attribute presence |
446
+ | `link[rel="stylesheet"]` | yes | attribute equals |
447
+ | `img[src$="png"]` | yes | substring match |
448
+ | `img:not([data-cdn-ignore])` | yes | negation |
449
+ | `div img` | no | descendant |
450
+ | `div > img` | no | child combinator |
451
+ | `p + img` | no | sibling combinator |
452
+
453
+ A selector using a combinator **will not throw — it will simply never match**,
454
+ so nothing gets cdnified. If a transform silently does nothing, check whether
455
+ its selector reaches outside the element.
456
+
457
+ This is a deliberate trade: matching one tag at a time is what lets the
458
+ transform stream, and lets everything a selector doesn't touch pass through
459
+ byte-for-byte.
460
+
461
+ #### CDNTransformFunction
462
+
463
+ ```ts
388
464
  (cdnUrl: string, oldUrl: string, bufferPath: string) => string;
389
465
  ```
390
466
 
391
- ### TransformFunction
467
+ #### TransformFunction
392
468
 
393
- ```
469
+ ```ts
394
470
  (oldAttribute: string) => string;
395
471
  ```
396
472
 
397
473
  ---
398
474
 
399
- Shield (in logo) by [Flaticon](http://www.flaticon.com) from [Freepik](http://www.freepik.com)
475
+ Shield (in logo) by [Flaticon](http://www.flaticon.com) from
476
+ [Freepik](http://www.freepik.com)
@@ -8,7 +8,7 @@ interface CDNTransformerOptions {
8
8
  transformFunction?: CDNTransformFunction;
9
9
  }
10
10
  /**
11
- * Wraps a node-trumpet Transform stream, w/ HTML as input and
11
+ * Wraps an HtmlRewriteStream, w/ HTML as input and
12
12
  * HTML as output. The output will have all non-absolute-URL
13
13
  * attributes that identify CDN-able resources converted to
14
14
  * use the given CDN URL.
@@ -1,6 +1,6 @@
1
1
  import urlConverter_default from "./urlConverter.js";
2
2
  import { HtmlAttributeStreamTransformer } from "./HtmlAttributeStreamTransformer.js";
3
- import { merge, mergeWith, unionBy } from "lodash";
3
+ import { merge, mergeWith, unionBy } from "lodash-es";
4
4
 
5
5
  //#region src/CDNTransformer.ts
6
6
  /**
@@ -22,7 +22,7 @@ var OptionsMerger = class OptionsMerger {
22
22
  }
23
23
  };
24
24
  /**
25
- * Wraps a node-trumpet Transform stream, w/ HTML as input and
25
+ * Wraps an HtmlRewriteStream, w/ HTML as input and
26
26
  * HTML as output. The output will have all non-absolute-URL
27
27
  * attributes that identify CDN-able resources converted to
28
28
  * use the given CDN URL.
@@ -1 +1 @@
1
- {"version":3,"file":"CDNTransformer.js","names":["HtmlAttributeStreamTransformer.HtmlAttributeStreamTransformer","#defaultHtmlAttributeStreamTransformerOptions","#cdnifyAttribute","#defaultCdnOptions","urlConverter"],"sources":["../src/CDNTransformer.ts"],"sourcesContent":["import { unionBy, merge, mergeWith, type MergeWithCustomizer } from \"lodash\";\nimport urlConverter from \"./urlConverter.js\";\n\nimport * as HtmlAttributeStreamTransformer from \"./HtmlAttributeStreamTransformer.js\";\n\nexport type CDNTransformFunction = (\n cdnUrl: string,\n oldUrl: string,\n bufferPath: string,\n) => string;\n\nexport interface CDNTransformerOptions {\n cdnUrl: string;\n bufferPath?: string;\n\n transformDefinitions?: HtmlAttributeStreamTransformer.TransformDefinition[];\n transformFunction?: CDNTransformFunction;\n}\n\n/**\n * Handles merging objects that have TransformDefinition arrays\n */\nclass OptionsMerger {\n static isTransformDefinition(\n a: unknown,\n ): a is HtmlAttributeStreamTransformer.TransformDefinition {\n return (\n a != null &&\n typeof a === \"object\" &&\n \"selector\" in a &&\n \"attribute\" in a &&\n typeof a.selector === \"string\" &&\n typeof a.selector === \"string\" &&\n typeof a.attribute === \"string\"\n );\n }\n\n static isTransformDefinitions(\n a: unknown,\n ): a is HtmlAttributeStreamTransformer.TransformDefinition[] {\n return (\n Array.isArray(a) &&\n a.length > 0 &&\n OptionsMerger.isTransformDefinition(a[0])\n );\n }\n\n static mergeCustomizer: MergeWithCustomizer = (objValue, srcValue) => {\n if (\n OptionsMerger.isTransformDefinitions(objValue) &&\n OptionsMerger.isTransformDefinitions(srcValue)\n ) {\n const result = unionBy(\n srcValue,\n objValue, // reverse order to allow srcValue to take precedence\n (value: HtmlAttributeStreamTransformer.TransformDefinition) =>\n `${value.selector} / ${value.attribute}`,\n );\n\n return result;\n } else {\n return undefined;\n }\n };\n\n static merge<T, U, V, W>(\n target: T,\n source1: U,\n source2: V,\n source3: W,\n ): T & U & V & W {\n return mergeWith(\n target,\n source1,\n source2,\n source3,\n OptionsMerger.mergeCustomizer,\n );\n }\n}\n\n/**\n * Wraps a node-trumpet Transform stream, w/ HTML as input and\n * HTML as output. The output will have all non-absolute-URL\n * attributes that identify CDN-able resources converted to\n * use the given CDN URL.\n */\nexport class CDNTransformer\n extends HtmlAttributeStreamTransformer.HtmlAttributeStreamTransformer\n{\n public cdnOptions: Required<CDNTransformerOptions>;\n\n constructor(cdnOptions: CDNTransformerOptions) {\n super(\n OptionsMerger.merge(\n {},\n CDNTransformer.#defaultHtmlAttributeStreamTransformerOptions,\n {\n transformDefinitions: [],\n transformFunction: (attribute: string) =>\n this.#cdnifyAttribute(attribute),\n },\n {\n transformDefinitions: cdnOptions.transformDefinitions,\n },\n ),\n );\n\n this.cdnOptions = merge(\n {\n transformDefinitions: [],\n },\n CDNTransformer.#defaultCdnOptions,\n cdnOptions,\n );\n\n if (typeof this.cdnOptions.cdnUrl !== \"string\") {\n throw new Error(`Invalid cdnUrl specified.`);\n }\n }\n\n #cdnifyAttribute(oldUrl: string): string {\n return this.cdnOptions.transformFunction(\n this.cdnOptions.cdnUrl,\n oldUrl,\n this.cdnOptions.bufferPath,\n );\n }\n\n static #defaultCdnOptions = {\n bufferPath: \".\",\n transformFunction: urlConverter,\n } as const;\n\n public static defaultTransformFunction: CDNTransformFunction =\n CDNTransformer.#defaultCdnOptions.transformFunction;\n\n static #defaultHtmlAttributeStreamTransformerOptions = {\n attributeToMarkElementToBeIgnored: \"data-cdn-ignore\",\n\n // inspired by https://github.com/callumlocke/grunt-cdnify/blob/master/tasks/cdnify.js#L31\n transformDefinitions: [\n {\n selector: `video[poster]:not([data-cdn-ignore])`,\n attribute: \"poster\",\n },\n {\n selector: `img[data-src]:not([data-cdn-ignore])`,\n attribute: \"data-src\",\n },\n {\n selector: `script[src]:not([data-cdn-ignore])`,\n attribute: \"src\",\n },\n {\n selector: `source[src]:not([data-cdn-ignore])`,\n attribute: \"src\",\n },\n {\n selector: `link[rel=\"apple-touch-icon\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `link[rel=\"icon\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `link[rel=\"shortcut icon\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `link[rel=\"stylesheet\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `img[src]:not([data-cdn-ignore])`,\n attribute: \"src\",\n },\n {\n selector: `img[srcset]:not([data-cdn-ignore])`,\n attribute: \"srcset\",\n attributeParser(\n attr: string,\n transformFunction: HtmlAttributeStreamTransformer.TransformFunction,\n ): string {\n return attr\n .split(\",\")\n .map((imgInfo) => imgInfo.replace(/([^ ]+)/, transformFunction))\n .join(\",\");\n },\n },\n ],\n } as const;\n}\n"],"mappings":";;;;;;;;AAsBA,IAAM,gBAAN,MAAM,cAAc;CAClB,OAAO,sBACL,GACyD;EACzD,OACE,KAAK,QACL,OAAO,MAAM,YACb,cAAc,KACd,eAAe,KACf,OAAO,EAAE,aAAa,YACtB,OAAO,EAAE,aAAa,YACtB,OAAO,EAAE,cAAc;CAE3B;CAEA,OAAO,uBACL,GAC2D;EAC3D,OACE,MAAM,QAAQ,CAAC,KACf,EAAE,SAAS,KACX,cAAc,sBAAsB,EAAE,EAAE;CAE5C;CAEA,OAAO,mBAAwC,UAAU,aAAa;EACpE,IACE,cAAc,uBAAuB,QAAQ,KAC7C,cAAc,uBAAuB,QAAQ,GAS7C,OAPe,QACb,UACA,WACC,UACC,GAAG,MAAM,SAAS,KAAK,MAAM,WAGrB;OAEZ;CAEJ;CAEA,OAAO,MACL,QACA,SACA,SACA,SACe;EACf,OAAO,UACL,QACA,SACA,SACA,SACA,cAAc,eAChB;CACF;AACF;;;;;;;AAQA,IAAa,iBAAb,MAAa,uBACHA,+BACV;CACE,AAAO;CAEP,YAAY,YAAmC;EAC7C,MACE,cAAc,MACZ,CAAC,GACD,eAAeC,+CACf;GACE,sBAAsB,CAAC;GACvB,oBAAoB,cAClB,KAAKC,iBAAiB,SAAS;EACnC,GACA,EACE,sBAAsB,WAAW,qBACnC,CACF,CACF;EAEA,KAAK,aAAa,MAChB,EACE,sBAAsB,CAAC,EACzB,GACA,eAAeC,oBACf,UACF;EAEA,IAAI,OAAO,KAAK,WAAW,WAAW,UACpC,MAAM,IAAI,MAAM,2BAA2B;CAE/C;CAEA,iBAAiB,QAAwB;EACvC,OAAO,KAAK,WAAW,kBACrB,KAAK,WAAW,QAChB,QACA,KAAK,WAAW,UAClB;CACF;CAEA,OAAOA,qBAAqB;EAC1B,YAAY;EACZ,mBAAmBC;CACrB;CAEA,OAAc,2BACZ,eAAeD,mBAAmB;CAEpC,OAAOF,gDAAgD;EACrD,mCAAmC;EAGnC,sBAAsB;GACpB;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;IACX,gBACE,MACA,mBACQ;KACR,OAAO,KACJ,MAAM,GAAG,CAAC,CACV,KAAK,YAAY,QAAQ,QAAQ,WAAW,iBAAiB,CAAC,CAAC,CAC/D,KAAK,GAAG;IACb;GACF;EACF;CACF;AACF"}
1
+ {"version":3,"file":"CDNTransformer.js","names":["HtmlAttributeStreamTransformer.HtmlAttributeStreamTransformer","#defaultHtmlAttributeStreamTransformerOptions","#cdnifyAttribute","#defaultCdnOptions","urlConverter"],"sources":["../src/CDNTransformer.ts"],"sourcesContent":["import { unionBy, merge, mergeWith } from \"lodash-es\";\nimport urlConverter from \"./urlConverter.js\";\n\nimport * as HtmlAttributeStreamTransformer from \"./HtmlAttributeStreamTransformer.js\";\n\nexport type CDNTransformFunction = (\n cdnUrl: string,\n oldUrl: string,\n bufferPath: string,\n) => string;\n\nexport interface CDNTransformerOptions {\n cdnUrl: string;\n bufferPath?: string;\n\n transformDefinitions?: HtmlAttributeStreamTransformer.TransformDefinition[];\n transformFunction?: CDNTransformFunction;\n}\n\n/**\n * Handles merging objects that have TransformDefinition arrays\n */\nclass OptionsMerger {\n static isTransformDefinition(\n a: unknown,\n ): a is HtmlAttributeStreamTransformer.TransformDefinition {\n return (\n a != null &&\n typeof a === \"object\" &&\n \"selector\" in a &&\n \"attribute\" in a &&\n typeof a.selector === \"string\" &&\n typeof a.selector === \"string\" &&\n typeof a.attribute === \"string\"\n );\n }\n\n static isTransformDefinitions(\n a: unknown,\n ): a is HtmlAttributeStreamTransformer.TransformDefinition[] {\n return (\n Array.isArray(a) &&\n a.length > 0 &&\n OptionsMerger.isTransformDefinition(a[0])\n );\n }\n\n static mergeCustomizer = (objValue: unknown, srcValue: unknown): unknown => {\n if (\n OptionsMerger.isTransformDefinitions(objValue) &&\n OptionsMerger.isTransformDefinitions(srcValue)\n ) {\n const result = unionBy(\n srcValue,\n objValue, // reverse order to allow srcValue to take precedence\n (value: HtmlAttributeStreamTransformer.TransformDefinition) =>\n `${value.selector} / ${value.attribute}`,\n );\n\n return result;\n } else {\n return undefined;\n }\n };\n\n static merge<T, U, V, W>(\n target: T,\n source1: U,\n source2: V,\n source3: W,\n ): T & U & V & W {\n return mergeWith(\n target,\n source1,\n source2,\n source3,\n OptionsMerger.mergeCustomizer,\n );\n }\n}\n\n/**\n * Wraps an HtmlRewriteStream, w/ HTML as input and\n * HTML as output. The output will have all non-absolute-URL\n * attributes that identify CDN-able resources converted to\n * use the given CDN URL.\n */\nexport class CDNTransformer\n extends HtmlAttributeStreamTransformer.HtmlAttributeStreamTransformer\n{\n public cdnOptions: Required<CDNTransformerOptions>;\n\n constructor(cdnOptions: CDNTransformerOptions) {\n super(\n OptionsMerger.merge(\n {},\n CDNTransformer.#defaultHtmlAttributeStreamTransformerOptions,\n {\n transformDefinitions: [],\n transformFunction: (attribute: string) =>\n this.#cdnifyAttribute(attribute),\n },\n {\n transformDefinitions: cdnOptions.transformDefinitions,\n },\n ),\n );\n\n this.cdnOptions = merge(\n {\n transformDefinitions: [],\n },\n CDNTransformer.#defaultCdnOptions,\n cdnOptions,\n );\n\n if (typeof this.cdnOptions.cdnUrl !== \"string\") {\n throw new Error(`Invalid cdnUrl specified.`);\n }\n }\n\n #cdnifyAttribute(oldUrl: string): string {\n return this.cdnOptions.transformFunction(\n this.cdnOptions.cdnUrl,\n oldUrl,\n this.cdnOptions.bufferPath,\n );\n }\n\n static #defaultCdnOptions = {\n bufferPath: \".\",\n transformFunction: urlConverter,\n } as const;\n\n public static defaultTransformFunction: CDNTransformFunction =\n CDNTransformer.#defaultCdnOptions.transformFunction;\n\n static #defaultHtmlAttributeStreamTransformerOptions = {\n attributeToMarkElementToBeIgnored: \"data-cdn-ignore\",\n\n // inspired by https://github.com/callumlocke/grunt-cdnify/blob/master/tasks/cdnify.js#L31\n transformDefinitions: [\n {\n selector: `video[poster]:not([data-cdn-ignore])`,\n attribute: \"poster\",\n },\n {\n selector: `img[data-src]:not([data-cdn-ignore])`,\n attribute: \"data-src\",\n },\n {\n selector: `script[src]:not([data-cdn-ignore])`,\n attribute: \"src\",\n },\n {\n selector: `source[src]:not([data-cdn-ignore])`,\n attribute: \"src\",\n },\n {\n selector: `link[rel=\"apple-touch-icon\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `link[rel=\"icon\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `link[rel=\"shortcut icon\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `link[rel=\"stylesheet\"]:not([data-cdn-ignore])`,\n attribute: \"href\",\n },\n {\n selector: `img[src]:not([data-cdn-ignore])`,\n attribute: \"src\",\n },\n {\n selector: `img[srcset]:not([data-cdn-ignore])`,\n attribute: \"srcset\",\n attributeParser(\n attr: string,\n transformFunction: HtmlAttributeStreamTransformer.TransformFunction,\n ): string {\n return attr\n .split(\",\")\n .map((imgInfo) => imgInfo.replace(/([^ ]+)/, transformFunction))\n .join(\",\");\n },\n },\n ],\n } as const;\n}\n"],"mappings":";;;;;;;;AAsBA,IAAM,gBAAN,MAAM,cAAc;CAClB,OAAO,sBACL,GACyD;EACzD,OACE,KAAK,QACL,OAAO,MAAM,YACb,cAAc,KACd,eAAe,KACf,OAAO,EAAE,aAAa,YACtB,OAAO,EAAE,aAAa,YACtB,OAAO,EAAE,cAAc;CAE3B;CAEA,OAAO,uBACL,GAC2D;EAC3D,OACE,MAAM,QAAQ,CAAC,KACf,EAAE,SAAS,KACX,cAAc,sBAAsB,EAAE,EAAE;CAE5C;CAEA,OAAO,mBAAmB,UAAmB,aAA+B;EAC1E,IACE,cAAc,uBAAuB,QAAQ,KAC7C,cAAc,uBAAuB,QAAQ,GAS7C,OAPe,QACb,UACA,WACC,UACC,GAAG,MAAM,SAAS,KAAK,MAAM,WAGrB;OAEZ;CAEJ;CAEA,OAAO,MACL,QACA,SACA,SACA,SACe;EACf,OAAO,UACL,QACA,SACA,SACA,SACA,cAAc,eAChB;CACF;AACF;;;;;;;AAQA,IAAa,iBAAb,MAAa,uBACHA,+BACV;CACE,AAAO;CAEP,YAAY,YAAmC;EAC7C,MACE,cAAc,MACZ,CAAC,GACD,eAAeC,+CACf;GACE,sBAAsB,CAAC;GACvB,oBAAoB,cAClB,KAAKC,iBAAiB,SAAS;EACnC,GACA,EACE,sBAAsB,WAAW,qBACnC,CACF,CACF;EAEA,KAAK,aAAa,MAChB,EACE,sBAAsB,CAAC,EACzB,GACA,eAAeC,oBACf,UACF;EAEA,IAAI,OAAO,KAAK,WAAW,WAAW,UACpC,MAAM,IAAI,MAAM,2BAA2B;CAE/C;CAEA,iBAAiB,QAAwB;EACvC,OAAO,KAAK,WAAW,kBACrB,KAAK,WAAW,QAChB,QACA,KAAK,WAAW,UAClB;CACF;CAEA,OAAOA,qBAAqB;EAC1B,YAAY;EACZ,mBAAmBC;CACrB;CAEA,OAAc,2BACZ,eAAeD,mBAAmB;CAEpC,OAAOF,gDAAgD;EACrD,mCAAmC;EAGnC,sBAAsB;GACpB;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;GACb;GACA;IACE,UAAU;IACV,WAAW;IACX,gBACE,MACA,mBACQ;KACR,OAAO,KACJ,MAAM,GAAG,CAAC,CACV,KAAK,YAAY,QAAQ,QAAQ,WAAW,iBAAiB,CAAC,CAAC,CAC/D,KAAK,GAAG;IACb;GACF;EACF;CACF;AACF"}
@@ -18,7 +18,7 @@ interface AttributeParsers {
18
18
  [index: string]: AttributeParser;
19
19
  }
20
20
  /**
21
- * Wraps a node-trumpet Transform stream, w/ HTML as input and
21
+ * Wraps an HtmlRewriteStream, w/ HTML as input and
22
22
  * HTML as output. The output will have all non-absolute-URL
23
23
  * attributes that identify CDN-able resources converted to
24
24
  * use the given CDN URL.
@@ -1,9 +1,9 @@
1
1
  import HtmlTransformer from "./HtmlTransformer.js";
2
- import { groupBy } from "lodash";
2
+ import { groupBy } from "lodash-es";
3
3
 
4
4
  //#region src/HtmlAttributeStreamTransformer.ts
5
5
  /**
6
- * Wraps a node-trumpet Transform stream, w/ HTML as input and
6
+ * Wraps an HtmlRewriteStream, w/ HTML as input and
7
7
  * HTML as output. The output will have all non-absolute-URL
8
8
  * attributes that identify CDN-able resources converted to
9
9
  * use the given CDN URL.
@@ -1 +1 @@
1
- {"version":3,"file":"HtmlAttributeStreamTransformer.js","names":[],"sources":["../src/HtmlAttributeStreamTransformer.ts"],"sourcesContent":["import { groupBy } from \"lodash\";\nimport HtmlTransformer from \"./HtmlTransformer.js\";\n\nexport interface HtmlAttributeStreamTransformerOptions {\n transformDefinitions: TransformDefinition[];\n transformFunction: TransformFunction;\n attributeToMarkElementToBeIgnored?: string;\n}\n\nexport interface TransformDefinition {\n selector: string;\n attribute: string;\n attributeParser?:\n | undefined\n | ((oldAttribute: string, transformFunction: TransformFunction) => string);\n}\n\nexport type TransformFunction = (oldAttribute: string) => string;\n\nexport interface AttributeParser {\n (oldAttribute: string, transformFunction: TransformFunction): string;\n}\n\nexport interface AttributeParsers {\n [index: string]: AttributeParser;\n}\n\n/**\n * Wraps a node-trumpet Transform stream, w/ HTML as input and\n * HTML as output. The output will have all non-absolute-URL\n * attributes that identify CDN-able resources converted to\n * use the given CDN URL.\n */\nexport class HtmlAttributeStreamTransformer extends HtmlTransformer {\n public condensedTransformOptions: TransformDefinition[] = [];\n public options: HtmlAttributeStreamTransformerOptions;\n\n constructor(options: HtmlAttributeStreamTransformerOptions) {\n super();\n\n this.options = options;\n\n this.processTransforms();\n\n this.applyTransforms();\n }\n\n static attributeParsers: AttributeParsers = {\n default: (attr, transformFunction) => transformFunction(attr),\n };\n\n protected applyTransforms(): void {\n this.condensedTransformOptions.forEach((settings) => {\n const selector = settings.selector;\n const attribute = settings.attribute;\n const parser = settings.attributeParser;\n\n this.stream.selectAll(selector, (elem) => {\n const oldAttributeValue = elem.getAttribute(attribute);\n\n // Check that the attribute is set (which should always be the case) and the\n // typeof=string, which will be false in the case of empty attributes (which\n // cause getAttribute() to return the boolean true)\n if (typeof oldAttributeValue === \"string\") {\n const newAttributeValue =\n parser && parser(oldAttributeValue, this.options.transformFunction);\n if (newAttributeValue && newAttributeValue !== oldAttributeValue) {\n elem.setAttribute(attribute, newAttributeValue);\n }\n }\n });\n });\n\n // Run cleanup transform last\n const { attributeToMarkElementToBeIgnored } = this.options;\n if (attributeToMarkElementToBeIgnored) {\n this.stream.selectAll(\n `[${attributeToMarkElementToBeIgnored}]`,\n (elem) => {\n elem.removeAttribute(attributeToMarkElementToBeIgnored);\n },\n );\n }\n }\n\n private processTransforms(): void {\n this.options.transformDefinitions.forEach((option) => {\n if (!option.attributeParser) {\n option.attributeParser =\n HtmlAttributeStreamTransformer.attributeParsers[\"default\"];\n }\n\n if (typeof option.selector !== \"string\") {\n throw new Error(`Invalid selector specified.`);\n }\n\n if (typeof option.attribute !== \"string\") {\n throw new Error(`Invalid attribute specified.`);\n }\n\n if (!(option.attributeParser instanceof Function)) {\n throw new Error(\"Invalid attributeParser specified.\");\n }\n });\n\n /**\n * As a performance enhancement, we can condense the list of transformDefinitions such that\n * selectors with the same attribute and attributeParser can be combined into a single,\n * comma-delimited CSS selector.\n */\n const groupedTransformOptions = groupBy(\n this.options.transformDefinitions,\n (obj) => {\n return [obj.attribute, obj.attributeParser];\n },\n );\n\n this.condensedTransformOptions = Object.values(groupedTransformOptions).map(\n (likeTransformOptions) => {\n const attribute = likeTransformOptions[0]!.attribute;\n const attributeParser = likeTransformOptions[0]!.attributeParser;\n\n return {\n attribute,\n attributeParser,\n selector: likeTransformOptions\n .map((likeTransformOption) => likeTransformOption.selector)\n .join(\", \"),\n };\n },\n );\n }\n}\n"],"mappings":";;;;;;;;;;AAiCA,IAAa,iCAAb,MAAa,uCAAuC,gBAAgB;CAClE,AAAO,4BAAmD,CAAC;CAC3D,AAAO;CAEP,YAAY,SAAgD;EAC1D,MAAM;EAEN,KAAK,UAAU;EAEf,KAAK,kBAAkB;EAEvB,KAAK,gBAAgB;CACvB;CAEA,OAAO,mBAAqC,EAC1C,UAAU,MAAM,sBAAsB,kBAAkB,IAAI,EAC9D;CAEA,AAAU,kBAAwB;EAChC,KAAK,0BAA0B,SAAS,aAAa;GACnD,MAAM,WAAW,SAAS;GAC1B,MAAM,YAAY,SAAS;GAC3B,MAAM,SAAS,SAAS;GAExB,KAAK,OAAO,UAAU,WAAW,SAAS;IACxC,MAAM,oBAAoB,KAAK,aAAa,SAAS;IAKrD,IAAI,OAAO,sBAAsB,UAAU;KACzC,MAAM,oBACJ,UAAU,OAAO,mBAAmB,KAAK,QAAQ,iBAAiB;KACpE,IAAI,qBAAqB,sBAAsB,mBAC7C,KAAK,aAAa,WAAW,iBAAiB;IAElD;GACF,CAAC;EACH,CAAC;EAGD,MAAM,EAAE,sCAAsC,KAAK;EACnD,IAAI,mCACF,KAAK,OAAO,UACV,IAAI,kCAAkC,KACrC,SAAS;GACR,KAAK,gBAAgB,iCAAiC;EACxD,CACF;CAEJ;CAEA,AAAQ,oBAA0B;EAChC,KAAK,QAAQ,qBAAqB,SAAS,WAAW;GACpD,IAAI,CAAC,OAAO,iBACV,OAAO,kBACL,+BAA+B,iBAAiB;GAGpD,IAAI,OAAO,OAAO,aAAa,UAC7B,MAAM,IAAI,MAAM,6BAA6B;GAG/C,IAAI,OAAO,OAAO,cAAc,UAC9B,MAAM,IAAI,MAAM,8BAA8B;GAGhD,IAAI,EAAE,OAAO,2BAA2B,WACtC,MAAM,IAAI,MAAM,oCAAoC;EAExD,CAAC;;;;;;EAOD,MAAM,0BAA0B,QAC9B,KAAK,QAAQ,uBACZ,QAAQ;GACP,OAAO,CAAC,IAAI,WAAW,IAAI,eAAe;EAC5C,CACF;EAEA,KAAK,4BAA4B,OAAO,OAAO,uBAAuB,CAAC,CAAC,KACrE,yBAAyB;GAIxB,OAAO;IACL,WAJgB,qBAAqB,EAAE,CAAE;IAKzC,iBAJsB,qBAAqB,EAAE,CAAE;IAK/C,UAAU,qBACP,KAAK,wBAAwB,oBAAoB,QAAQ,CAAC,CAC1D,KAAK,IAAI;GACd;EACF,CACF;CACF;AACF"}
1
+ {"version":3,"file":"HtmlAttributeStreamTransformer.js","names":[],"sources":["../src/HtmlAttributeStreamTransformer.ts"],"sourcesContent":["import { groupBy } from \"lodash-es\";\nimport HtmlTransformer from \"./HtmlTransformer.js\";\n\nexport interface HtmlAttributeStreamTransformerOptions {\n transformDefinitions: TransformDefinition[];\n transformFunction: TransformFunction;\n attributeToMarkElementToBeIgnored?: string;\n}\n\nexport interface TransformDefinition {\n selector: string;\n attribute: string;\n attributeParser?:\n | undefined\n | ((oldAttribute: string, transformFunction: TransformFunction) => string);\n}\n\nexport type TransformFunction = (oldAttribute: string) => string;\n\nexport interface AttributeParser {\n (oldAttribute: string, transformFunction: TransformFunction): string;\n}\n\nexport interface AttributeParsers {\n [index: string]: AttributeParser;\n}\n\n/**\n * Wraps an HtmlRewriteStream, w/ HTML as input and\n * HTML as output. The output will have all non-absolute-URL\n * attributes that identify CDN-able resources converted to\n * use the given CDN URL.\n */\nexport class HtmlAttributeStreamTransformer extends HtmlTransformer {\n public condensedTransformOptions: TransformDefinition[] = [];\n public options: HtmlAttributeStreamTransformerOptions;\n\n constructor(options: HtmlAttributeStreamTransformerOptions) {\n super();\n\n this.options = options;\n\n this.processTransforms();\n\n this.applyTransforms();\n }\n\n static attributeParsers: AttributeParsers = {\n default: (attr, transformFunction) => transformFunction(attr),\n };\n\n protected applyTransforms(): void {\n this.condensedTransformOptions.forEach((settings) => {\n const selector = settings.selector;\n const attribute = settings.attribute;\n const parser = settings.attributeParser;\n\n this.stream.selectAll(selector, (elem) => {\n const oldAttributeValue = elem.getAttribute(attribute);\n\n // Check that the attribute is set (which should always be the case) and the\n // typeof=string, which will be false in the case of empty attributes (which\n // cause getAttribute() to return the boolean true)\n if (typeof oldAttributeValue === \"string\") {\n const newAttributeValue =\n parser && parser(oldAttributeValue, this.options.transformFunction);\n if (newAttributeValue && newAttributeValue !== oldAttributeValue) {\n elem.setAttribute(attribute, newAttributeValue);\n }\n }\n });\n });\n\n // Run cleanup transform last\n const { attributeToMarkElementToBeIgnored } = this.options;\n if (attributeToMarkElementToBeIgnored) {\n this.stream.selectAll(\n `[${attributeToMarkElementToBeIgnored}]`,\n (elem) => {\n elem.removeAttribute(attributeToMarkElementToBeIgnored);\n },\n );\n }\n }\n\n private processTransforms(): void {\n this.options.transformDefinitions.forEach((option) => {\n if (!option.attributeParser) {\n option.attributeParser =\n HtmlAttributeStreamTransformer.attributeParsers[\"default\"];\n }\n\n if (typeof option.selector !== \"string\") {\n throw new Error(`Invalid selector specified.`);\n }\n\n if (typeof option.attribute !== \"string\") {\n throw new Error(`Invalid attribute specified.`);\n }\n\n if (!(option.attributeParser instanceof Function)) {\n throw new Error(\"Invalid attributeParser specified.\");\n }\n });\n\n /**\n * As a performance enhancement, we can condense the list of transformDefinitions such that\n * selectors with the same attribute and attributeParser can be combined into a single,\n * comma-delimited CSS selector.\n */\n const groupedTransformOptions = groupBy(\n this.options.transformDefinitions,\n (obj) => {\n return [obj.attribute, obj.attributeParser];\n },\n );\n\n this.condensedTransformOptions = Object.values(groupedTransformOptions).map(\n (likeTransformOptions) => {\n const attribute = likeTransformOptions[0]!.attribute;\n const attributeParser = likeTransformOptions[0]!.attributeParser;\n\n return {\n attribute,\n attributeParser,\n selector: likeTransformOptions\n .map((likeTransformOption) => likeTransformOption.selector)\n .join(\", \"),\n };\n },\n );\n }\n}\n"],"mappings":";;;;;;;;;;AAiCA,IAAa,iCAAb,MAAa,uCAAuC,gBAAgB;CAClE,AAAO,4BAAmD,CAAC;CAC3D,AAAO;CAEP,YAAY,SAAgD;EAC1D,MAAM;EAEN,KAAK,UAAU;EAEf,KAAK,kBAAkB;EAEvB,KAAK,gBAAgB;CACvB;CAEA,OAAO,mBAAqC,EAC1C,UAAU,MAAM,sBAAsB,kBAAkB,IAAI,EAC9D;CAEA,AAAU,kBAAwB;EAChC,KAAK,0BAA0B,SAAS,aAAa;GACnD,MAAM,WAAW,SAAS;GAC1B,MAAM,YAAY,SAAS;GAC3B,MAAM,SAAS,SAAS;GAExB,KAAK,OAAO,UAAU,WAAW,SAAS;IACxC,MAAM,oBAAoB,KAAK,aAAa,SAAS;IAKrD,IAAI,OAAO,sBAAsB,UAAU;KACzC,MAAM,oBACJ,UAAU,OAAO,mBAAmB,KAAK,QAAQ,iBAAiB;KACpE,IAAI,qBAAqB,sBAAsB,mBAC7C,KAAK,aAAa,WAAW,iBAAiB;IAElD;GACF,CAAC;EACH,CAAC;EAGD,MAAM,EAAE,sCAAsC,KAAK;EACnD,IAAI,mCACF,KAAK,OAAO,UACV,IAAI,kCAAkC,KACrC,SAAS;GACR,KAAK,gBAAgB,iCAAiC;EACxD,CACF;CAEJ;CAEA,AAAQ,oBAA0B;EAChC,KAAK,QAAQ,qBAAqB,SAAS,WAAW;GACpD,IAAI,CAAC,OAAO,iBACV,OAAO,kBACL,+BAA+B,iBAAiB;GAGpD,IAAI,OAAO,OAAO,aAAa,UAC7B,MAAM,IAAI,MAAM,6BAA6B;GAG/C,IAAI,OAAO,OAAO,cAAc,UAC9B,MAAM,IAAI,MAAM,8BAA8B;GAGhD,IAAI,EAAE,OAAO,2BAA2B,WACtC,MAAM,IAAI,MAAM,oCAAoC;EAExD,CAAC;;;;;;EAOD,MAAM,0BAA0B,QAC9B,KAAK,QAAQ,uBACZ,QAAQ;GACP,OAAO,CAAC,IAAI,WAAW,IAAI,eAAe;EAC5C,CACF;EAEA,KAAK,4BAA4B,OAAO,OAAO,uBAAuB,CAAC,CAAC,KACrE,yBAAyB;GAIxB,OAAO;IACL,WAJgB,qBAAqB,EAAE,CAAE;IAKzC,iBAJsB,qBAAqB,EAAE,CAAE;IAK/C,UAAU,qBACP,KAAK,wBAAwB,oBAAoB,QAAQ,CAAC,CAC1D,KAAK,IAAI;GACd;EACF,CACF;CACF;AACF"}
@@ -0,0 +1,48 @@
1
+ import { RewritingStream } from "parse5-html-rewriting-stream";
2
+ //#region src/HtmlRewriteStream.d.ts
3
+ /**
4
+ * The subset of a DOM element that transforms are allowed to touch.
5
+ *
6
+ * `getAttribute` reports a valueless attribute (`<img data-cdn-ignore>`) as
7
+ * `true` rather than `""`, so callers can tell it apart from an attribute
8
+ * holding a real value.
9
+ */
10
+ interface SelectedElement {
11
+ getAttribute(name: string): string | true | undefined;
12
+ setAttribute(name: string, value: string): void;
13
+ removeAttribute(name: string): void;
14
+ }
15
+ type SelectionHandler = (element: SelectedElement) => void;
16
+ /**
17
+ * An HTML-in, HTML-out transform stream that rewrites attributes on elements
18
+ * matching a CSS selector.
19
+ *
20
+ * Anything the selectors don't match passes through byte-for-byte; a matched
21
+ * start tag is re-serialized, so its internal whitespace collapses and a
22
+ * self-closing `/` is dropped. That asymmetry is deliberate — it reproduces the
23
+ * observable behavior of `@gofunky/trumpet`, which this replaced.
24
+ *
25
+ * Selectors are evaluated against each start tag on its own, so combinators
26
+ * (descendant, child, sibling) will not match. Every selector this package
27
+ * ships, and every selector its tests exercise, is element-local: a tag name
28
+ * plus attribute predicates, optionally negated with `:not()`.
29
+ */
30
+ declare class HtmlRewriteStream extends RewritingStream {
31
+ #private;
32
+ constructor();
33
+ /**
34
+ * Register a handler to run against every element matching `selector`.
35
+ * Must be called before any data is written to the stream.
36
+ */
37
+ selectAll(selector: string, handler: SelectionHandler): void;
38
+ /**
39
+ * parse5's parser accepts only strings, but callers pipe Buffers in. Decode
40
+ * through a StringDecoder rather than `chunk.toString()`, so a multi-byte
41
+ * character split across two chunks survives the boundary.
42
+ */
43
+ override _transform(chunk: string | Buffer, encoding: string, callback: (error?: Error | null, data?: string) => void): void;
44
+ override _final(callback: (error?: Error | null, data?: string) => void): void;
45
+ }
46
+ //#endregion
47
+ export { HtmlRewriteStream, SelectedElement, SelectionHandler };
48
+ //# sourceMappingURL=HtmlRewriteStream.d.ts.map
@@ -0,0 +1,85 @@
1
+ import { RewritingStream } from "parse5-html-rewriting-stream";
2
+ import { Element } from "domhandler";
3
+ import { compile } from "css-select";
4
+ import { StringDecoder } from "node:string_decoder";
5
+
6
+ //#region src/HtmlRewriteStream.ts
7
+ /**
8
+ * An HTML-in, HTML-out transform stream that rewrites attributes on elements
9
+ * matching a CSS selector.
10
+ *
11
+ * Anything the selectors don't match passes through byte-for-byte; a matched
12
+ * start tag is re-serialized, so its internal whitespace collapses and a
13
+ * self-closing `/` is dropped. That asymmetry is deliberate — it reproduces the
14
+ * observable behavior of `@gofunky/trumpet`, which this replaced.
15
+ *
16
+ * Selectors are evaluated against each start tag on its own, so combinators
17
+ * (descendant, child, sibling) will not match. Every selector this package
18
+ * ships, and every selector its tests exercise, is element-local: a tag name
19
+ * plus attribute predicates, optionally negated with `:not()`.
20
+ */
21
+ var HtmlRewriteStream = class extends RewritingStream {
22
+ #selections = [];
23
+ #decoder = new StringDecoder("utf8");
24
+ constructor() {
25
+ super();
26
+ this.on("startTag", (token, raw) => {
27
+ let dirty = false;
28
+ const element = {
29
+ getAttribute(name) {
30
+ const attr = token.attrs.find((a) => a.name === name);
31
+ if (attr == null) return;
32
+ return attr.value === "" ? true : attr.value;
33
+ },
34
+ setAttribute(name, value) {
35
+ const attr = token.attrs.find((a) => a.name === name);
36
+ if (attr) attr.value = value;
37
+ else token.attrs.push({
38
+ name,
39
+ value
40
+ });
41
+ dirty = true;
42
+ },
43
+ removeAttribute(name) {
44
+ const before = token.attrs.length;
45
+ token.attrs = token.attrs.filter((a) => a.name !== name);
46
+ if (token.attrs.length !== before) dirty = true;
47
+ }
48
+ };
49
+ const node = new Element(token.tagName, Object.fromEntries(token.attrs.map((a) => [a.name, a.value])));
50
+ for (const { match, handler } of this.#selections) if (match(node)) handler(element);
51
+ if (dirty) {
52
+ token.selfClosing = false;
53
+ this.emitStartTag(token);
54
+ } else this.emitRaw(raw);
55
+ });
56
+ }
57
+ /**
58
+ * Register a handler to run against every element matching `selector`.
59
+ * Must be called before any data is written to the stream.
60
+ */
61
+ selectAll(selector, handler) {
62
+ this.#selections.push({
63
+ match: compile(selector),
64
+ handler
65
+ });
66
+ }
67
+ /**
68
+ * parse5's parser accepts only strings, but callers pipe Buffers in. Decode
69
+ * through a StringDecoder rather than `chunk.toString()`, so a multi-byte
70
+ * character split across two chunks survives the boundary.
71
+ */
72
+ _transform(chunk, encoding, callback) {
73
+ const text = typeof chunk === "string" ? chunk : this.#decoder.write(chunk);
74
+ super._transform(text, encoding, callback);
75
+ }
76
+ _final(callback) {
77
+ const rest = this.#decoder.end();
78
+ if (rest !== "") this.push(this._transformChunk(rest));
79
+ super._final(callback);
80
+ }
81
+ };
82
+
83
+ //#endregion
84
+ export { HtmlRewriteStream };
85
+ //# sourceMappingURL=HtmlRewriteStream.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HtmlRewriteStream.js","names":["#selections","#decoder"],"sources":["../src/HtmlRewriteStream.ts"],"sourcesContent":["import { RewritingStream } from \"parse5-html-rewriting-stream\";\nimport { Element } from \"domhandler\";\nimport { compile } from \"css-select\";\nimport { StringDecoder } from \"node:string_decoder\";\nimport type { Element as DomElement } from \"domhandler\";\n\n/**\n * css-select's own `CompiledQuery` isn't re-exported from its entry point, but\n * it is structurally just a predicate over a node.\n */\ntype CompiledSelector = (node: DomElement) => boolean;\n\n/**\n * The subset of a DOM element that transforms are allowed to touch.\n *\n * `getAttribute` reports a valueless attribute (`<img data-cdn-ignore>`) as\n * `true` rather than `\"\"`, so callers can tell it apart from an attribute\n * holding a real value.\n */\nexport interface SelectedElement {\n getAttribute(name: string): string | true | undefined;\n setAttribute(name: string, value: string): void;\n removeAttribute(name: string): void;\n}\n\nexport type SelectionHandler = (element: SelectedElement) => void;\n\ninterface Selection {\n match: CompiledSelector;\n handler: SelectionHandler;\n}\n\n/**\n * An HTML-in, HTML-out transform stream that rewrites attributes on elements\n * matching a CSS selector.\n *\n * Anything the selectors don't match passes through byte-for-byte; a matched\n * start tag is re-serialized, so its internal whitespace collapses and a\n * self-closing `/` is dropped. That asymmetry is deliberate — it reproduces the\n * observable behavior of `@gofunky/trumpet`, which this replaced.\n *\n * Selectors are evaluated against each start tag on its own, so combinators\n * (descendant, child, sibling) will not match. Every selector this package\n * ships, and every selector its tests exercise, is element-local: a tag name\n * plus attribute predicates, optionally negated with `:not()`.\n */\nexport class HtmlRewriteStream extends RewritingStream {\n readonly #selections: Selection[] = [];\n readonly #decoder = new StringDecoder(\"utf8\");\n\n constructor() {\n super();\n\n this.on(\"startTag\", (token, raw) => {\n let dirty = false;\n\n const element: SelectedElement = {\n getAttribute(name) {\n const attr = token.attrs.find((a) => a.name === name);\n if (attr == null) {\n return undefined;\n }\n return attr.value === \"\" ? true : attr.value;\n },\n setAttribute(name, value) {\n const attr = token.attrs.find((a) => a.name === name);\n if (attr) {\n attr.value = value;\n } else {\n token.attrs.push({ name, value });\n }\n dirty = true;\n },\n removeAttribute(name) {\n const before = token.attrs.length;\n token.attrs = token.attrs.filter((a) => a.name !== name);\n if (token.attrs.length !== before) {\n dirty = true;\n }\n },\n };\n\n // css-select needs a domhandler node. Building a throwaway one per start\n // tag keeps this independent of any parse tree.\n const node = new Element(\n token.tagName,\n Object.fromEntries(token.attrs.map((a) => [a.name, a.value])),\n );\n\n for (const { match, handler } of this.#selections) {\n if (match(node)) handler(element);\n }\n\n if (dirty) {\n // trumpet emitted `<img>` for `<img />`; parse5 would keep the marker.\n token.selfClosing = false;\n this.emitStartTag(token);\n } else {\n // Untouched tags go out exactly as they came in, quirks and all.\n this.emitRaw(raw);\n }\n });\n }\n\n /**\n * Register a handler to run against every element matching `selector`.\n * Must be called before any data is written to the stream.\n */\n selectAll(selector: string, handler: SelectionHandler): void {\n this.#selections.push({ match: compile(selector), handler });\n }\n\n /**\n * parse5's parser accepts only strings, but callers pipe Buffers in. Decode\n * through a StringDecoder rather than `chunk.toString()`, so a multi-byte\n * character split across two chunks survives the boundary.\n */\n override _transform(\n chunk: string | Buffer,\n encoding: string,\n callback: (error?: Error | null, data?: string) => void,\n ): void {\n const text = typeof chunk === \"string\" ? chunk : this.#decoder.write(chunk);\n super._transform(text, encoding, callback);\n }\n\n override _final(\n callback: (error?: Error | null, data?: string) => void,\n ): void {\n // Anything the decoder is still holding is an incomplete sequence at end\n // of input; `end()` turns it into replacement characters. Push it through\n // the parser before finishing rather than dropping it.\n const rest = this.#decoder.end();\n if (rest !== \"\") this.push(this._transformChunk(rest));\n super._final(callback);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA8CA,IAAa,oBAAb,cAAuC,gBAAgB;CACrD,AAASA,cAA2B,CAAC;CACrC,AAASC,WAAW,IAAI,cAAc,MAAM;CAE5C,cAAc;EACZ,MAAM;EAEN,KAAK,GAAG,aAAa,OAAO,QAAQ;GAClC,IAAI,QAAQ;GAEZ,MAAM,UAA2B;IAC/B,aAAa,MAAM;KACjB,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI;KACpD,IAAI,QAAQ,MACV;KAEF,OAAO,KAAK,UAAU,KAAK,OAAO,KAAK;IACzC;IACA,aAAa,MAAM,OAAO;KACxB,MAAM,OAAO,MAAM,MAAM,MAAM,MAAM,EAAE,SAAS,IAAI;KACpD,IAAI,MACF,KAAK,QAAQ;UAEb,MAAM,MAAM,KAAK;MAAE;MAAM;KAAM,CAAC;KAElC,QAAQ;IACV;IACA,gBAAgB,MAAM;KACpB,MAAM,SAAS,MAAM,MAAM;KAC3B,MAAM,QAAQ,MAAM,MAAM,QAAQ,MAAM,EAAE,SAAS,IAAI;KACvD,IAAI,MAAM,MAAM,WAAW,QACzB,QAAQ;IAEZ;GACF;GAIA,MAAM,OAAO,IAAI,QACf,MAAM,SACN,OAAO,YAAY,MAAM,MAAM,KAAK,MAAM,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAC9D;GAEA,KAAK,MAAM,EAAE,OAAO,aAAa,KAAKD,aACpC,IAAI,MAAM,IAAI,GAAG,QAAQ,OAAO;GAGlC,IAAI,OAAO;IAET,MAAM,cAAc;IACpB,KAAK,aAAa,KAAK;GACzB,OAEE,KAAK,QAAQ,GAAG;EAEpB,CAAC;CACH;;;;;CAMA,UAAU,UAAkB,SAAiC;EAC3D,KAAKA,YAAY,KAAK;GAAE,OAAO,QAAQ,QAAQ;GAAG;EAAQ,CAAC;CAC7D;;;;;;CAOA,AAAS,WACP,OACA,UACA,UACM;EACN,MAAM,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAKC,SAAS,MAAM,KAAK;EAC1E,MAAM,WAAW,MAAM,UAAU,QAAQ;CAC3C;CAEA,AAAS,OACP,UACM;EAIN,MAAM,OAAO,KAAKA,SAAS,IAAI;EAC/B,IAAI,SAAS,IAAI,KAAK,KAAK,KAAK,gBAAgB,IAAI,CAAC;EACrD,MAAM,OAAO,QAAQ;CACvB;AACF"}
@@ -1,18 +1,16 @@
1
- import { Trumpet } from "@gofunky/trumpet";
1
+ import { HtmlRewriteStream, SelectedElement, SelectionHandler } from "./HtmlRewriteStream.js";
2
2
  //#region src/HtmlTransformer.d.ts
3
- type TrumpetTransformStream = Trumpet;
4
3
  declare class HtmlTransformer {
5
4
  private transformStream;
6
- constructor();
7
5
  /**
8
6
  * Get the underlying transform stream
9
7
  */
10
- get stream(): Trumpet;
8
+ get stream(): HtmlRewriteStream;
11
9
  /**
12
10
  * Get a promise to the complete output Buffer (alternative API to dealing with stream)
13
11
  */
14
12
  get outputBufferPromise(): Promise<Buffer>;
15
13
  }
16
14
  //#endregion
17
- export { TrumpetTransformStream, HtmlTransformer as default };
15
+ export { HtmlRewriteStream, type SelectedElement, type SelectionHandler, HtmlTransformer as default };
18
16
  //# sourceMappingURL=HtmlTransformer.d.ts.map
@@ -1,12 +1,9 @@
1
- import trumpet from "@gofunky/trumpet";
2
- import streamToPromise from "stream-to-promise";
1
+ import { HtmlRewriteStream } from "./HtmlRewriteStream.js";
2
+ import { buffer } from "node:stream/consumers";
3
3
 
4
4
  //#region src/HtmlTransformer.ts
5
5
  var HtmlTransformer = class {
6
- transformStream;
7
- constructor() {
8
- this.transformStream = trumpet();
9
- }
6
+ transformStream = new HtmlRewriteStream();
10
7
  /**
11
8
  * Get the underlying transform stream
12
9
  */
@@ -17,10 +14,10 @@ var HtmlTransformer = class {
17
14
  * Get a promise to the complete output Buffer (alternative API to dealing with stream)
18
15
  */
19
16
  get outputBufferPromise() {
20
- return streamToPromise(this.stream);
17
+ return buffer(this.stream);
21
18
  }
22
19
  };
23
20
 
24
21
  //#endregion
25
- export { HtmlTransformer as default };
22
+ export { HtmlRewriteStream, HtmlTransformer as default };
26
23
  //# sourceMappingURL=HtmlTransformer.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"HtmlTransformer.js","names":[],"sources":["../src/HtmlTransformer.ts"],"sourcesContent":["import trumpet, { type Trumpet } from \"@gofunky/trumpet\";\nimport streamToPromise from \"stream-to-promise\";\n\nexport type TrumpetTransformStream = Trumpet;\n\nexport default class HtmlTransformer {\n private transformStream: TrumpetTransformStream;\n\n constructor(/*public inputPath: string*/) {\n this.transformStream = trumpet();\n }\n\n /**\n * Get the underlying transform stream\n */\n get stream(): Trumpet {\n return this.transformStream;\n }\n\n /**\n * Get a promise to the complete output Buffer (alternative API to dealing with stream)\n */\n get outputBufferPromise(): Promise<Buffer> {\n return streamToPromise(this.stream);\n }\n}\n"],"mappings":";;;;AAKA,IAAqB,kBAArB,MAAqC;CACnC,AAAQ;CAER,cAA0C;EACxC,KAAK,kBAAkB,QAAQ;CACjC;;;;CAKA,IAAI,SAAkB;EACpB,OAAO,KAAK;CACd;;;;CAKA,IAAI,sBAAuC;EACzC,OAAO,gBAAgB,KAAK,MAAM;CACpC;AACF"}
1
+ {"version":3,"file":"HtmlTransformer.js","names":[],"sources":["../src/HtmlTransformer.ts"],"sourcesContent":["import { buffer } from \"node:stream/consumers\";\nimport { HtmlRewriteStream } from \"./HtmlRewriteStream.js\";\n\nexport type { SelectedElement, SelectionHandler } from \"./HtmlRewriteStream.js\";\nexport { HtmlRewriteStream } from \"./HtmlRewriteStream.js\";\n\nexport default class HtmlTransformer {\n private transformStream: HtmlRewriteStream = new HtmlRewriteStream();\n\n /**\n * Get the underlying transform stream\n */\n get stream(): HtmlRewriteStream {\n return this.transformStream;\n }\n\n /**\n * Get a promise to the complete output Buffer (alternative API to dealing with stream)\n */\n get outputBufferPromise(): Promise<Buffer> {\n return buffer(this.stream);\n }\n}\n"],"mappings":";;;;AAMA,IAAqB,kBAArB,MAAqC;CACnC,AAAQ,kBAAqC,IAAI,kBAAkB;;;;CAKnE,IAAI,SAA4B;EAC9B,OAAO,KAAK;CACd;;;;CAKA,IAAI,sBAAuC;EACzC,OAAO,OAAO,KAAK,MAAM;CAC3B;AACF"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@altano/html-cdnify",
3
- "version": "5.0.0",
3
+ "version": "6.0.0",
4
4
  "description": "Transform the relative URLs in your HTML markup (e.g. scripts, stylesheets, images) to use your CDN URL.",
5
5
  "keywords": [
6
6
  "cdn",
@@ -31,22 +31,22 @@
31
31
  "dist/**/*.d.ts"
32
32
  ],
33
33
  "dependencies": {
34
- "@gofunky/trumpet": "^3.0.3",
35
- "lodash": "^4.18.1",
36
- "stream-to-promise": "^3.0.0",
34
+ "css-select": "^7.0.0",
35
+ "domhandler": "^6.0.1",
36
+ "lodash-es": "^4.18.1",
37
+ "parse5-html-rewriting-stream": "^8.0.1",
37
38
  "streamifier": "^0.1.1"
38
39
  },
39
40
  "devDependencies": {
40
41
  "@arethetypeswrong/cli": "^0.18.5",
41
- "@types/lodash": "^4.17.24",
42
+ "@types/lodash-es": "^4.17.12",
42
43
  "@types/node": "^26.1.1",
43
- "@types/stream-to-promise": "^2.2.5",
44
44
  "@types/streamifier": "^0.1.2",
45
45
  "@vitest/coverage-v8": "^4.1.10",
46
46
  "depcheck": "^1.4.7",
47
47
  "eslint": "^9.39.5",
48
48
  "prettier": "3.9.6",
49
- "tsdown": "^0.22.13",
49
+ "tsdown": "^0.22.14",
50
50
  "typescript": "^6.0.3",
51
51
  "vite": "^8.1.5",
52
52
  "vitest": "^4.1.10",
@@ -55,7 +55,7 @@
55
55
  "@altano/tsconfig": "0.0.1"
56
56
  },
57
57
  "engines": {
58
- "node": ">=5.10.1"
58
+ "node": ">=16.7.0"
59
59
  },
60
60
  "publishConfig": {
61
61
  "access": "public"