@egildo/provenance 0.0.0-stage → 0.6.2
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/README.md +171 -2
- package/dist/css.d.ts +2 -0
- package/dist/css.js +77 -0
- package/dist/html.d.ts +2 -0
- package/dist/html.js +128 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +3 -0
- package/dist/memory.d.ts +6 -0
- package/dist/memory.js +83 -0
- package/dist/node.d.ts +4 -0
- package/dist/node.js +110 -0
- package/dist/offsets.d.ts +6 -0
- package/dist/offsets.js +43 -0
- package/dist/resolvers.d.ts +4 -0
- package/dist/resolvers.js +42 -0
- package/dist/session.d.ts +11 -0
- package/dist/session.js +893 -0
- package/package.json +59 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Egildo Tagliareni
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,3 +1,172 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<picture>
|
|
3
|
+
<source media="(prefers-color-scheme: dark)" srcset="assets/logo-dark.svg">
|
|
4
|
+
<img src="assets/logo.svg" alt="Provenance" width="360">
|
|
5
|
+
</picture>
|
|
6
|
+
</p>
|
|
2
7
|
|
|
3
|
-
|
|
8
|
+
<p align="center">
|
|
9
|
+
<strong>Every byte of rendered output, traced back to the exact file, version, and byte range that produced it.</strong>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img alt="version" src="https://img.shields.io/badge/version-0.5.0-blue">
|
|
14
|
+
<img alt="node" src="https://img.shields.io/badge/node-%E2%89%A522-339933?logo=node.js&logoColor=white">
|
|
15
|
+
<img alt="license" src="https://img.shields.io/badge/license-MIT-blue">
|
|
16
|
+
<img alt="status" src="https://img.shields.io/badge/status-kernel%20%2B%20editing-orange">
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
A rendered document is made of many files. By the time a page appears in a browser, it has read
|
|
22
|
+
an include tree, a configuration cascade, stylesheets, fonts, images, and whatever plugins pulled
|
|
23
|
+
in along the way. Two questions follow every render: **what did it actually load**, and **which
|
|
24
|
+
files must change on disk** when someone edits what they see.
|
|
25
|
+
|
|
26
|
+
Provenance is one structure that answers both. For a single rendered document, it knows every
|
|
27
|
+
source involved, how those sources depend on each other, where each rendered thing came from, and
|
|
28
|
+
how to stage and write changes back — without ever touching a filesystem itself.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { openSession } from "@egildo/provenance";
|
|
32
|
+
import { createNodeHost } from "@egildo/provenance/node";
|
|
33
|
+
import { css } from "@egildo/provenance/css";
|
|
34
|
+
import { html } from "@egildo/provenance/html";
|
|
35
|
+
|
|
36
|
+
const session = await openSession({
|
|
37
|
+
host: createNodeHost(),
|
|
38
|
+
entry: "/path/to/index.html",
|
|
39
|
+
handlers: [html, css],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
for (const source of session.sources()) {
|
|
43
|
+
console.log(source.id, source.state, source.location);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
session.onChange(change => console.log(change));
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Why
|
|
50
|
+
|
|
51
|
+
Most tools that need this answer it twice, with bookkeeping that can't talk to itself. BelType —
|
|
52
|
+
the project Provenance was extracted from — kept a list of files read, a separate list of URLs
|
|
53
|
+
loaded, two near-identical file watchers, and one hand-rolled rule mapping URLs back to files.
|
|
54
|
+
Each piece answered one question. None of them could answer the other, because they shared no
|
|
55
|
+
coordinates.
|
|
56
|
+
|
|
57
|
+
Provenance gives every fact — an edge, a stamp, an edit — one coordinate system: a source, a
|
|
58
|
+
version, and a byte range. Anything it knows can be joined against anything else it knows.
|
|
59
|
+
|
|
60
|
+
## What it does
|
|
61
|
+
|
|
62
|
+
- **Tracks sources of any kind.** Markdown, HTML, CSS, configuration, images, fonts — and
|
|
63
|
+
whatever a handler teaches it. Binary files are leaves: addressable, never parsed.
|
|
64
|
+
- **Records dependencies twice.** *Declared* edges come from parsing; *observed* edges come from
|
|
65
|
+
what a runtime actually fetched. The two are kept apart on purpose — the gap between them is
|
|
66
|
+
the product, not noise to smooth over.
|
|
67
|
+
- **Addresses rendered output.** Any rendered node can point back to the exact source, version,
|
|
68
|
+
and byte range that produced it.
|
|
69
|
+
- **Stages edits.** Changes are held against a base version, previewed in memory, and written to
|
|
70
|
+
disk together. What gets written is exactly what the preview showed.
|
|
71
|
+
|
|
72
|
+
It runs in any JavaScript realm — Node, a browser tab, a worker — because it performs **no I/O of
|
|
73
|
+
its own**. Everything outside the call (reading, writing, watching, path arithmetic) is asked of a
|
|
74
|
+
**host** the embedder supplies. A Node host ships as its own entry point; nothing else in the
|
|
75
|
+
library imports from `node:`, and a test enforces that rather than trusting convention.
|
|
76
|
+
|
|
77
|
+
## How it decides things
|
|
78
|
+
|
|
79
|
+
Provenance is opinionated on purpose. Nine design principles govern it, each one stated as
|
|
80
|
+
something it refuses to do:
|
|
81
|
+
|
|
82
|
+
| Principle | Forbids |
|
|
83
|
+
|---|---|
|
|
84
|
+
| **One address space** | A fact with no address. A second coordinate system past the handler boundary. |
|
|
85
|
+
| **Handlers declare; Provenance decides** | A handler that reads, resolves, caches, watches, or writes. |
|
|
86
|
+
| **Derived facts are pure functions of versions** | Any `invalidate()` call, anywhere. A cache hit that skips re-registering its probes. |
|
|
87
|
+
| **Declared and observed stay apart** | An observation that overwrites a declared edge. A disagreement smoothed away. |
|
|
88
|
+
| **Source is the stored reality; renders are projections** | Writing Provenance's own bookkeeping into a source file. |
|
|
89
|
+
| **Nothing touches disk before commit** | Any write outside commit. A staged edit expressed against another staged edit's result. |
|
|
90
|
+
| **Edits are splices** | A printer or serializer that regenerates a source from a tree. |
|
|
91
|
+
| **Languages are plugins; nesting is delegation** | Language knowledge in the core. A handler parsing another language's region itself. |
|
|
92
|
+
| **Supply, not meaning** | A `node:` import outside the Node host. A host that returns a verdict instead of a fact. |
|
|
93
|
+
|
|
94
|
+
The full reasoning — with the incidents each rule was cut from — is in
|
|
95
|
+
[the design principles](docs/principles.md).
|
|
96
|
+
|
|
97
|
+
## Status
|
|
98
|
+
|
|
99
|
+
**The kernel is built, and editing's first slice. Observation is not.**
|
|
100
|
+
|
|
101
|
+
A session finds every source reachable from an entry document and its roots, records each
|
|
102
|
+
declared dependency against the exact bytes that asked for it, keeps broken references visible
|
|
103
|
+
until they heal, and reports what changed as files change. See
|
|
104
|
+
[the kernel spec](specs/source-graph-kernel/spec.md) for exactly what that covers, and
|
|
105
|
+
[the changelog](CHANGELOG.md) for what shipped in each release.
|
|
106
|
+
|
|
107
|
+
Since 0.5.0 it also **writes back**: a tool stages an edit to a value, sees a preview, and commits it;
|
|
108
|
+
the edit lands in exactly the bytes of that value, every other byte untouched, and survives the
|
|
109
|
+
file changing on disk in the meantime ([the design](docs/write-back.md),
|
|
110
|
+
[the editing spec](specs/editing/spec.md)). Writing is granted, never ambient. Since 0.6.0 it
|
|
111
|
+
also inserts and deletes entries ([insert-delete](specs/insert-delete/spec.md)), and an edit staged on a file that a
|
|
112
|
+
colleague is mid-way through saving waits for it to parse again. Format plug-ins come from outside —
|
|
113
|
+
Cascata's YAML and JSON writers are the first.
|
|
114
|
+
|
|
115
|
+
What's next: **stamps** (outputs that carry their source address, so an edit on a rendered page
|
|
116
|
+
finds its way back), **Markdown units** and a **CSS writer**, then **observation** — see
|
|
117
|
+
[the vision's "What arrives first"](docs/vision.md#what-arrives-first).
|
|
118
|
+
|
|
119
|
+
## Install
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm install @egildo/provenance
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Its tests prove write-back end to end with Cascata, a development dependency, `@egildo/cascata`.
|
|
126
|
+
|
|
127
|
+
Requires Node 22 or later (22.18+ to run the tests from source, for type stripping).
|
|
128
|
+
|
|
129
|
+
## Entry points
|
|
130
|
+
|
|
131
|
+
| Import | What it gives you |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `@egildo/provenance` | `openSession` and the core types |
|
|
134
|
+
| `@egildo/provenance/html` | An HTML handler, on [parse5](https://github.com/inikulin/parse5) |
|
|
135
|
+
| `@egildo/provenance/css` | A CSS handler, on [PostCSS](https://postcss.org/) |
|
|
136
|
+
| `@egildo/provenance/node` | The Node host |
|
|
137
|
+
| `@egildo/provenance/memory` | An in-memory host, for any realm — tests, browsers, workers |
|
|
138
|
+
|
|
139
|
+
Every export is listed in [the public API contract](specs/source-graph-kernel/contracts/public-api.md).
|
|
140
|
+
|
|
141
|
+
## Who it's for
|
|
142
|
+
|
|
143
|
+
- **BelType**, first — replacing its file list, URL list, two watchers, and eventually its
|
|
144
|
+
editing server's write path.
|
|
145
|
+
- **Cascata**, by shape and not by import — a read pass's host satisfies Cascata's `Host`
|
|
146
|
+
interface structurally, so Cascata's configuration files become session roots without Cascata
|
|
147
|
+
ever importing Provenance.
|
|
148
|
+
- **The CSS tools**, which build on Provenance's addresses and its edit model.
|
|
149
|
+
- **Any tool** that renders documents from files and needs to know what it read, what the runtime
|
|
150
|
+
loaded, and how to write back.
|
|
151
|
+
|
|
152
|
+
What it deliberately stays out of — rendering, transforming, reading meaning, serving, history —
|
|
153
|
+
is in [the vision's "What it is not"](docs/vision.md#what-it-is-not), each exclusion argued for.
|
|
154
|
+
|
|
155
|
+
## Documentation
|
|
156
|
+
|
|
157
|
+
- **[Vision](docs/vision.md)** — what Provenance is for, who it serves, what it refuses to be.
|
|
158
|
+
- **[Design principles](docs/principles.md)** — how it decides things, each rule with what it forbids.
|
|
159
|
+
- **[Glossary](docs/glossary.md)** — every christened term, and which meaning of "provenance" is ours.
|
|
160
|
+
- **[Design synthesis](docs/design-synthesis.md)** — the day-zero design the vision was drawn from.
|
|
161
|
+
- **[Kernel spec](specs/source-graph-kernel/spec.md)** and **[read-passes spec](specs/read-passes/spec.md)** — what's built, precisely.
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm test # type-checks and runs the tests
|
|
167
|
+
npm run build # writes dist/
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
[MIT](LICENSE)
|
package/dist/css.d.ts
ADDED
package/dist/css.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// The CSS handler: `url()`, `image-set()` strings and `@import`, found with PostCSS
|
|
2
|
+
// (specs/source-graph-kernel/research.md, CSS handler). Embedded regions are not its concern.
|
|
3
|
+
import safeParse from "postcss-safe-parser";
|
|
4
|
+
import valueParser from "postcss-value-parser";
|
|
5
|
+
/** Undoes CSS escapes: `\` and one to six hex digits (and one following space), or `\` and a character. */
|
|
6
|
+
function unescape(text) {
|
|
7
|
+
return text.replace(/\\(?:([0-9a-f]{1,6})[ \t\n]?|([^\n]))/gi, (_, hex, char) => hex ? String.fromCodePoint(Math.min(parseInt(hex, 16), 0x10ffff) || 0xfffd) : (char ?? ""));
|
|
8
|
+
}
|
|
9
|
+
/** The request a `url()` argument or a string holds, with its range in the whole text. */
|
|
10
|
+
function literal(node, offset, kind) {
|
|
11
|
+
if (node.type === "word") {
|
|
12
|
+
return { start: offset + node.sourceIndex, end: offset + node.sourceEndIndex, request: unescape(node.value), kind };
|
|
13
|
+
}
|
|
14
|
+
if (node.type === "string") {
|
|
15
|
+
const end = node.unclosed ? node.sourceEndIndex : node.sourceEndIndex - 1;
|
|
16
|
+
return { start: offset + node.sourceIndex + 1, end: offset + end, request: unescape(node.value), kind };
|
|
17
|
+
}
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
const isNamed = (node, ...names) => node.type === "function" && names.includes(node.value.toLowerCase());
|
|
21
|
+
/** Every `url()`, and every string directly inside `image-set()`, in a value placed at `offset`. */
|
|
22
|
+
function urls(value, offset, requests) {
|
|
23
|
+
valueParser(value).walk(node => {
|
|
24
|
+
if (node.type !== "function")
|
|
25
|
+
return;
|
|
26
|
+
if (isNamed(node, "url")) {
|
|
27
|
+
const found = node.nodes[0] && literal(node.nodes[0], offset, "candidate");
|
|
28
|
+
if (found)
|
|
29
|
+
requests.push(found);
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (isNamed(node, "image-set", "-webkit-image-set")) {
|
|
33
|
+
for (const child of node.nodes) {
|
|
34
|
+
const found = child.type === "string" && literal(child, offset, "candidate");
|
|
35
|
+
if (found)
|
|
36
|
+
requests.push(found);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** `@import`'s URL: required unless a condition (media, `supports()`) follows; a `layer` is not one. */
|
|
43
|
+
function importRequest(params, offset) {
|
|
44
|
+
const nodes = valueParser(params).nodes.filter(n => n.type !== "space" && n.type !== "comment");
|
|
45
|
+
const [first, ...rest] = nodes;
|
|
46
|
+
if (!first)
|
|
47
|
+
return undefined;
|
|
48
|
+
const conditioned = rest.some(n => !(n.type === "word" && n.value.toLowerCase() === "layer") && !isNamed(n, "layer"));
|
|
49
|
+
const kind = conditioned ? "candidate" : "requires";
|
|
50
|
+
if (first.type === "string")
|
|
51
|
+
return literal(first, offset, kind);
|
|
52
|
+
if (isNamed(first, "url") && first.type === "function" && first.nodes[0])
|
|
53
|
+
return literal(first.nodes[0], offset, kind);
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
export const css = {
|
|
57
|
+
claims: location => location.toLowerCase().endsWith(".css"),
|
|
58
|
+
analyze(text) {
|
|
59
|
+
const requests = [];
|
|
60
|
+
safeParse(text).walk(node => {
|
|
61
|
+
const start = node.source?.start?.offset;
|
|
62
|
+
if (start === undefined)
|
|
63
|
+
return;
|
|
64
|
+
if (node.type === "decl") {
|
|
65
|
+
const value = node.raws.value?.raw ?? node.value;
|
|
66
|
+
urls(value, start + node.prop.length + (node.raws.between ?? "").length, requests);
|
|
67
|
+
}
|
|
68
|
+
else if (node.type === "atrule" && node.name.toLowerCase() === "import") {
|
|
69
|
+
const params = node.raws.params?.raw ?? node.params;
|
|
70
|
+
const found = importRequest(params, start + 1 + node.name.length + (node.raws.afterName ?? "").length);
|
|
71
|
+
if (found)
|
|
72
|
+
requests.push(found);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
return { requests };
|
|
76
|
+
},
|
|
77
|
+
};
|
package/dist/html.d.ts
ADDED
package/dist/html.js
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// The HTML handler: the attributes that make a browser load something, found with parse5
|
|
2
|
+
// (specs/source-graph-kernel/contracts/public-api.md, Handlers). `<style>` and `style="…"` are
|
|
3
|
+
// CSS regions, not this handler's language, and wait for regions.
|
|
4
|
+
import { parse } from "parse5";
|
|
5
|
+
/** The value's range inside an attribute's `name="value"` span, or undefined for a bare name. */
|
|
6
|
+
function valueRange(text, span) {
|
|
7
|
+
const raw = text.slice(span.startOffset, span.endOffset);
|
|
8
|
+
const head = /^[^\s=]+\s*=\s*(["']?)\s*/.exec(raw);
|
|
9
|
+
if (!head)
|
|
10
|
+
return undefined;
|
|
11
|
+
const quoted = head[1] !== "" && raw.endsWith(head[1]) && raw.length > head[0].length;
|
|
12
|
+
const value = raw.slice(head[0].length, raw.length - (quoted ? 1 : 0));
|
|
13
|
+
return { start: span.startOffset + head[0].length, end: span.startOffset + head[0].length + value.trimEnd().length };
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The URL ranges in a `srcset`, per the HTML standard's "parse a srcset attribute": a URL is a
|
|
17
|
+
* run of non-whitespace; trailing commas end it; descriptors run to a comma outside parentheses.
|
|
18
|
+
*/
|
|
19
|
+
function srcsetUrls(value) {
|
|
20
|
+
const urls = [];
|
|
21
|
+
const space = /[\t\n\f\r ]/;
|
|
22
|
+
let i = 0;
|
|
23
|
+
while (i < value.length) {
|
|
24
|
+
while (i < value.length && (space.test(value[i]) || value[i] === ","))
|
|
25
|
+
i++;
|
|
26
|
+
if (i >= value.length)
|
|
27
|
+
break;
|
|
28
|
+
const start = i;
|
|
29
|
+
while (i < value.length && !space.test(value[i]))
|
|
30
|
+
i++;
|
|
31
|
+
let end = i;
|
|
32
|
+
if (value[end - 1] === ",") {
|
|
33
|
+
while (end > start && value[end - 1] === ",")
|
|
34
|
+
end--;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
let depth = 0;
|
|
38
|
+
while (i < value.length && !(value[i] === "," && depth === 0)) {
|
|
39
|
+
if (value[i] === "(")
|
|
40
|
+
depth++;
|
|
41
|
+
else if (value[i] === ")" && depth > 0)
|
|
42
|
+
depth--;
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (end > start)
|
|
47
|
+
urls.push({ start, end });
|
|
48
|
+
}
|
|
49
|
+
return urls;
|
|
50
|
+
}
|
|
51
|
+
/** Which attributes of an element load something, and how surely. */
|
|
52
|
+
function loads(element) {
|
|
53
|
+
const attr = (name) => element.attrs.find(a => a.name === name && !a.prefix)?.value;
|
|
54
|
+
const lazy = attr("loading")?.toLowerCase() === "lazy";
|
|
55
|
+
switch (element.tagName) {
|
|
56
|
+
case "link": {
|
|
57
|
+
const rel = (attr("rel") ?? "").toLowerCase().split(/\s+/);
|
|
58
|
+
if (rel.includes("stylesheet")) {
|
|
59
|
+
return [["href", attr("media") !== undefined || rel.includes("alternate") ? "candidate" : "requires"]];
|
|
60
|
+
}
|
|
61
|
+
return ["icon", "preload", "modulepreload", "prefetch"].some(r => rel.includes(r)) ? [["href", "candidate"]] : [];
|
|
62
|
+
}
|
|
63
|
+
case "img":
|
|
64
|
+
return [["src", lazy ? "candidate" : "requires"], ["srcset", "candidate", true]];
|
|
65
|
+
case "iframe":
|
|
66
|
+
return [["src", lazy ? "candidate" : "requires"]];
|
|
67
|
+
case "source":
|
|
68
|
+
return [["src", "candidate"], ["srcset", "candidate", true]];
|
|
69
|
+
case "video":
|
|
70
|
+
return [["src", "candidate"], ["poster", "candidate"]];
|
|
71
|
+
case "audio":
|
|
72
|
+
return [["src", "candidate"]];
|
|
73
|
+
case "object":
|
|
74
|
+
return [["data", "requires"]];
|
|
75
|
+
case "script":
|
|
76
|
+
return [["src", "requires"]];
|
|
77
|
+
case "use":
|
|
78
|
+
return [["href", "requires"], ["xlink:href", "requires"]];
|
|
79
|
+
default:
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export const html = {
|
|
84
|
+
claims: location => /\.html?$/i.test(location),
|
|
85
|
+
analyze(text) {
|
|
86
|
+
const requests = [];
|
|
87
|
+
let base;
|
|
88
|
+
const visit = (node) => {
|
|
89
|
+
if ("tagName" in node) {
|
|
90
|
+
if (node.tagName === "base" && base === undefined) {
|
|
91
|
+
base = node.attrs.find(a => a.name === "href" && !a.prefix)?.value;
|
|
92
|
+
}
|
|
93
|
+
for (const [name, kind, isSrcset] of loads(node)) {
|
|
94
|
+
const attribute = node.attrs.find(a => (a.prefix ? `${a.prefix}:${a.name}` : a.name) === name);
|
|
95
|
+
const span = node.sourceCodeLocation?.attrs?.[name];
|
|
96
|
+
const range = attribute && span && valueRange(text, span);
|
|
97
|
+
if (!attribute || !range)
|
|
98
|
+
continue;
|
|
99
|
+
if (!isSrcset) {
|
|
100
|
+
requests.push({ ...range, request: attribute.value.trim(), kind });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// The ranges come from the text as written, the requests from the decoded value; they
|
|
104
|
+
// pair up unless an entity changed where a comma or a space falls.
|
|
105
|
+
const written = srcsetUrls(text.slice(range.start, range.end));
|
|
106
|
+
const decoded = srcsetUrls(attribute.value);
|
|
107
|
+
if (written.length !== decoded.length)
|
|
108
|
+
continue;
|
|
109
|
+
written.forEach((w, k) => {
|
|
110
|
+
requests.push({
|
|
111
|
+
start: range.start + w.start,
|
|
112
|
+
end: range.start + w.end,
|
|
113
|
+
request: attribute.value.slice(decoded[k].start, decoded[k].end),
|
|
114
|
+
kind,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if ("childNodes" in node)
|
|
120
|
+
for (const child of node.childNodes)
|
|
121
|
+
visit(child);
|
|
122
|
+
if ("content" in node)
|
|
123
|
+
visit(node.content);
|
|
124
|
+
};
|
|
125
|
+
visit(parse(text, { sourceCodeLocationInfo: true }));
|
|
126
|
+
return base === undefined ? { requests } : { requests, base };
|
|
127
|
+
},
|
|
128
|
+
};
|