@altano/html-cdnify 4.0.2 → 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 +152 -75
- package/dist/CDNTransformer.d.ts +5 -6
- package/dist/CDNTransformer.js +2 -2
- package/dist/CDNTransformer.js.map +1 -1
- package/dist/HtmlAttributeStreamTransformer.d.ts +6 -7
- package/dist/HtmlAttributeStreamTransformer.js +3 -2
- package/dist/HtmlAttributeStreamTransformer.js.map +1 -1
- package/dist/HtmlRewriteStream.d.ts +48 -0
- package/dist/HtmlRewriteStream.js +85 -0
- package/dist/HtmlRewriteStream.js.map +1 -0
- package/dist/HtmlTransformer.d.ts +7 -10
- package/dist/HtmlTransformer.js +5 -8
- package/dist/HtmlTransformer.js.map +1 -1
- package/dist/cdnify.d.ts +2 -3
- package/dist/cdnify.js.map +1 -1
- package/dist/urlConverter.d.ts +2 -2
- package/dist/urlConverter.js +26 -6
- package/dist/urlConverter.js.map +1 -1
- package/package.json +20 -20
package/README.md
CHANGED
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
[![npm]
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
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
|
|
13
|
-
|
|
14
|
-
- Like [cdnify](https://www.npmjs.com/package/cdnify) but
|
|
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
|
-
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
Any URLs where only the path (and after) is specified which are found in the
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
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
|
-
|
|
70
|
+
### Won't be cdnified
|
|
55
71
|
|
|
56
|
-
| HTMLElement | Reason not cdnified
|
|
57
|
-
| ------------------------------------- |
|
|
58
|
-
| `<img src="http://foo.com/logo.png">` | Absolute URL
|
|
59
|
-
| `<img src="//foo.com/logo.png">` | Scheme-relative
|
|
60
|
-
| `<img custom="/logo.png">` |
|
|
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
|
|
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
|
-
|
|
82
|
+
## More Examples
|
|
65
83
|
|
|
66
|
-
|
|
84
|
+
### Using `bufferPath` to deal with subdirectories
|
|
67
85
|
|
|
68
|
-
You can specify `bufferPath`, the relative path to the buffer being processed.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
255
|
+
### Specifying a custom attributeParser
|
|
225
256
|
|
|
226
|
-
A custom attributeParser is like a custom transformFunction, but is scoped to
|
|
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) =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
365
|
+
## API
|
|
330
366
|
|
|
331
|
-
|
|
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
|
-
|
|
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
|
|
344
|
-
| -------- |
|
|
345
|
-
| buffer
|
|
379
|
+
| Property | Type | Required | Description |
|
|
380
|
+
| -------- | ------------------ | -------- | ------------------ |
|
|
381
|
+
| `buffer` | `Buffer \| string` | yes | Content to cdnify. |
|
|
346
382
|
|
|
347
|
-
|
|
383
|
+
### Stream-based API
|
|
348
384
|
|
|
349
|
-
|
|
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
|
-
|
|
398
|
+
#### Stream-based options
|
|
359
399
|
|
|
360
|
-
| Property
|
|
361
|
-
|
|
|
362
|
-
| cdnUrl | string
|
|
363
|
-
| bufferPath | string
|
|
364
|
-
| transformDefinitions | TransformDefinition
|
|
365
|
-
| transformFunction | CDNTransformFunction
|
|
400
|
+
| Property | Type | Required |
|
|
401
|
+
| ---------------------- | ----------------------- | -------- |
|
|
402
|
+
| `cdnUrl` | `string` | yes |
|
|
403
|
+
| `bufferPath` | `string` | no |
|
|
404
|
+
| `transformDefinitions` | `TransformDefinition[]` | no |
|
|
405
|
+
| `transformFunction` | `CDNTransformFunction` | no |
|
|
366
406
|
|
|
367
|
-
|
|
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?: (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
475
|
+
Shield (in logo) by [Flaticon](http://www.flaticon.com) from
|
|
476
|
+
[Freepik](http://www.freepik.com)
|
package/dist/CDNTransformer.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { HtmlAttributeStreamTransformer, TransformDefinition } from "./HtmlAttributeStreamTransformer.js";
|
|
2
|
-
|
|
3
2
|
//#region src/CDNTransformer.d.ts
|
|
4
3
|
type CDNTransformFunction = (cdnUrl: string, oldUrl: string, bufferPath: string) => string;
|
|
5
4
|
interface CDNTransformerOptions {
|
|
@@ -9,11 +8,11 @@ interface CDNTransformerOptions {
|
|
|
9
8
|
transformFunction?: CDNTransformFunction;
|
|
10
9
|
}
|
|
11
10
|
/**
|
|
12
|
-
* Wraps
|
|
13
|
-
* HTML as output. The output will have all non-absolute-URL
|
|
14
|
-
* attributes that identify CDN-able resources converted to
|
|
15
|
-
* use the given CDN URL.
|
|
16
|
-
*/
|
|
11
|
+
* Wraps an HtmlRewriteStream, w/ HTML as input and
|
|
12
|
+
* HTML as output. The output will have all non-absolute-URL
|
|
13
|
+
* attributes that identify CDN-able resources converted to
|
|
14
|
+
* use the given CDN URL.
|
|
15
|
+
*/
|
|
17
16
|
declare class CDNTransformer extends HtmlAttributeStreamTransformer {
|
|
18
17
|
#private;
|
|
19
18
|
cdnOptions: Required<CDNTransformerOptions>;
|
package/dist/CDNTransformer.js
CHANGED
|
@@ -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
|
|
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
|
|
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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import HtmlTransformer from "./HtmlTransformer.js";
|
|
2
|
-
|
|
3
2
|
//#region src/HtmlAttributeStreamTransformer.d.ts
|
|
4
3
|
interface HtmlAttributeStreamTransformerOptions {
|
|
5
4
|
transformDefinitions: TransformDefinition[];
|
|
@@ -19,14 +18,14 @@ interface AttributeParsers {
|
|
|
19
18
|
[index: string]: AttributeParser;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Wraps
|
|
23
|
-
* HTML as output. The output will have all non-absolute-URL
|
|
24
|
-
* attributes that identify CDN-able resources converted to
|
|
25
|
-
* use the given CDN URL.
|
|
26
|
-
*/
|
|
21
|
+
* Wraps an HtmlRewriteStream, w/ HTML as input and
|
|
22
|
+
* HTML as output. The output will have all non-absolute-URL
|
|
23
|
+
* attributes that identify CDN-able resources converted to
|
|
24
|
+
* use the given CDN URL.
|
|
25
|
+
*/
|
|
27
26
|
declare class HtmlAttributeStreamTransformer extends HtmlTransformer {
|
|
28
|
-
options: HtmlAttributeStreamTransformerOptions;
|
|
29
27
|
condensedTransformOptions: TransformDefinition[];
|
|
28
|
+
options: HtmlAttributeStreamTransformerOptions;
|
|
30
29
|
constructor(options: HtmlAttributeStreamTransformerOptions);
|
|
31
30
|
static attributeParsers: AttributeParsers;
|
|
32
31
|
protected applyTransforms(): void;
|
|
@@ -1,15 +1,16 @@
|
|
|
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
|
|
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.
|
|
10
10
|
*/
|
|
11
11
|
var HtmlAttributeStreamTransformer = class HtmlAttributeStreamTransformer extends HtmlTransformer {
|
|
12
12
|
condensedTransformOptions = [];
|
|
13
|
+
options;
|
|
13
14
|
constructor(options) {
|
|
14
15
|
super();
|
|
15
16
|
this.options = options;
|
|
@@ -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
|
|
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,19 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { HtmlRewriteStream, SelectedElement, SelectionHandler } from "./HtmlRewriteStream.js";
|
|
3
2
|
//#region src/HtmlTransformer.d.ts
|
|
4
|
-
type TrumpetTransformStream = Trumpet;
|
|
5
3
|
declare class HtmlTransformer {
|
|
6
4
|
private transformStream;
|
|
7
|
-
constructor();
|
|
8
5
|
/**
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
get stream():
|
|
6
|
+
* Get the underlying transform stream
|
|
7
|
+
*/
|
|
8
|
+
get stream(): HtmlRewriteStream;
|
|
12
9
|
/**
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
* Get a promise to the complete output Buffer (alternative API to dealing with stream)
|
|
11
|
+
*/
|
|
15
12
|
get outputBufferPromise(): Promise<Buffer>;
|
|
16
13
|
}
|
|
17
14
|
//#endregion
|
|
18
|
-
export {
|
|
15
|
+
export { HtmlRewriteStream, type SelectedElement, type SelectionHandler, HtmlTransformer as default };
|
|
19
16
|
//# sourceMappingURL=HtmlTransformer.d.ts.map
|
package/dist/HtmlTransformer.js
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
|
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
|
|
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/dist/cdnify.d.ts
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { CDNTransformerOptions } from "./CDNTransformer.js";
|
|
2
|
-
|
|
3
2
|
//#region src/cdnify.d.ts
|
|
4
3
|
interface CdnifyOptions extends CDNTransformerOptions {
|
|
5
4
|
buffer: string | Buffer;
|
|
6
5
|
}
|
|
7
6
|
/**
|
|
8
|
-
* Alternative wrapper for simple Promise-based use of library (as opposed to transform stream)
|
|
9
|
-
*/
|
|
7
|
+
* Alternative wrapper for simple Promise-based use of library (as opposed to transform stream)
|
|
8
|
+
*/
|
|
10
9
|
declare function cdnify(options: CdnifyOptions): Promise<Buffer>;
|
|
11
10
|
//#endregion
|
|
12
11
|
export { CdnifyOptions, cdnify, cdnify as default };
|
package/dist/cdnify.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cdnify.js","names":[],"sources":["../src/cdnify.ts"],"sourcesContent":["import {\n CDNTransformer,\n type CDNTransformerOptions,\n} from \"./CDNTransformer.js\";\n\nimport streamifier from \"streamifier\";\n\nexport interface CdnifyOptions extends CDNTransformerOptions {\n buffer: string | Buffer;\n}\n\n/**\n * Alternative wrapper for simple Promise-based use of library (as opposed to transform stream)\n */\nexport function cdnify(options: CdnifyOptions): Promise<Buffer> {\n if (typeof options.buffer !== \"string\") {\n options.buffer = options.buffer.toString();\n }\n\n const cdnTransformer = new CDNTransformer(options);\n\n streamifier.createReadStream(options.buffer).pipe(cdnTransformer.stream);\n\n return cdnTransformer.outputBufferPromise;\n}\n\nexport default cdnify;\n"],"mappings":";;;;;;;AAcA,SAAgB,OAAO,SAAyC;
|
|
1
|
+
{"version":3,"file":"cdnify.js","names":[],"sources":["../src/cdnify.ts"],"sourcesContent":["import {\n CDNTransformer,\n type CDNTransformerOptions,\n} from \"./CDNTransformer.js\";\n\nimport streamifier from \"streamifier\";\n\nexport interface CdnifyOptions extends CDNTransformerOptions {\n buffer: string | Buffer;\n}\n\n/**\n * Alternative wrapper for simple Promise-based use of library (as opposed to transform stream)\n */\nexport function cdnify(options: CdnifyOptions): Promise<Buffer> {\n if (typeof options.buffer !== \"string\") {\n options.buffer = options.buffer.toString();\n }\n\n const cdnTransformer = new CDNTransformer(options);\n\n streamifier.createReadStream(options.buffer).pipe(cdnTransformer.stream);\n\n return cdnTransformer.outputBufferPromise;\n}\n\nexport default cdnify;\n"],"mappings":";;;;;;;AAcA,SAAgB,OAAO,SAAyC;CAC9D,IAAI,OAAO,QAAQ,WAAW,UAC5B,QAAQ,SAAS,QAAQ,OAAO,SAAS;CAG3C,MAAM,iBAAiB,IAAI,eAAe,OAAO;CAEjD,YAAY,iBAAiB,QAAQ,MAAM,CAAC,CAAC,KAAK,eAAe,MAAM;CAEvE,OAAO,eAAe;AACxB"}
|
package/dist/urlConverter.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
//#region src/urlConverter.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* Resolve oldUrl against pathOldUrlIsRelativeTo, and then prepend newUrlBase
|
|
4
|
-
*/
|
|
3
|
+
* Resolve oldUrl against pathOldUrlIsRelativeTo, and then prepend newUrlBase
|
|
4
|
+
*/
|
|
5
5
|
declare const _default: (newUrlBase: string, oldUrl: string, pathOldUrlIsRelativeTo?: string) => string;
|
|
6
6
|
//#endregion
|
|
7
7
|
export { _default as default };
|
package/dist/urlConverter.js
CHANGED
|
@@ -1,11 +1,33 @@
|
|
|
1
|
-
import url from "node:url";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
4
3
|
//#region src/urlConverter.ts
|
|
4
|
+
/** Origin used to resolve path-only bases, which `new URL()` can't parse. */
|
|
5
|
+
const PLACEHOLDER_ORIGIN = "http://html-cdnify.invalid";
|
|
6
|
+
const SCHEME_PATTERN = /^[a-z][a-z0-9+\-.]*:/i;
|
|
5
7
|
function isLocalPath(filePath) {
|
|
6
8
|
return typeof filePath === "string" && filePath.length > 0 && filePath.indexOf("//") === -1 && filePath.indexOf("data:") !== 0;
|
|
7
9
|
}
|
|
8
10
|
/**
|
|
11
|
+
* Resolve `relativeUrl` against `base`, the way the (deprecated) legacy
|
|
12
|
+
* `url.resolve` did, but using the WHATWG URL API.
|
|
13
|
+
*
|
|
14
|
+
* `new URL()` only accepts a fully absolute base, so the two other base shapes
|
|
15
|
+
* this package supports are resolved against a stand-in that is stripped back
|
|
16
|
+
* off afterwards:
|
|
17
|
+
*
|
|
18
|
+
* - protocol-relative (`//cdn.com/cdn/`) borrows a protocol
|
|
19
|
+
* - path-only (`someDir/`) borrows a whole origin
|
|
20
|
+
*/
|
|
21
|
+
function resolveUrl(base, relativeUrl) {
|
|
22
|
+
if (base.startsWith("//")) return new URL(relativeUrl, `http:${base}`).href.slice(5);
|
|
23
|
+
else if (SCHEME_PATTERN.test(base)) return new URL(relativeUrl, base).href;
|
|
24
|
+
else {
|
|
25
|
+
const absoluteBase = new URL(base, PLACEHOLDER_ORIGIN);
|
|
26
|
+
const resolved = new URL(relativeUrl, absoluteBase).href.slice(26);
|
|
27
|
+
return base.startsWith("/") || relativeUrl.startsWith("/") ? resolved : resolved.slice(1);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
9
31
|
* Resolve oldUrl against pathOldUrlIsRelativeTo, and then prepend newUrlBase
|
|
10
32
|
*/
|
|
11
33
|
var urlConverter_default = (newUrlBase, oldUrl, pathOldUrlIsRelativeTo) => {
|
|
@@ -13,13 +35,11 @@ var urlConverter_default = (newUrlBase, oldUrl, pathOldUrlIsRelativeTo) => {
|
|
|
13
35
|
else {
|
|
14
36
|
oldUrl = oldUrl.trim();
|
|
15
37
|
let relativePath = ".";
|
|
16
|
-
if (pathOldUrlIsRelativeTo)
|
|
17
|
-
|
|
18
|
-
relativePath = url.resolve(fileDirName, oldUrl);
|
|
19
|
-
} else relativePath = oldUrl;
|
|
38
|
+
if (pathOldUrlIsRelativeTo) relativePath = resolveUrl(pathOldUrlIsRelativeTo.endsWith("/") ? pathOldUrlIsRelativeTo : path.dirname(pathOldUrlIsRelativeTo) + "/", oldUrl);
|
|
39
|
+
else relativePath = oldUrl;
|
|
20
40
|
if (relativePath.startsWith("/")) relativePath = relativePath.slice(1);
|
|
21
41
|
if (!newUrlBase.endsWith("/")) newUrlBase += "/";
|
|
22
|
-
return
|
|
42
|
+
return resolveUrl(newUrlBase, relativePath);
|
|
23
43
|
}
|
|
24
44
|
};
|
|
25
45
|
|
package/dist/urlConverter.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"urlConverter.js","names":[],"sources":["../src/urlConverter.ts"],"sourcesContent":["import
|
|
1
|
+
{"version":3,"file":"urlConverter.js","names":[],"sources":["../src/urlConverter.ts"],"sourcesContent":["import path from \"node:path\";\n\n/** Origin used to resolve path-only bases, which `new URL()` can't parse. */\nconst PLACEHOLDER_ORIGIN = \"http://html-cdnify.invalid\";\n\nconst SCHEME_PATTERN = /^[a-z][a-z0-9+\\-.]*:/i;\n\nfunction isLocalPath(filePath: string): boolean {\n return (\n typeof filePath === \"string\" &&\n filePath.length > 0 &&\n filePath.indexOf(\"//\") === -1 &&\n filePath.indexOf(\"data:\") !== 0\n );\n}\n\n/**\n * Resolve `relativeUrl` against `base`, the way the (deprecated) legacy\n * `url.resolve` did, but using the WHATWG URL API.\n *\n * `new URL()` only accepts a fully absolute base, so the two other base shapes\n * this package supports are resolved against a stand-in that is stripped back\n * off afterwards:\n *\n * - protocol-relative (`//cdn.com/cdn/`) borrows a protocol\n * - path-only (`someDir/`) borrows a whole origin\n */\nfunction resolveUrl(base: string, relativeUrl: string): string {\n if (base.startsWith(\"//\")) {\n return new URL(relativeUrl, `http:${base}`).href.slice(\"http:\".length);\n } else if (SCHEME_PATTERN.test(base)) {\n return new URL(relativeUrl, base).href;\n } else {\n const absoluteBase = new URL(base, PLACEHOLDER_ORIGIN);\n const resolved = new URL(relativeUrl, absoluteBase).href.slice(\n PLACEHOLDER_ORIGIN.length,\n );\n // The placeholder origin makes everything look rooted at `/`. Only keep\n // that slash if it was already there before resolving.\n const isRooted = base.startsWith(\"/\") || relativeUrl.startsWith(\"/\");\n return isRooted ? resolved : resolved.slice(1);\n }\n}\n\n/**\n * Resolve oldUrl against pathOldUrlIsRelativeTo, and then prepend newUrlBase\n */\nexport default (\n newUrlBase: string,\n oldUrl: string,\n pathOldUrlIsRelativeTo?: string,\n): string => {\n if (!isLocalPath(oldUrl)) {\n return oldUrl;\n } else {\n oldUrl = oldUrl.trim();\n let relativePath = \".\";\n\n if (pathOldUrlIsRelativeTo) {\n const isDirAlready = pathOldUrlIsRelativeTo.endsWith(\"/\");\n const fileDirName = isDirAlready\n ? pathOldUrlIsRelativeTo\n : path.dirname(pathOldUrlIsRelativeTo) + \"/\";\n relativePath = resolveUrl(fileDirName, oldUrl);\n } else {\n relativePath = oldUrl;\n }\n\n // When resolving against the CDN URL, we should always use a relative URL.\n //\n // e.g. http://somecdn.com/cdn/ + /article/some.css => http://somecdn.com/cdn/article/some.css\n // and *NOT*\n // http://somecdn.com/cdn/ + /article/some.css => http://somecdn.com/article/some.css\n //\n if (relativePath.startsWith(\"/\")) {\n relativePath = relativePath.slice(1);\n }\n\n if (!newUrlBase.endsWith(\"/\")) {\n newUrlBase += \"/\";\n }\n\n return resolveUrl(newUrlBase, relativePath);\n }\n};\n"],"mappings":";;;;AAGA,MAAM,qBAAqB;AAE3B,MAAM,iBAAiB;AAEvB,SAAS,YAAY,UAA2B;CAC9C,OACE,OAAO,aAAa,YACpB,SAAS,SAAS,KAClB,SAAS,QAAQ,IAAI,MAAM,MAC3B,SAAS,QAAQ,OAAO,MAAM;AAElC;;;;;;;;;;;;AAaA,SAAS,WAAW,MAAc,aAA6B;CAC7D,IAAI,KAAK,WAAW,IAAI,GACtB,OAAO,IAAI,IAAI,aAAa,QAAQ,MAAM,CAAC,CAAC,KAAK,MAAM,CAAc;MAChE,IAAI,eAAe,KAAK,IAAI,GACjC,OAAO,IAAI,IAAI,aAAa,IAAI,CAAC,CAAC;MAC7B;EACL,MAAM,eAAe,IAAI,IAAI,MAAM,kBAAkB;EACrD,MAAM,WAAW,IAAI,IAAI,aAAa,YAAY,CAAC,CAAC,KAAK,MACvD,EACF;EAIA,OADiB,KAAK,WAAW,GAAG,KAAK,YAAY,WAAW,GAAG,IACjD,WAAW,SAAS,MAAM,CAAC;CAC/C;AACF;;;;AAKA,4BACE,YACA,QACA,2BACW;CACX,IAAI,CAAC,YAAY,MAAM,GACrB,OAAO;MACF;EACL,SAAS,OAAO,KAAK;EACrB,IAAI,eAAe;EAEnB,IAAI,wBAKF,eAAe,WAJM,uBAAuB,SAAS,GACtB,IAC3B,yBACA,KAAK,QAAQ,sBAAsB,IAAI,KACJ,MAAM;OAE7C,eAAe;EASjB,IAAI,aAAa,WAAW,GAAG,GAC7B,eAAe,aAAa,MAAM,CAAC;EAGrC,IAAI,CAAC,WAAW,SAAS,GAAG,GAC1B,cAAc;EAGhB,OAAO,WAAW,YAAY,YAAY;CAC5C;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@altano/html-cdnify",
|
|
3
|
-
"version": "
|
|
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",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"license": "ISC",
|
|
18
18
|
"author": {
|
|
19
19
|
"name": "Alan Norbauer",
|
|
20
|
-
"email": "
|
|
20
|
+
"email": "alan@norbauer.com",
|
|
21
21
|
"url": "https://alan.norbauer.com"
|
|
22
22
|
},
|
|
23
23
|
"sideEffects": false,
|
|
@@ -31,45 +31,45 @@
|
|
|
31
31
|
"dist/**/*.d.ts"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
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
|
-
"@arethetypeswrong/cli": "^0.18.
|
|
41
|
-
"@types/lodash": "^4.17.
|
|
42
|
-
"@types/node": "^
|
|
43
|
-
"@types/stream-to-promise": "^2.2.5",
|
|
41
|
+
"@arethetypeswrong/cli": "^0.18.5",
|
|
42
|
+
"@types/lodash-es": "^4.17.12",
|
|
43
|
+
"@types/node": "^26.1.1",
|
|
44
44
|
"@types/streamifier": "^0.1.2",
|
|
45
|
-
"@vitest/coverage-v8": "^4.1.
|
|
45
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
46
46
|
"depcheck": "^1.4.7",
|
|
47
|
-
"eslint": "^9.39.
|
|
48
|
-
"prettier": "
|
|
49
|
-
"tsdown": "^0.
|
|
50
|
-
"typescript": "^
|
|
51
|
-
"vite": "^
|
|
52
|
-
"vitest": "^4.1.
|
|
47
|
+
"eslint": "^9.39.5",
|
|
48
|
+
"prettier": "3.9.6",
|
|
49
|
+
"tsdown": "^0.22.14",
|
|
50
|
+
"typescript": "^6.0.3",
|
|
51
|
+
"vite": "^8.1.5",
|
|
52
|
+
"vitest": "^4.1.10",
|
|
53
53
|
"@altano/build-config": "0.0.1",
|
|
54
54
|
"@altano/testing": "0.0.1",
|
|
55
55
|
"@altano/tsconfig": "0.0.1"
|
|
56
56
|
},
|
|
57
57
|
"engines": {
|
|
58
|
-
"node": ">=
|
|
58
|
+
"node": ">=16.7.0"
|
|
59
59
|
},
|
|
60
60
|
"publishConfig": {
|
|
61
61
|
"access": "public"
|
|
62
62
|
},
|
|
63
63
|
"scripts": {
|
|
64
|
-
"build": "tsdown
|
|
64
|
+
"build": "tsdown",
|
|
65
65
|
"check:exports": "attw --pack . --profile esm-only --quiet",
|
|
66
66
|
"check:types:src": "tsc --noEmit",
|
|
67
67
|
"check:types:test": "tsc --noEmit --project ./tests/tsconfig.json",
|
|
68
68
|
"check:unused-dependencies": "depcheck",
|
|
69
69
|
"clean": "rm -rf .turbo && rm -rf .tsbuildinfo && rm -rf node_modules && rm -rf dist",
|
|
70
70
|
"dev": "pnpm run build --watch",
|
|
71
|
-
"format": "prettier --check
|
|
72
|
-
"format:fix": "prettier --write
|
|
71
|
+
"format": "prettier --check .",
|
|
72
|
+
"format:fix": "prettier --write .",
|
|
73
73
|
"lint": "eslint",
|
|
74
74
|
"lint:fix": "TIMING=1 pnpm lint --fix",
|
|
75
75
|
"lint:timing": "TIMING=1 pnpm lint",
|