frond-js 0.4.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/LICENSE +21 -0
- package/README.en.md +247 -0
- package/README.md +211 -0
- package/dist/abort-BY8vBk0v.d.cts +99 -0
- package/dist/abort-BY8vBk0v.d.ts +99 -0
- package/dist/adapter-3G46J3CA.cjs +503 -0
- package/dist/adapter-3ONQWJVQ.js +501 -0
- package/dist/adapter-55QHWRSE.js +124 -0
- package/dist/adapter-DF34GBWJ.cjs +19 -0
- package/dist/adapter-EXTNILTC.cjs +126 -0
- package/dist/adapter-GOFC7TMC.js +284 -0
- package/dist/adapter-LRJTQ47I.cjs +286 -0
- package/dist/adapter-TWWZML4A.js +17 -0
- package/dist/adapter-ZM5FQTJT.js +1415 -0
- package/dist/adapter-ZRNSDQUV.cjs +1417 -0
- package/dist/chunk-2SUG7YFZ.cjs +108 -0
- package/dist/chunk-B2L2YVXD.js +89 -0
- package/dist/chunk-D4KWSEZD.js +393 -0
- package/dist/chunk-EZTIZO6R.cjs +430 -0
- package/dist/chunk-G7DLWGBW.cjs +103 -0
- package/dist/chunk-GTGLDLJD.cjs +479 -0
- package/dist/chunk-IIV6VIUJ.cjs +83 -0
- package/dist/chunk-JDTHZQUK.js +102 -0
- package/dist/chunk-JJVXT3AC.js +30 -0
- package/dist/chunk-LIXRYQL2.js +473 -0
- package/dist/chunk-NABYHI6X.cjs +400 -0
- package/dist/chunk-SJVOYNTF.js +425 -0
- package/dist/chunk-SLI2YL25.cjs +252 -0
- package/dist/chunk-U4264IQH.js +78 -0
- package/dist/chunk-UY2YRCFC.js +250 -0
- package/dist/chunk-WCZTTQ7Z.cjs +32 -0
- package/dist/core/index.cjs +162 -0
- package/dist/core/index.d.cts +321 -0
- package/dist/core/index.d.ts +321 -0
- package/dist/core/index.js +49 -0
- package/dist/default-DRLIJX73.js +1183 -0
- package/dist/default-UK52WOO5.cjs +1192 -0
- package/dist/formats/epub/index.cjs +29 -0
- package/dist/formats/epub/index.d.cts +286 -0
- package/dist/formats/epub/index.d.ts +286 -0
- package/dist/formats/epub/index.js +11 -0
- package/dist/index.cjs +655 -0
- package/dist/index.d.cts +335 -0
- package/dist/index.d.ts +335 -0
- package/dist/index.js +600 -0
- package/dist/render/index.cjs +2 -0
- package/dist/render/index.d.cts +116 -0
- package/dist/render/index.d.ts +116 -0
- package/dist/render/index.js +1 -0
- package/dist/types-B76GOMxj.d.ts +129 -0
- package/dist/types-B7mslPBY.d.cts +166 -0
- package/dist/types-B7mslPBY.d.ts +166 -0
- package/dist/types-BH88rUYt.d.cts +129 -0
- package/dist/types-C-5eHSRH.d.ts +379 -0
- package/dist/types-CPUqTEPW.d.cts +379 -0
- package/dist/types-DQYmArgv.d.cts +17 -0
- package/dist/types-DQYmArgv.d.ts +17 -0
- package/package.json +115 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/core/formats/probe.ts
|
|
4
|
+
var PROBE_PREFIX_BYTES = 1024;
|
|
5
|
+
var PDF_MAGIC = "%PDF-";
|
|
6
|
+
var ZIP_MAGIC = [80, 75, 3, 4];
|
|
7
|
+
var MOBI_MARKER = "BOOKMOBI";
|
|
8
|
+
var MOBI_MARKER_OFFSET = 60;
|
|
9
|
+
var XML_DECLARATION = "<?xml";
|
|
10
|
+
var FB2_ROOT = "<FictionBook";
|
|
11
|
+
var UTF8_BOM = [239, 187, 191];
|
|
12
|
+
var EPUB_CONTAINER_ENTRY = "META-INF/container.xml";
|
|
13
|
+
var FB2_EXTENSION = ".fb2";
|
|
14
|
+
var IMAGE_EXTENSIONS = [".jpg", ".jpeg", ".png", ".gif", ".webp", ".avif"];
|
|
15
|
+
function probeFormat(prefix) {
|
|
16
|
+
if (matchesAscii(prefix, 0, PDF_MAGIC)) {
|
|
17
|
+
return { kind: "format", format: "pdf", evidence: `%PDF- \u9B54\u6570\uFF08offset 0\uFF09` };
|
|
18
|
+
}
|
|
19
|
+
if (matchesBytes(prefix, 0, ZIP_MAGIC)) {
|
|
20
|
+
return {
|
|
21
|
+
kind: "container",
|
|
22
|
+
container: "zip",
|
|
23
|
+
evidence: "PK\\x03\\x04 \u9B54\u6570\uFF08offset 0\uFF09\u2014\u2014 ZIP \u5BB9\u5668\uFF0C\u9700\u8BFB\u6761\u76EE\u540D\u624D\u80FD\u5B9A\u683C\u5F0F"
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (matchesAscii(prefix, MOBI_MARKER_OFFSET, MOBI_MARKER)) {
|
|
27
|
+
return {
|
|
28
|
+
kind: "format",
|
|
29
|
+
format: "mobi",
|
|
30
|
+
evidence: `BOOKMOBI \u6807\u8BB0\uFF08offset ${String(MOBI_MARKER_OFFSET)}\uFF0CPalmDB \u5934\uFF09`
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (looksLikeFb2(prefix)) {
|
|
34
|
+
return {
|
|
35
|
+
kind: "format",
|
|
36
|
+
format: "fb2",
|
|
37
|
+
evidence: "XML \u58F0\u660E\u6216 <FictionBook> \u6839\u5143\u7D20 + \u542B <FictionBook>"
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function probeZipContainer(names) {
|
|
43
|
+
const entries = names.filter((name) => !name.endsWith("/"));
|
|
44
|
+
if (entries.includes(EPUB_CONTAINER_ENTRY)) {
|
|
45
|
+
return {
|
|
46
|
+
kind: "format",
|
|
47
|
+
format: "epub",
|
|
48
|
+
evidence: `ZIP \u5185\u542B ${EPUB_CONTAINER_ENTRY}\uFF08OCF \u89C4\u8303\u56FA\u5B9A\u4F4D\u7F6E\uFF09`
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if (entries.some((name) => name.toLowerCase().endsWith(FB2_EXTENSION))) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "format",
|
|
54
|
+
format: "fb2",
|
|
55
|
+
evidence: `ZIP \u5185\u542B ${FB2_EXTENSION} \u4E14\u65E0 ${EPUB_CONTAINER_ENTRY}\uFF08.fb2.zip \u53D8\u4F53\uFF09`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
if (entries.some(
|
|
59
|
+
(name) => IMAGE_EXTENSIONS.some((extension) => name.toLowerCase().endsWith(extension))
|
|
60
|
+
)) {
|
|
61
|
+
return {
|
|
62
|
+
kind: "format",
|
|
63
|
+
format: "cbz",
|
|
64
|
+
evidence: `ZIP \u5185\u53EA\u6709\u56FE\u7247\u6761\u76EE\u4E14\u65E0 ${EPUB_CONTAINER_ENTRY}\uFF08CBZ\uFF09`
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function matchesAscii(bytes, offset, text) {
|
|
70
|
+
if (offset < 0 || offset + text.length > bytes.byteLength) return false;
|
|
71
|
+
for (let i = 0; i < text.length; i++) {
|
|
72
|
+
if (bytes[offset + i] !== text.charCodeAt(i)) return false;
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
function matchesBytes(bytes, offset, expected) {
|
|
77
|
+
if (offset + expected.length > bytes.byteLength) return false;
|
|
78
|
+
for (let i = 0; i < expected.length; i++) {
|
|
79
|
+
if (bytes[offset + i] !== expected[i]) return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
function indexOfAscii(bytes, text) {
|
|
84
|
+
const limit = bytes.byteLength - text.length;
|
|
85
|
+
for (let start = 0; start <= limit; start++) {
|
|
86
|
+
if (matchesAscii(bytes, start, text)) return start;
|
|
87
|
+
}
|
|
88
|
+
return -1;
|
|
89
|
+
}
|
|
90
|
+
function looksLikeFb2(prefix) {
|
|
91
|
+
if (indexOfAscii(prefix, FB2_ROOT) === -1) return false;
|
|
92
|
+
let start = matchesBytes(prefix, 0, UTF8_BOM) ? UTF8_BOM.length : 0;
|
|
93
|
+
for (; ; ) {
|
|
94
|
+
const byte = prefix[start];
|
|
95
|
+
if (byte === void 0 || !isAsciiWhitespace(byte)) break;
|
|
96
|
+
start++;
|
|
97
|
+
}
|
|
98
|
+
return matchesAscii(prefix, start, XML_DECLARATION) || matchesAscii(prefix, start, FB2_ROOT);
|
|
99
|
+
}
|
|
100
|
+
function isAsciiWhitespace(byte) {
|
|
101
|
+
return byte === 32 || byte === 9 || byte === 13 || byte === 10 || byte === 12 || byte === 11;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
exports.EPUB_CONTAINER_ENTRY = EPUB_CONTAINER_ENTRY;
|
|
105
|
+
exports.IMAGE_EXTENSIONS = IMAGE_EXTENSIONS;
|
|
106
|
+
exports.PROBE_PREFIX_BYTES = PROBE_PREFIX_BYTES;
|
|
107
|
+
exports.probeFormat = probeFormat;
|
|
108
|
+
exports.probeZipContainer = probeZipContainer;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var FrondError = class extends Error {
|
|
3
|
+
/** 稳定错误码,用于程序化分支。 */
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message, options) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "FrondError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
if (options?.cause !== void 0) {
|
|
10
|
+
this.cause = options.cause;
|
|
11
|
+
}
|
|
12
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var ParseError = class extends FrondError {
|
|
16
|
+
constructor(message, options) {
|
|
17
|
+
super("ERR_PARSE", message, options);
|
|
18
|
+
this.name = "ParseError";
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var FormatError = class extends FrondError {
|
|
22
|
+
constructor(message, options) {
|
|
23
|
+
super("ERR_FORMAT", message, options);
|
|
24
|
+
this.name = "FormatError";
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var NetworkError = class extends FrondError {
|
|
28
|
+
constructor(message, options) {
|
|
29
|
+
super("ERR_NETWORK", message, options);
|
|
30
|
+
this.name = "NetworkError";
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
var SecurityError = class extends FrondError {
|
|
34
|
+
constructor(message, options) {
|
|
35
|
+
super("ERR_SECURITY", message, options);
|
|
36
|
+
this.name = "SecurityError";
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
var AbortError = class extends FrondError {
|
|
40
|
+
constructor(message = "\u64CD\u4F5C\u5DF2\u53D6\u6D88", options) {
|
|
41
|
+
super("ERR_ABORT", message, options);
|
|
42
|
+
this.name = "AbortError";
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
var StateError = class extends FrondError {
|
|
46
|
+
constructor(message, options) {
|
|
47
|
+
super("ERR_STATE", message, options);
|
|
48
|
+
this.name = "StateError";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
function isFrondError(value) {
|
|
52
|
+
return value instanceof FrondError;
|
|
53
|
+
}
|
|
54
|
+
function isAbortError(value) {
|
|
55
|
+
if (value instanceof AbortError) return true;
|
|
56
|
+
return value instanceof Error && value.name === "AbortError";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/core/abort.ts
|
|
60
|
+
function createAbortError(reason) {
|
|
61
|
+
const message = reason instanceof Error && reason.message !== "" ? reason.message : "\u64CD\u4F5C\u5DF2\u53D6\u6D88";
|
|
62
|
+
return new AbortError(message, reason === void 0 ? void 0 : { cause: reason });
|
|
63
|
+
}
|
|
64
|
+
function throwIfAborted(signal) {
|
|
65
|
+
if (signal === void 0 || !signal.aborted) return;
|
|
66
|
+
throw createAbortError(signal.reason);
|
|
67
|
+
}
|
|
68
|
+
function linkSignals(...signals) {
|
|
69
|
+
const active = signals.filter((signal) => signal !== void 0);
|
|
70
|
+
if (active.length === 0) return void 0;
|
|
71
|
+
if (active.length === 1) return active[0];
|
|
72
|
+
return AbortSignal.any(active);
|
|
73
|
+
}
|
|
74
|
+
function onAbort(signal, handler) {
|
|
75
|
+
if (signal === void 0) return () => void 0;
|
|
76
|
+
if (signal.aborted) {
|
|
77
|
+
handler();
|
|
78
|
+
return () => void 0;
|
|
79
|
+
}
|
|
80
|
+
const listener = () => {
|
|
81
|
+
handler();
|
|
82
|
+
};
|
|
83
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
84
|
+
return () => {
|
|
85
|
+
signal.removeEventListener("abort", listener);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export { AbortError, FormatError, FrondError, NetworkError, ParseError, SecurityError, StateError, createAbortError, isAbortError, isFrondError, linkSignals, onAbort, throwIfAborted };
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import { ParseError } from './chunk-B2L2YVXD.js';
|
|
2
|
+
|
|
3
|
+
// src/core/xml/parse.ts
|
|
4
|
+
var PREDEFINED_ENTITIES = /* @__PURE__ */ new Map([
|
|
5
|
+
["lt", "<"],
|
|
6
|
+
["gt", ">"],
|
|
7
|
+
["amp", "&"],
|
|
8
|
+
["quot", '"'],
|
|
9
|
+
["apos", "'"]
|
|
10
|
+
]);
|
|
11
|
+
var COMMENT_OPEN = "<!--";
|
|
12
|
+
var COMMENT_CLOSE = "-->";
|
|
13
|
+
var CDATA_OPEN = "<![CDATA[";
|
|
14
|
+
var CDATA_CLOSE = "]]>";
|
|
15
|
+
var DOCTYPE_OPEN = "<!DOCTYPE";
|
|
16
|
+
var PROCESSING_INSTRUCTION_CLOSE = "?>";
|
|
17
|
+
var CODE_TAB = 9;
|
|
18
|
+
var CODE_LF = 10;
|
|
19
|
+
var CODE_CR = 13;
|
|
20
|
+
var CODE_SPACE = 32;
|
|
21
|
+
var CODE_BANG = 33;
|
|
22
|
+
var CODE_QUOTE = 34;
|
|
23
|
+
var CODE_HASH = 35;
|
|
24
|
+
var CODE_APOS = 39;
|
|
25
|
+
var CODE_SLASH = 47;
|
|
26
|
+
var CODE_LT = 60;
|
|
27
|
+
var CODE_EQ = 61;
|
|
28
|
+
var CODE_GT = 62;
|
|
29
|
+
var CODE_QUESTION = 63;
|
|
30
|
+
var CODE_OPEN_BRACKET = 91;
|
|
31
|
+
var CODE_CLOSE_BRACKET = 93;
|
|
32
|
+
var CODE_LOWER_X = 120;
|
|
33
|
+
var CODE_BOM = 65279;
|
|
34
|
+
var MAX_CODE_POINT = 1114111;
|
|
35
|
+
var DEFAULT_MAX_DEPTH = 512;
|
|
36
|
+
function isWhitespaceCode(code) {
|
|
37
|
+
return code === CODE_SPACE || code === CODE_TAB || code === CODE_LF || code === CODE_CR;
|
|
38
|
+
}
|
|
39
|
+
function isNameTerminator(code) {
|
|
40
|
+
return Number.isNaN(code) || isWhitespaceCode(code) || code === CODE_LT || code === CODE_GT || code === CODE_SLASH || code === CODE_EQ || code === CODE_QUESTION || code === CODE_QUOTE || code === CODE_APOS || code === CODE_BANG;
|
|
41
|
+
}
|
|
42
|
+
function normalizeNewlines(value) {
|
|
43
|
+
return value.includes("\r") ? value.replace(/\r\n?/g, "\n") : value;
|
|
44
|
+
}
|
|
45
|
+
function parseDecimalDigits(text) {
|
|
46
|
+
if (text === "") return void 0;
|
|
47
|
+
let value = 0;
|
|
48
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
49
|
+
const code = text.charCodeAt(index);
|
|
50
|
+
if (code < 48 || code > 57) return void 0;
|
|
51
|
+
value = value * 10 + (code - 48);
|
|
52
|
+
if (value > MAX_CODE_POINT) return void 0;
|
|
53
|
+
}
|
|
54
|
+
return value;
|
|
55
|
+
}
|
|
56
|
+
function parseHexDigits(text) {
|
|
57
|
+
if (text === "") return void 0;
|
|
58
|
+
let value = 0;
|
|
59
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
60
|
+
const code = text.charCodeAt(index);
|
|
61
|
+
let digit;
|
|
62
|
+
if (code >= 48 && code <= 57) {
|
|
63
|
+
digit = code - 48;
|
|
64
|
+
} else if (code >= 97 && code <= 102) {
|
|
65
|
+
digit = code - 97 + 10;
|
|
66
|
+
} else if (code >= 65 && code <= 70) {
|
|
67
|
+
digit = code - 65 + 10;
|
|
68
|
+
} else {
|
|
69
|
+
return void 0;
|
|
70
|
+
}
|
|
71
|
+
value = value * 16 + digit;
|
|
72
|
+
if (value > MAX_CODE_POINT) return void 0;
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
function decodeReference(body) {
|
|
77
|
+
if (body === "") return void 0;
|
|
78
|
+
if (body.charCodeAt(0) !== CODE_HASH) return PREDEFINED_ENTITIES.get(body);
|
|
79
|
+
const digits = body.slice(1);
|
|
80
|
+
if (digits === "") return void 0;
|
|
81
|
+
let codePoint;
|
|
82
|
+
if (digits.charCodeAt(0) === CODE_LOWER_X) {
|
|
83
|
+
codePoint = parseHexDigits(digits.slice(1));
|
|
84
|
+
} else {
|
|
85
|
+
codePoint = parseDecimalDigits(digits);
|
|
86
|
+
}
|
|
87
|
+
if (codePoint === void 0 || codePoint > MAX_CODE_POINT) return void 0;
|
|
88
|
+
return String.fromCodePoint(codePoint);
|
|
89
|
+
}
|
|
90
|
+
function decodeEntities(raw) {
|
|
91
|
+
if (!raw.includes("&")) return raw;
|
|
92
|
+
let result = "";
|
|
93
|
+
let cursor = 0;
|
|
94
|
+
for (; ; ) {
|
|
95
|
+
const amp = raw.indexOf("&", cursor);
|
|
96
|
+
if (amp === -1) return result + raw.slice(cursor);
|
|
97
|
+
result += raw.slice(cursor, amp);
|
|
98
|
+
const semicolon = raw.indexOf(";", amp + 1);
|
|
99
|
+
if (semicolon === -1) {
|
|
100
|
+
return result + raw.slice(amp);
|
|
101
|
+
}
|
|
102
|
+
const decoded = decodeReference(raw.slice(amp + 1, semicolon));
|
|
103
|
+
result += decoded ?? raw.slice(amp, semicolon + 1);
|
|
104
|
+
cursor = semicolon + 1;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function splitName(name) {
|
|
108
|
+
const colon = name.indexOf(":");
|
|
109
|
+
if (colon === -1) return { prefix: "", localName: name };
|
|
110
|
+
return { prefix: name.slice(0, colon), localName: name.slice(colon + 1) };
|
|
111
|
+
}
|
|
112
|
+
var XmlParser = class {
|
|
113
|
+
#source;
|
|
114
|
+
#label;
|
|
115
|
+
#maxDepth;
|
|
116
|
+
#pos = 0;
|
|
117
|
+
/** 当前**已进入**的元素层数(根元素为 0 → 进入后即 1)。由 `#parseElement` 维护。 */
|
|
118
|
+
#depth = 0;
|
|
119
|
+
constructor(source, label, maxDepth) {
|
|
120
|
+
this.#source = source;
|
|
121
|
+
this.#label = label;
|
|
122
|
+
this.#maxDepth = maxDepth;
|
|
123
|
+
}
|
|
124
|
+
parseDocument() {
|
|
125
|
+
if (this.#source.charCodeAt(0) === CODE_BOM) this.#pos = 1;
|
|
126
|
+
for (; ; ) {
|
|
127
|
+
this.#skipMisc();
|
|
128
|
+
if (this.#startsWith(DOCTYPE_OPEN)) {
|
|
129
|
+
this.#skipDoctype();
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
if (this.#atEnd()) this.#fail("\u6587\u6863\u7F3A\u5C11\u6839\u5143\u7D20", this.#pos);
|
|
135
|
+
const root = this.#parseElement();
|
|
136
|
+
this.#skipMisc();
|
|
137
|
+
if (!this.#atEnd()) this.#fail("\u6839\u5143\u7D20\u4E4B\u540E\u5B58\u5728\u591A\u4F59\u5185\u5BB9", this.#pos);
|
|
138
|
+
return root;
|
|
139
|
+
}
|
|
140
|
+
// -- 位置与错误 ----------------------------------------------------------
|
|
141
|
+
#atEnd() {
|
|
142
|
+
return this.#pos >= this.#source.length;
|
|
143
|
+
}
|
|
144
|
+
#startsWith(text) {
|
|
145
|
+
return this.#source.startsWith(text, this.#pos);
|
|
146
|
+
}
|
|
147
|
+
#skipWhitespace() {
|
|
148
|
+
while (isWhitespaceCode(this.#source.charCodeAt(this.#pos))) this.#pos += 1;
|
|
149
|
+
}
|
|
150
|
+
#fail(reason, offset) {
|
|
151
|
+
throw new ParseError(`XML \u89E3\u6790\u5931\u8D25\uFF1A${reason}\uFF08\u6E90: ${this.#label}\uFF0C\u504F\u79FB ${String(offset)}\uFF09`);
|
|
152
|
+
}
|
|
153
|
+
// -- 跳过类构造 ----------------------------------------------------------
|
|
154
|
+
#skipMisc() {
|
|
155
|
+
for (; ; ) {
|
|
156
|
+
const code = this.#source.charCodeAt(this.#pos);
|
|
157
|
+
if (isWhitespaceCode(code)) {
|
|
158
|
+
this.#pos += 1;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (this.#startsWith(COMMENT_OPEN)) {
|
|
162
|
+
this.#skipComment();
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (this.#startsWith("<?")) {
|
|
166
|
+
this.#skipProcessingInstruction();
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
#skipComment() {
|
|
173
|
+
const start = this.#pos;
|
|
174
|
+
const end = this.#source.indexOf(COMMENT_CLOSE, start + COMMENT_OPEN.length);
|
|
175
|
+
if (end === -1) this.#fail("\u6CE8\u91CA\u672A\u95ED\u5408", start);
|
|
176
|
+
this.#pos = end + COMMENT_CLOSE.length;
|
|
177
|
+
}
|
|
178
|
+
#skipProcessingInstruction() {
|
|
179
|
+
const start = this.#pos;
|
|
180
|
+
const end = this.#source.indexOf(PROCESSING_INSTRUCTION_CLOSE, start + 2);
|
|
181
|
+
if (end === -1) this.#fail("\u5904\u7406\u6307\u4EE4\u672A\u95ED\u5408", start);
|
|
182
|
+
this.#pos = end + PROCESSING_INSTRUCTION_CLOSE.length;
|
|
183
|
+
}
|
|
184
|
+
/** 跳过 DOCTYPE。内部子集用方括号计数,引号内的 `>` 不视为结束。 */
|
|
185
|
+
#skipDoctype() {
|
|
186
|
+
const start = this.#pos;
|
|
187
|
+
let index = start + DOCTYPE_OPEN.length;
|
|
188
|
+
let depth = 0;
|
|
189
|
+
while (index < this.#source.length) {
|
|
190
|
+
const code = this.#source.charCodeAt(index);
|
|
191
|
+
if (code === CODE_QUOTE || code === CODE_APOS) {
|
|
192
|
+
const close = this.#source.indexOf(String.fromCharCode(code), index + 1);
|
|
193
|
+
if (close === -1) this.#fail("DOCTYPE \u4E2D\u7684\u5F15\u53F7\u672A\u95ED\u5408", index);
|
|
194
|
+
index = close + 1;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (code === CODE_OPEN_BRACKET) {
|
|
198
|
+
depth += 1;
|
|
199
|
+
} else if (code === CODE_CLOSE_BRACKET) {
|
|
200
|
+
depth -= 1;
|
|
201
|
+
} else if (code === CODE_GT && depth <= 0) {
|
|
202
|
+
this.#pos = index + 1;
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
index += 1;
|
|
206
|
+
}
|
|
207
|
+
this.#fail("DOCTYPE \u672A\u95ED\u5408", start);
|
|
208
|
+
}
|
|
209
|
+
// -- 元素 ----------------------------------------------------------------
|
|
210
|
+
#parseElement() {
|
|
211
|
+
if (this.#depth >= this.#maxDepth) {
|
|
212
|
+
this.#fail(
|
|
213
|
+
`\u5143\u7D20\u5D4C\u5957\u8D85\u8FC7\u4E0A\u9650 ${String(this.#maxDepth)} \u5C42 \u2014\u2014 \u7578\u5F62\u6587\u6863\u5E38\u4EE5\u6781\u6DF1\u5D4C\u5957\u89E6\u53D1\u5BBF\u4E3B\u6808\u6EA2\u51FA\uFF0C\u6545\u6309\u89E3\u6790\u5931\u8D25\u5904\u7406\u800C\u975E\u7EE7\u7EED\u9012\u5F52`,
|
|
214
|
+
this.#pos
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const { name, attributes, selfClosing } = this.#parseStartTag();
|
|
218
|
+
const { prefix, localName } = splitName(name);
|
|
219
|
+
if (selfClosing) {
|
|
220
|
+
return { type: "element", name, prefix, localName, attributes, children: [] };
|
|
221
|
+
}
|
|
222
|
+
this.#depth += 1;
|
|
223
|
+
const children = this.#parseContent(name);
|
|
224
|
+
this.#depth -= 1;
|
|
225
|
+
return { type: "element", name, prefix, localName, attributes, children };
|
|
226
|
+
}
|
|
227
|
+
#parseStartTag() {
|
|
228
|
+
const start = this.#pos;
|
|
229
|
+
if (this.#source.charCodeAt(this.#pos) !== CODE_LT) {
|
|
230
|
+
this.#fail('\u671F\u671B\u5143\u7D20\u8D77\u59CB\u6807\u8BB0 "<"', start);
|
|
231
|
+
}
|
|
232
|
+
this.#pos += 1;
|
|
233
|
+
const name = this.#readName();
|
|
234
|
+
if (name === "") this.#fail("\u5143\u7D20\u540D\u7F3A\u5931", start);
|
|
235
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
236
|
+
for (; ; ) {
|
|
237
|
+
const beforeSpace = this.#pos;
|
|
238
|
+
this.#skipWhitespace();
|
|
239
|
+
const hadSpace = this.#pos > beforeSpace;
|
|
240
|
+
const code = this.#source.charCodeAt(this.#pos);
|
|
241
|
+
if (code === CODE_SLASH) {
|
|
242
|
+
if (this.#source.charCodeAt(this.#pos + 1) !== CODE_GT) {
|
|
243
|
+
this.#fail('\u81EA\u95ED\u5408\u6807\u8BB0\u683C\u5F0F\u9519\u8BEF\uFF0C\u671F\u671B "/>"', this.#pos);
|
|
244
|
+
}
|
|
245
|
+
this.#pos += 2;
|
|
246
|
+
return { name, attributes, selfClosing: true };
|
|
247
|
+
}
|
|
248
|
+
if (code === CODE_GT) {
|
|
249
|
+
this.#pos += 1;
|
|
250
|
+
return { name, attributes, selfClosing: false };
|
|
251
|
+
}
|
|
252
|
+
if (!hadSpace) this.#fail("\u5C5E\u6027\u4E4B\u95F4\u7F3A\u5C11\u7A7A\u767D\u5206\u9694", this.#pos);
|
|
253
|
+
const attributeStart = this.#pos;
|
|
254
|
+
const attributeName = this.#readName();
|
|
255
|
+
if (attributeName === "") this.#fail("\u5C5E\u6027\u540D\u7F3A\u5931", attributeStart);
|
|
256
|
+
if (attributes.has(attributeName)) {
|
|
257
|
+
this.#fail(`\u5C5E\u6027 "${attributeName}" \u91CD\u590D`, attributeStart);
|
|
258
|
+
}
|
|
259
|
+
this.#skipWhitespace();
|
|
260
|
+
if (this.#source.charCodeAt(this.#pos) !== CODE_EQ) {
|
|
261
|
+
this.#fail(`\u5C5E\u6027 "${attributeName}" \u7F3A\u5C11 "="`, this.#pos);
|
|
262
|
+
}
|
|
263
|
+
this.#pos += 1;
|
|
264
|
+
this.#skipWhitespace();
|
|
265
|
+
const quote = this.#source.charCodeAt(this.#pos);
|
|
266
|
+
if (quote !== CODE_QUOTE && quote !== CODE_APOS) {
|
|
267
|
+
this.#fail(`\u5C5E\u6027 "${attributeName}" \u7684\u503C\u5FC5\u987B\u7528\u5F15\u53F7\u5305\u88F9`, this.#pos);
|
|
268
|
+
}
|
|
269
|
+
this.#pos += 1;
|
|
270
|
+
const close = this.#source.indexOf(String.fromCharCode(quote), this.#pos);
|
|
271
|
+
if (close === -1) {
|
|
272
|
+
this.#fail(`\u5C5E\u6027 "${attributeName}" \u7684\u5F15\u53F7\u672A\u95ED\u5408`, this.#pos);
|
|
273
|
+
}
|
|
274
|
+
const rawValue = this.#source.slice(this.#pos, close);
|
|
275
|
+
attributes.set(attributeName, decodeEntities(normalizeNewlines(rawValue)));
|
|
276
|
+
this.#pos = close + 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
#parseContent(expectedName) {
|
|
280
|
+
const children = [];
|
|
281
|
+
for (; ; ) {
|
|
282
|
+
if (this.#atEnd()) this.#fail(`\u5143\u7D20 <${expectedName}> \u672A\u95ED\u5408`, this.#pos);
|
|
283
|
+
if (this.#source.charCodeAt(this.#pos) !== CODE_LT) {
|
|
284
|
+
children.push(this.#readText());
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const next = this.#source.charCodeAt(this.#pos + 1);
|
|
288
|
+
if (next === CODE_SLASH) {
|
|
289
|
+
this.#parseEndTag(expectedName);
|
|
290
|
+
return children;
|
|
291
|
+
}
|
|
292
|
+
if (this.#startsWith(COMMENT_OPEN)) {
|
|
293
|
+
this.#skipComment();
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (this.#startsWith(CDATA_OPEN)) {
|
|
297
|
+
children.push(this.#readCdata());
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (next === CODE_QUESTION) {
|
|
301
|
+
this.#skipProcessingInstruction();
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
if (next === CODE_BANG) {
|
|
305
|
+
this.#fail('\u5143\u7D20\u5185\u5BB9\u4E2D\u4E0D\u5141\u8BB8\u51FA\u73B0 "<!" \u58F0\u660E', this.#pos);
|
|
306
|
+
}
|
|
307
|
+
children.push(this.#parseElement());
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
#parseEndTag(expectedName) {
|
|
311
|
+
const start = this.#pos;
|
|
312
|
+
this.#pos += 2;
|
|
313
|
+
const name = this.#readName();
|
|
314
|
+
if (name === "") this.#fail("\u7ED3\u675F\u6807\u8BB0\u7F3A\u5C11\u5143\u7D20\u540D", start);
|
|
315
|
+
this.#skipWhitespace();
|
|
316
|
+
if (this.#source.charCodeAt(this.#pos) !== CODE_GT) {
|
|
317
|
+
this.#fail(`\u7ED3\u675F\u6807\u8BB0 </${name}> \u683C\u5F0F\u9519\u8BEF\uFF0C\u671F\u671B ">"`, this.#pos);
|
|
318
|
+
}
|
|
319
|
+
this.#pos += 1;
|
|
320
|
+
if (name !== expectedName) {
|
|
321
|
+
this.#fail(`\u6807\u7B7E\u4E0D\u5339\u914D\uFF1A\u671F\u671B </${expectedName}>\uFF0C\u5B9E\u9645 </${name}>`, start);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
#readName() {
|
|
325
|
+
const start = this.#pos;
|
|
326
|
+
while (!isNameTerminator(this.#source.charCodeAt(this.#pos))) this.#pos += 1;
|
|
327
|
+
return this.#source.slice(start, this.#pos);
|
|
328
|
+
}
|
|
329
|
+
#readText() {
|
|
330
|
+
const start = this.#pos;
|
|
331
|
+
const next = this.#source.indexOf("<", this.#pos);
|
|
332
|
+
this.#pos = next === -1 ? this.#source.length : next;
|
|
333
|
+
const raw = this.#source.slice(start, this.#pos);
|
|
334
|
+
return { type: "text", value: decodeEntities(normalizeNewlines(raw)), cdata: false };
|
|
335
|
+
}
|
|
336
|
+
#readCdata() {
|
|
337
|
+
const start = this.#pos;
|
|
338
|
+
const contentStart = start + CDATA_OPEN.length;
|
|
339
|
+
const end = this.#source.indexOf(CDATA_CLOSE, contentStart);
|
|
340
|
+
if (end === -1) this.#fail("CDATA \u6BB5\u672A\u95ED\u5408", start);
|
|
341
|
+
this.#pos = end + CDATA_CLOSE.length;
|
|
342
|
+
return {
|
|
343
|
+
type: "text",
|
|
344
|
+
value: normalizeNewlines(this.#source.slice(contentStart, end)),
|
|
345
|
+
cdata: true
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
function parseXml(source, options) {
|
|
350
|
+
return new XmlParser(
|
|
351
|
+
source,
|
|
352
|
+
options?.source ?? "<anonymous>",
|
|
353
|
+
options?.maxDepth ?? DEFAULT_MAX_DEPTH
|
|
354
|
+
).parseDocument();
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// src/core/xml/query.ts
|
|
358
|
+
function findAll(root, localName) {
|
|
359
|
+
const matched = [];
|
|
360
|
+
const walk = (node) => {
|
|
361
|
+
for (const child of node.children) {
|
|
362
|
+
if (child.type !== "element") continue;
|
|
363
|
+
if (child.localName === localName) matched.push(child);
|
|
364
|
+
walk(child);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
walk(root);
|
|
368
|
+
return matched;
|
|
369
|
+
}
|
|
370
|
+
function findFirst(root, localName) {
|
|
371
|
+
for (const child of root.children) {
|
|
372
|
+
if (child.type !== "element") continue;
|
|
373
|
+
if (child.localName === localName) return child;
|
|
374
|
+
const found = findFirst(child, localName);
|
|
375
|
+
if (found !== void 0) return found;
|
|
376
|
+
}
|
|
377
|
+
return void 0;
|
|
378
|
+
}
|
|
379
|
+
function getAttribute(element, name) {
|
|
380
|
+
return element.attributes.get(name);
|
|
381
|
+
}
|
|
382
|
+
function getText(element) {
|
|
383
|
+
let text = "";
|
|
384
|
+
for (const child of element.children) {
|
|
385
|
+
text += child.type === "text" ? child.value : getText(child);
|
|
386
|
+
}
|
|
387
|
+
return text;
|
|
388
|
+
}
|
|
389
|
+
function isElement(node) {
|
|
390
|
+
return node.type === "element";
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export { findAll, findFirst, getAttribute, getText, isElement, parseXml };
|