@occultist/extensions 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/css-parser.d.ts +21 -0
- package/dist/css-parser.js +67 -0
- package/dist/dependancy-graph.d.ts +17 -0
- package/dist/dependancy-graph.js +74 -0
- package/dist/file-info.d.ts +97 -0
- package/dist/file-info.js +66 -0
- package/dist/html-parser.d.ts +11 -0
- package/dist/html-parser.js +171 -0
- package/dist/js-parser.d.ts +14 -0
- package/dist/js-parser.js +74 -0
- package/dist/static-extension.d.ts +79 -0
- package/dist/static-extension.js +273 -0
- package/dist/ts-preprocessor.d.ts +8 -0
- package/dist/ts-preprocessor.js +69 -0
- package/dist/types.d.ts +68 -0
- package/dist/types.js +1 -0
- package/lib/static/css-parser.ts +98 -0
- package/lib/static/dependancy-graph.ts +101 -0
- package/lib/static/file-info.ts +153 -0
- package/lib/static/html-parser.ts +225 -0
- package/lib/static/js-parser.ts +107 -0
- package/lib/static/static-extension.ts +400 -0
- package/lib/static/ts-preprocessor.ts +114 -0
- package/lib/static/types.ts +120 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Matthew Quinn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FilesByURL, PolicyDirective, ReferenceDetails, ReferenceParser } from "./types.js";
|
|
2
|
+
import { type FileInfo } from "./file-info.ts";
|
|
3
|
+
type PolicyDirectiveMap = Record<string, PolicyDirective>;
|
|
4
|
+
export type CSSReferenceParserArgs = {
|
|
5
|
+
contentType?: string | string[];
|
|
6
|
+
};
|
|
7
|
+
export declare class CSSReferenceParser implements ReferenceParser {
|
|
8
|
+
ruleRe: RegExp;
|
|
9
|
+
urlRe: RegExp;
|
|
10
|
+
supports: Set<string>;
|
|
11
|
+
directives: PolicyDirectiveMap;
|
|
12
|
+
constructor(args?: CSSReferenceParserArgs);
|
|
13
|
+
/**
|
|
14
|
+
* Parses all references within a css document.
|
|
15
|
+
*
|
|
16
|
+
* @param content Content of a css file.
|
|
17
|
+
*/
|
|
18
|
+
parse(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<ReferenceDetails[]>;
|
|
19
|
+
update(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<Blob>;
|
|
20
|
+
}
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const ruleRe = /(?:(\@[a-z]+)|(?:([a-z][a-z\-]*\s*):))\s*(.*);/gm;
|
|
2
|
+
const urlRe = /url\(\s*(?:(?:\"(.*)\")|(?:\'(.*)\')|(.*))\s*\)/gm;
|
|
3
|
+
const defaultDirectives = {
|
|
4
|
+
'@import': 'style-src',
|
|
5
|
+
'background': 'img-src',
|
|
6
|
+
'background-image': 'img-src',
|
|
7
|
+
'src': 'font-src',
|
|
8
|
+
};
|
|
9
|
+
export class CSSReferenceParser {
|
|
10
|
+
ruleRe = ruleRe;
|
|
11
|
+
urlRe = urlRe;
|
|
12
|
+
supports = new Set(['text/css']);
|
|
13
|
+
directives = defaultDirectives;
|
|
14
|
+
constructor(args) {
|
|
15
|
+
if (Array.isArray(args?.contentType)) {
|
|
16
|
+
this.supports = new Set(args.contentType);
|
|
17
|
+
}
|
|
18
|
+
else if (args?.contentType != null) {
|
|
19
|
+
this.supports = new Set([args.contentType]);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Parses all references within a css document.
|
|
24
|
+
*
|
|
25
|
+
* @param content Content of a css file.
|
|
26
|
+
*/
|
|
27
|
+
async parse(content, file, filesByURL) {
|
|
28
|
+
let m1;
|
|
29
|
+
let m2;
|
|
30
|
+
let property;
|
|
31
|
+
let url;
|
|
32
|
+
let directive;
|
|
33
|
+
const references = [];
|
|
34
|
+
const text = await content.text();
|
|
35
|
+
this.ruleRe.lastIndex = 0;
|
|
36
|
+
while ((m1 = this.ruleRe.exec(text))) {
|
|
37
|
+
property = m1[1] ?? m1[2];
|
|
38
|
+
directive = this.directives[property];
|
|
39
|
+
if (directive == null)
|
|
40
|
+
continue;
|
|
41
|
+
this.urlRe.lastIndex = 0;
|
|
42
|
+
while ((m2 = this.urlRe.exec(m1[3]))) {
|
|
43
|
+
url = new URL(m2[1] ?? m2[2] ?? m2[3], file.aliasURL).toString();
|
|
44
|
+
references.push({
|
|
45
|
+
url,
|
|
46
|
+
directive,
|
|
47
|
+
file: filesByURL.get(url),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return references;
|
|
52
|
+
}
|
|
53
|
+
async update(content, file, filesByURL) {
|
|
54
|
+
const text = await content.text();
|
|
55
|
+
const updated = text.replace(ruleRe, (match) => {
|
|
56
|
+
return match.replace(urlRe, (...matches) => {
|
|
57
|
+
const src = matches[1] ?? matches[2] ?? matches[3];
|
|
58
|
+
const url = new URL(src, file.aliasURL).toString();
|
|
59
|
+
const ref = filesByURL.get(url);
|
|
60
|
+
if (ref == null)
|
|
61
|
+
return matches[0];
|
|
62
|
+
return `url(${ref.url})`;
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
return new Blob([updated], { type: file.contentType });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FileInfo } from "./file-info.ts";
|
|
2
|
+
import type { PolicyDirective, ReferenceDetails } from "./types.ts";
|
|
3
|
+
export declare class DependancyMap {
|
|
4
|
+
file: FileInfo;
|
|
5
|
+
references: ReferenceDetails[];
|
|
6
|
+
polices: Map<PolicyDirective, ReferenceDetails[]>;
|
|
7
|
+
constructor(file: FileInfo, references: ReferenceDetails[]);
|
|
8
|
+
finalize(): void;
|
|
9
|
+
}
|
|
10
|
+
export declare class DependancyGraph {
|
|
11
|
+
dependancies: Map<string, DependancyMap>;
|
|
12
|
+
constructor(dependancies: Map<string, DependancyMap>);
|
|
13
|
+
/**
|
|
14
|
+
* Returns a debug string for this dependancy graph.
|
|
15
|
+
*/
|
|
16
|
+
debug(): string;
|
|
17
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export class DependancyMap {
|
|
2
|
+
file;
|
|
3
|
+
references;
|
|
4
|
+
polices = new Map();
|
|
5
|
+
constructor(file, references) {
|
|
6
|
+
this.file = file;
|
|
7
|
+
this.references = references;
|
|
8
|
+
}
|
|
9
|
+
finalize() {
|
|
10
|
+
let reference;
|
|
11
|
+
let policies;
|
|
12
|
+
for (let i = 0; i < this.references.length; i++) {
|
|
13
|
+
reference = this.references[i];
|
|
14
|
+
policies = this.polices.get(reference.directive);
|
|
15
|
+
if (policies == null) {
|
|
16
|
+
policies = [];
|
|
17
|
+
this.polices.set(reference.directive, policies);
|
|
18
|
+
}
|
|
19
|
+
policies.push(reference);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class DependancyGraph {
|
|
24
|
+
dependancies = new Map();
|
|
25
|
+
constructor(dependancies) {
|
|
26
|
+
this.dependancies = dependancies;
|
|
27
|
+
for (const dependancyMap of this.dependancies.values()) {
|
|
28
|
+
for (let i = 0; i < dependancyMap.references.length; i++) {
|
|
29
|
+
const reference = dependancyMap.references[i];
|
|
30
|
+
if (reference.file == null) {
|
|
31
|
+
console.warn(`Unknown dependancy reference ${reference.url}`);
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
const other = this.dependancies.get(reference.file.alias);
|
|
35
|
+
if (other == null) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
for (let j = 0; j < other.references.length; j++) {
|
|
39
|
+
dependancyMap.references.push(other.references[j]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
dependancyMap.finalize();
|
|
43
|
+
}
|
|
44
|
+
Object.freeze(this.dependancies);
|
|
45
|
+
Object.freeze(this);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Returns a debug string for this dependancy graph.
|
|
49
|
+
*/
|
|
50
|
+
debug() {
|
|
51
|
+
let debug = 'Dependancy Graph\n----------------\n\n';
|
|
52
|
+
let dependancy;
|
|
53
|
+
const dependancies = Array.from(this.dependancies.values());
|
|
54
|
+
for (let i = 0; i < dependancies.length; i++) {
|
|
55
|
+
if (i !== 0)
|
|
56
|
+
debug += '\n';
|
|
57
|
+
dependancy = dependancies[i];
|
|
58
|
+
debug += dependancy.file.alias + '\n';
|
|
59
|
+
const polices = Array.from(dependancy.polices.entries());
|
|
60
|
+
if (polices.length === 0) {
|
|
61
|
+
debug += ` No dependancies\n`;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
for (const [policy, references] of dependancy.polices.entries()) {
|
|
65
|
+
debug += ` ${policy}\n`;
|
|
66
|
+
for (let j = 0; j < references.length; j++) {
|
|
67
|
+
debug += ` ${references[j].url}\n`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
debug += '----------------\n';
|
|
72
|
+
return debug;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type FileInfo = {
|
|
2
|
+
/**
|
|
3
|
+
* The name of the file.
|
|
4
|
+
*/
|
|
5
|
+
name: string;
|
|
6
|
+
/**
|
|
7
|
+
* Path of the file relative to the configured directory.
|
|
8
|
+
*/
|
|
9
|
+
relativePath: string;
|
|
10
|
+
/**
|
|
11
|
+
* Full path to the file.
|
|
12
|
+
*/
|
|
13
|
+
absolutePath: string;
|
|
14
|
+
/**
|
|
15
|
+
* The alias to the directory where the file is located in.
|
|
16
|
+
*/
|
|
17
|
+
directoryAlias: string;
|
|
18
|
+
/**
|
|
19
|
+
* The file's extension.
|
|
20
|
+
*/
|
|
21
|
+
extension: string;
|
|
22
|
+
/**
|
|
23
|
+
* The file's language if detected.
|
|
24
|
+
*/
|
|
25
|
+
lang?: string;
|
|
26
|
+
/**
|
|
27
|
+
* The file's content type.
|
|
28
|
+
*/
|
|
29
|
+
contentType: string;
|
|
30
|
+
/**
|
|
31
|
+
* This files directory + relative path + name.
|
|
32
|
+
*/
|
|
33
|
+
alias: string;
|
|
34
|
+
/**
|
|
35
|
+
* The hash of the original file contents.
|
|
36
|
+
*/
|
|
37
|
+
hash: string;
|
|
38
|
+
/**
|
|
39
|
+
* The URL of the file.
|
|
40
|
+
*/
|
|
41
|
+
url: string;
|
|
42
|
+
/**
|
|
43
|
+
* alias url for this resource.
|
|
44
|
+
*/
|
|
45
|
+
aliasURL: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Contains information about a static file.
|
|
49
|
+
*/
|
|
50
|
+
export declare class WorkingFileInfo {
|
|
51
|
+
/**
|
|
52
|
+
* The name of the file.
|
|
53
|
+
*/
|
|
54
|
+
name: string;
|
|
55
|
+
/**
|
|
56
|
+
* Path of the file relative to the configured directory.
|
|
57
|
+
*/
|
|
58
|
+
relativePath: string;
|
|
59
|
+
/**
|
|
60
|
+
* Full path to the file.
|
|
61
|
+
*/
|
|
62
|
+
absolutePath: string;
|
|
63
|
+
/**
|
|
64
|
+
* The alias to the directory where the file is located in.
|
|
65
|
+
*/
|
|
66
|
+
directoryAlias: string;
|
|
67
|
+
/**
|
|
68
|
+
* The file's extension.
|
|
69
|
+
*/
|
|
70
|
+
extension: string;
|
|
71
|
+
/**
|
|
72
|
+
* The file's language if detected.
|
|
73
|
+
*/
|
|
74
|
+
lang?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The file's content type.
|
|
77
|
+
*/
|
|
78
|
+
contentType: string;
|
|
79
|
+
/**
|
|
80
|
+
* This files directory + relative path + name.
|
|
81
|
+
*/
|
|
82
|
+
alias: string;
|
|
83
|
+
/**
|
|
84
|
+
* The hash of the original file contents.
|
|
85
|
+
*/
|
|
86
|
+
hash: string | null;
|
|
87
|
+
/**
|
|
88
|
+
* The URL of the file.
|
|
89
|
+
*/
|
|
90
|
+
url: string | null;
|
|
91
|
+
/**
|
|
92
|
+
* alias url for this resource.
|
|
93
|
+
*/
|
|
94
|
+
aliasURL: string | null;
|
|
95
|
+
constructor(name: string, directoryAlias: string, relativePath: string, absolutePath: string, extension: string, contentType: string, lang?: string);
|
|
96
|
+
finalize(hash: string, url: string, aliasURL: string): void;
|
|
97
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Contains information about a static file.
|
|
4
|
+
*/
|
|
5
|
+
export class WorkingFileInfo {
|
|
6
|
+
/**
|
|
7
|
+
* The name of the file.
|
|
8
|
+
*/
|
|
9
|
+
name;
|
|
10
|
+
/**
|
|
11
|
+
* Path of the file relative to the configured directory.
|
|
12
|
+
*/
|
|
13
|
+
relativePath;
|
|
14
|
+
/**
|
|
15
|
+
* Full path to the file.
|
|
16
|
+
*/
|
|
17
|
+
absolutePath;
|
|
18
|
+
/**
|
|
19
|
+
* The alias to the directory where the file is located in.
|
|
20
|
+
*/
|
|
21
|
+
directoryAlias;
|
|
22
|
+
/**
|
|
23
|
+
* The file's extension.
|
|
24
|
+
*/
|
|
25
|
+
extension;
|
|
26
|
+
/**
|
|
27
|
+
* The file's language if detected.
|
|
28
|
+
*/
|
|
29
|
+
lang;
|
|
30
|
+
/**
|
|
31
|
+
* The file's content type.
|
|
32
|
+
*/
|
|
33
|
+
contentType;
|
|
34
|
+
/**
|
|
35
|
+
* This files directory + relative path + name.
|
|
36
|
+
*/
|
|
37
|
+
alias;
|
|
38
|
+
/**
|
|
39
|
+
* The hash of the original file contents.
|
|
40
|
+
*/
|
|
41
|
+
hash = null;
|
|
42
|
+
/**
|
|
43
|
+
* The URL of the file.
|
|
44
|
+
*/
|
|
45
|
+
url = null;
|
|
46
|
+
/**
|
|
47
|
+
* alias url for this resource.
|
|
48
|
+
*/
|
|
49
|
+
aliasURL = null;
|
|
50
|
+
constructor(name, directoryAlias, relativePath, absolutePath, extension, contentType, lang) {
|
|
51
|
+
this.name = name;
|
|
52
|
+
this.directoryAlias = directoryAlias;
|
|
53
|
+
this.relativePath = relativePath;
|
|
54
|
+
this.absolutePath = absolutePath;
|
|
55
|
+
this.extension = extension;
|
|
56
|
+
this.contentType = contentType;
|
|
57
|
+
this.lang = lang;
|
|
58
|
+
this.alias = join(directoryAlias, relativePath);
|
|
59
|
+
}
|
|
60
|
+
finalize(hash, url, aliasURL) {
|
|
61
|
+
this.hash = hash;
|
|
62
|
+
this.url = url;
|
|
63
|
+
this.aliasURL = aliasURL;
|
|
64
|
+
Object.freeze(this);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type FileInfo } from "./file-info.ts";
|
|
2
|
+
import type { ReferenceDetails, ReferenceParser } from "./types.ts";
|
|
3
|
+
export type HTMLParserArgs = {
|
|
4
|
+
contentType?: string | string[];
|
|
5
|
+
};
|
|
6
|
+
export declare class HTMLParser implements ReferenceParser {
|
|
7
|
+
supports: Set<string>;
|
|
8
|
+
constructor(args?: HTMLParserArgs);
|
|
9
|
+
parse(content: Blob, file: FileInfo, filesByURL: Map<string, FileInfo>): Promise<ReferenceDetails[]>;
|
|
10
|
+
update(content: Blob, file: FileInfo, filesByURL: Map<string, FileInfo>): Promise<Blob>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { JSDOM } from 'jsdom';
|
|
2
|
+
const asMap = {
|
|
3
|
+
audio: 'media-src',
|
|
4
|
+
document: 'frame-src',
|
|
5
|
+
embed: 'object-src',
|
|
6
|
+
fetch: 'connect-src',
|
|
7
|
+
font: 'font-src',
|
|
8
|
+
image: 'img-src',
|
|
9
|
+
object: 'object-src',
|
|
10
|
+
script: 'script-src',
|
|
11
|
+
style: 'style-src',
|
|
12
|
+
track: 'media-src',
|
|
13
|
+
video: 'media-src',
|
|
14
|
+
worker: 'worker-src',
|
|
15
|
+
};
|
|
16
|
+
export class HTMLParser {
|
|
17
|
+
supports = new Set([
|
|
18
|
+
'text/html',
|
|
19
|
+
'application/xhtml+xml',
|
|
20
|
+
]);
|
|
21
|
+
constructor(args = {}) {
|
|
22
|
+
if (Array.isArray(args?.contentType)) {
|
|
23
|
+
this.supports = new Set(args.contentType);
|
|
24
|
+
}
|
|
25
|
+
else if (args?.contentType != null) {
|
|
26
|
+
this.supports = new Set([args.contentType]);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async parse(content, file, filesByURL) {
|
|
30
|
+
let references = [];
|
|
31
|
+
let src;
|
|
32
|
+
let rel;
|
|
33
|
+
let as;
|
|
34
|
+
let url;
|
|
35
|
+
const text = await content.text();
|
|
36
|
+
const dom = new JSDOM(text, {
|
|
37
|
+
contentType: file.contentType,
|
|
38
|
+
});
|
|
39
|
+
const document = dom.window.document;
|
|
40
|
+
for (const element of document.querySelectorAll('link')) {
|
|
41
|
+
src = element.getAttribute('src');
|
|
42
|
+
rel = element.getAttribute('rel');
|
|
43
|
+
as = element.getAttribute('as');
|
|
44
|
+
url = new URL(src, file.aliasURL).toString();
|
|
45
|
+
if (rel === 'stylesheet') {
|
|
46
|
+
references.push({
|
|
47
|
+
url,
|
|
48
|
+
directive: 'style-src',
|
|
49
|
+
file: filesByURL.get(url),
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (Object.hasOwn(asMap, as)) {
|
|
54
|
+
references.push({
|
|
55
|
+
url,
|
|
56
|
+
directive: asMap[as],
|
|
57
|
+
file: filesByURL.get(url),
|
|
58
|
+
});
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
references.push({
|
|
62
|
+
url,
|
|
63
|
+
file: filesByURL.get(url),
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
for (const element of [
|
|
67
|
+
...document.querySelectorAll('img'),
|
|
68
|
+
...document.querySelectorAll('picture source'),
|
|
69
|
+
]) {
|
|
70
|
+
src = element.tagName.toLowerCase() === 'source'
|
|
71
|
+
? element.getAttribute('srcset')
|
|
72
|
+
: element.getAttribute('src');
|
|
73
|
+
if (src == null) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
url = new URL(src, file.aliasURL).toString();
|
|
77
|
+
references.push({
|
|
78
|
+
url,
|
|
79
|
+
directive: 'img-src',
|
|
80
|
+
file: filesByURL.get(url),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
for (const element of [
|
|
84
|
+
...document.querySelectorAll('track'),
|
|
85
|
+
...document.querySelectorAll('audio'),
|
|
86
|
+
...document.querySelectorAll('video'),
|
|
87
|
+
...document.querySelectorAll('video source'),
|
|
88
|
+
]) {
|
|
89
|
+
src = element.getAttribute('src');
|
|
90
|
+
if (src == null) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
url = new URL(src, file.aliasURL).toString();
|
|
94
|
+
references.push({
|
|
95
|
+
url,
|
|
96
|
+
directive: 'media-src',
|
|
97
|
+
file: filesByURL.get(url),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
for (const element of [
|
|
101
|
+
...document.querySelectorAll('iframe'),
|
|
102
|
+
...document.querySelectorAll('fencedframe'),
|
|
103
|
+
]) {
|
|
104
|
+
src = element.getAttribute('src');
|
|
105
|
+
if (src == null) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
url = new URL(src, file.aliasURL).toString();
|
|
109
|
+
references.push({
|
|
110
|
+
url,
|
|
111
|
+
directive: 'child-src',
|
|
112
|
+
file: filesByURL.get(url),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
for (const element of document.querySelectorAll('script')) {
|
|
116
|
+
src = element.getAttribute('src');
|
|
117
|
+
if (src == null) {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
url = new URL(src, file.aliasURL).toString();
|
|
121
|
+
references.push({
|
|
122
|
+
url,
|
|
123
|
+
directive: 'script-src',
|
|
124
|
+
file: filesByURL.get(url),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
return references;
|
|
128
|
+
}
|
|
129
|
+
async update(content, file, filesByURL) {
|
|
130
|
+
let url;
|
|
131
|
+
let ref;
|
|
132
|
+
let text = await content.text();
|
|
133
|
+
const dom = new JSDOM(text, {
|
|
134
|
+
contentType: file.contentType,
|
|
135
|
+
});
|
|
136
|
+
const document = dom.window.document;
|
|
137
|
+
for (const element of [
|
|
138
|
+
...document.querySelectorAll('link'),
|
|
139
|
+
...document.querySelectorAll('img'),
|
|
140
|
+
...document.querySelectorAll('track'),
|
|
141
|
+
...document.querySelectorAll('audio'),
|
|
142
|
+
...document.querySelectorAll('video'),
|
|
143
|
+
...document.querySelectorAll('video source'),
|
|
144
|
+
...document.querySelectorAll('iframe'),
|
|
145
|
+
...document.querySelectorAll('fencedframe'),
|
|
146
|
+
...document.querySelectorAll('script'),
|
|
147
|
+
]) {
|
|
148
|
+
const src = element.getAttribute('src');
|
|
149
|
+
if (src == null) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
url = new URL(src, file.aliasURL).toString();
|
|
153
|
+
ref = filesByURL.get(url);
|
|
154
|
+
if (ref == null)
|
|
155
|
+
continue;
|
|
156
|
+
element.setAttribute('src', ref.url);
|
|
157
|
+
}
|
|
158
|
+
for (const element of document.querySelectorAll('picture source')) {
|
|
159
|
+
const src = element.getAttribute('srcset');
|
|
160
|
+
if (src == null) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
url = new URL(src, file.aliasURL).toString();
|
|
164
|
+
ref = filesByURL.get(url);
|
|
165
|
+
if (ref == null)
|
|
166
|
+
continue;
|
|
167
|
+
element.setAttribute('srcset', ref.url);
|
|
168
|
+
}
|
|
169
|
+
return new Blob([dom.serialize()], { type: file.contentType });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Options } from 'acorn';
|
|
2
|
+
import { type FileInfo } from "./file-info.ts";
|
|
3
|
+
import type { FilesByURL, ReferenceDetails, ReferenceParser } from "./types.ts";
|
|
4
|
+
export type JSReferenceParserArgs = {
|
|
5
|
+
contentType?: string | string[];
|
|
6
|
+
acornOptions?: Options;
|
|
7
|
+
};
|
|
8
|
+
export declare class JSReferenceParser implements ReferenceParser {
|
|
9
|
+
#private;
|
|
10
|
+
supports: Set<string>;
|
|
11
|
+
constructor(args?: JSReferenceParserArgs);
|
|
12
|
+
parse(content: Blob, file: FileInfo, filesByURL: FilesByURL): Promise<ReferenceDetails[]>;
|
|
13
|
+
update(content: Blob, file: FileInfo, filesByURL: Map<string, FileInfo>): Promise<Blob>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { parse } from 'acorn';
|
|
2
|
+
import { generate } from 'astring';
|
|
3
|
+
import { walk } from 'zimmerframe';
|
|
4
|
+
export class JSReferenceParser {
|
|
5
|
+
supports = new Set(['application/javascript']);
|
|
6
|
+
#acornOptions;
|
|
7
|
+
constructor(args = {}) {
|
|
8
|
+
if (Array.isArray(args?.contentType)) {
|
|
9
|
+
this.supports = new Set(args.contentType);
|
|
10
|
+
}
|
|
11
|
+
else if (args?.contentType != null) {
|
|
12
|
+
this.supports = new Set([args.contentType]);
|
|
13
|
+
}
|
|
14
|
+
this.#acornOptions = args.acornOptions ?? {
|
|
15
|
+
ecmaVersion: 'latest',
|
|
16
|
+
sourceType: 'module',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
async parse(content, file, filesByURL) {
|
|
20
|
+
let references = [];
|
|
21
|
+
let url;
|
|
22
|
+
const text = await content.text();
|
|
23
|
+
const ast = parse(text, this.#acornOptions);
|
|
24
|
+
walk(ast, {}, {
|
|
25
|
+
ImportDeclaration(node, { next }) {
|
|
26
|
+
url = new URL(node.source.value, file.aliasURL).toString();
|
|
27
|
+
references.push({
|
|
28
|
+
url,
|
|
29
|
+
directive: 'script-src',
|
|
30
|
+
file: filesByURL.get(url),
|
|
31
|
+
});
|
|
32
|
+
next();
|
|
33
|
+
},
|
|
34
|
+
ImportExpression(node, { next }) {
|
|
35
|
+
url = new URL(node.source.value, file.aliasURL).toString();
|
|
36
|
+
references.push({
|
|
37
|
+
url,
|
|
38
|
+
directive: 'script-src',
|
|
39
|
+
file: filesByURL.get(url),
|
|
40
|
+
});
|
|
41
|
+
next();
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
return references;
|
|
45
|
+
}
|
|
46
|
+
async update(content, file, filesByURL) {
|
|
47
|
+
let url;
|
|
48
|
+
let ref;
|
|
49
|
+
const text = await content.text();
|
|
50
|
+
const ast = parse(text, this.#acornOptions);
|
|
51
|
+
const updated = walk(ast, {}, {
|
|
52
|
+
ImportDeclaration(node, { next }) {
|
|
53
|
+
url = new URL(node.source.value, file.aliasURL).toString();
|
|
54
|
+
ref = filesByURL.get(url);
|
|
55
|
+
if (ref == null)
|
|
56
|
+
return;
|
|
57
|
+
const source = node.source;
|
|
58
|
+
source.raw = '"' + encodeURI(ref.url) + '"';
|
|
59
|
+
next();
|
|
60
|
+
},
|
|
61
|
+
ImportExpression(node, { next }) {
|
|
62
|
+
url = new URL(node.source.value, file.aliasURL).toString();
|
|
63
|
+
ref = filesByURL.get(url);
|
|
64
|
+
if (ref == null)
|
|
65
|
+
return;
|
|
66
|
+
const source = node.source;
|
|
67
|
+
source.raw = '"' + encodeURI(ref.url) + '"';
|
|
68
|
+
next();
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const serialized = generate(updated);
|
|
72
|
+
return new Blob([serialized], { type: file.contentType });
|
|
73
|
+
}
|
|
74
|
+
}
|