@sametalmdr/word-editor 0.2.0-alpha → 0.3.0-alpha
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/CHANGELOG.md +108 -70
- package/LICENSE +21 -21
- package/README.md +286 -247
- package/dist/canvas/box-clipboard.js +238 -238
- package/dist/canvas/box-selection.js +80 -80
- package/dist/canvas/canvas-editor.js +502 -502
- package/dist/canvas/controller.js +717 -717
- package/dist/canvas/geometry.js +131 -131
- package/dist/canvas/gesture.d.ts +0 -1
- package/dist/canvas/gesture.js +289 -289
- package/dist/canvas/handles.js +212 -212
- package/dist/canvas/menu.js +82 -82
- package/dist/canvas/mode-controller.js +81 -81
- package/dist/canvas/nudge.js +108 -108
- package/dist/canvas/overlay.js +149 -149
- package/dist/canvas/snap.js +132 -132
- package/dist/canvas/viewport.js +112 -112
- package/dist/canvas/z-order.js +171 -171
- package/dist/commands/alignment.js +150 -150
- package/dist/commands/box.js +272 -272
- package/dist/commands/clipboard.js +93 -93
- package/dist/commands/graphemes.js +112 -112
- package/dist/commands/marks.js +153 -153
- package/dist/commands/paragraph-props.js +176 -176
- package/dist/commands/size.js +368 -368
- package/dist/commands/text.js +291 -291
- package/dist/docx.d.ts +1 -0
- package/dist/docx.js +15 -13
- package/dist/editor/editing-host.js +434 -434
- package/dist/element.js +41 -41
- package/dist/fonts/OFL-Carlito.txt +94 -0
- package/dist/fonts/calibri-metrics.d.ts +27 -0
- package/dist/fonts/calibri-metrics.js +88 -0
- package/dist/history/history.js +433 -433
- package/dist/index.js +63 -63
- package/dist/input/beforeinput.js +224 -224
- package/dist/input/clipboard.js +107 -107
- package/dist/input/composition.js +341 -341
- package/dist/input/keymap.js +54 -54
- package/dist/io/ooxml/custgeom.js +72 -72
- package/dist/io/ooxml/drawingml.js +232 -232
- package/dist/io/ooxml/edits.js +67 -67
- package/dist/io/ooxml/export-splice.js +880 -880
- package/dist/io/ooxml/export.js +365 -365
- package/dist/io/ooxml/fill.d.ts +93 -8
- package/dist/io/ooxml/fill.js +618 -562
- package/dist/io/ooxml/fit.d.ts +252 -0
- package/dist/io/ooxml/fit.js +598 -0
- package/dist/io/ooxml/import-gate.js +200 -200
- package/dist/io/ooxml/import.js +691 -691
- package/dist/io/ooxml/inflate.js +126 -126
- package/dist/io/ooxml/media.js +87 -87
- package/dist/io/ooxml/ns.d.ts +2 -0
- package/dist/io/ooxml/ns.js +26 -21
- package/dist/io/ooxml/numbering.js +290 -290
- package/dist/io/ooxml/opc-write.js +171 -171
- package/dist/io/ooxml/provenance.js +93 -93
- package/dist/io/ooxml/recipe.js +708 -708
- package/dist/io/ooxml/relationships.js +59 -59
- package/dist/io/ooxml/run-fonts.d.ts +89 -0
- package/dist/io/ooxml/run-fonts.js +378 -0
- package/dist/io/ooxml/slots.d.ts +4 -0
- package/dist/io/ooxml/slots.js +582 -581
- package/dist/io/ooxml/styles-resolve.js +257 -257
- package/dist/io/ooxml/units.js +43 -43
- package/dist/io/ooxml/value.js +120 -120
- package/dist/io/ooxml/xml-scan.js +616 -616
- package/dist/io/ooxml/zip-container.js +228 -228
- package/dist/io/ooxml/zip-reject.js +106 -106
- package/dist/measure/placement.js +83 -83
- package/dist/measure/report.js +75 -75
- package/dist/model/ids.js +70 -70
- package/dist/model/normalize.js +146 -146
- package/dist/model/position.js +283 -283
- package/dist/model/transaction.js +209 -209
- package/dist/platform/capabilities.js +25 -25
- package/dist/react/index.js +21 -21
- package/dist/selection/controller.js +315 -315
- package/dist/selection/dom-map.js +334 -334
- package/dist/state/editor-state.js +109 -109
- package/dist/styles.css +66 -66
- package/package.json +94 -93
|
@@ -1,616 +1,616 @@
|
|
|
1
|
-
// Forward, single-pass, byte-offset-preserving XML scanner (ADR-0020 Karar 1).
|
|
2
|
-
//
|
|
3
|
-
// WHY THIS FILE EXISTS: §3.5's provenance model needs, for every anchor, the
|
|
4
|
-
// exact BYTE RANGE it occupies in the untouched word/document.xml — not a
|
|
5
|
-
// character index, not a DOM reference. No browser API gives that:
|
|
6
|
-
// - DOMParser is absent in Node (test/ssr-safety.test.js proves this on
|
|
7
|
-
// every src/ import), and even where present it returns a tree whose
|
|
8
|
-
// nodes carry no source offset at all — it does not solve the problem,
|
|
9
|
-
// it just re-creates option (a)'s flaw (two independent walks that must
|
|
10
|
-
// stay in lockstep) behind a different parser.
|
|
11
|
-
// - XMLSerializer's parse->serialize round-trip is not byte-preserving
|
|
12
|
-
// (entity spelling, attribute quoting, insignificant whitespace) — the
|
|
13
|
-
// exact thing §3.5 forbids for export.
|
|
14
|
-
// So this scanner produces node AND offset from the SAME event, in one pass
|
|
15
|
-
// — B1's bug class (two lists, indexed together, silently drifting apart)
|
|
16
|
-
// is structurally impossible here, not merely tested against.
|
|
17
|
-
//
|
|
18
|
-
// BYTE OFFSETS, NOT CHARACTER INDICES: this scanner walks bytes.Uint8Array,
|
|
19
|
-
// never decoding to a JS string until a leaf value (text/CDATA/attribute) is
|
|
20
|
-
// fully delimited. UTF-8 was designed so that every ASCII structural byte
|
|
21
|
-
// (<, >, /, =, ", ', &, ;, whitespace, all < 0x80) never appears as part of a
|
|
22
|
-
// multi-byte sequence's lead or continuation byte (those are always >= 0x80)
|
|
23
|
-
// — so scanning for structural bytes is correct UTF-8-wide without decoding
|
|
24
|
-
// a single character. Measured 2026-08-27: multi-byte UTF-8 (Turkish, e.g.
|
|
25
|
-
// "tankları" = 8 chars / 9 bytes, AA01.docx offset 7605) is real in this
|
|
26
|
-
// corpus. A scanner built on string indexOf/length would silently misalign
|
|
27
|
-
// on exactly this content — the bug §3.5 calls "sessizce kayar."
|
|
28
|
-
//
|
|
29
|
-
// SCOPE: this ONE module (per this piece's own instruction) covers scanning
|
|
30
|
-
// AND the reject checks ADR-0020 Karar 1/Karar 4 name for this layer —
|
|
31
|
-
// folding what Karar 8's file table called `xml-reject.js` into this file.
|
|
32
|
-
// No model transformation, no mc:AlternateContent interpretation (item 4:
|
|
33
|
-
// this scanner parses Choice AND Fallback identically and does not choose
|
|
34
|
-
// between them — that is drawingml.js's job, a later piece).
|
|
35
|
-
|
|
36
|
-
const LT = 0x3c; // <
|
|
37
|
-
const GT = 0x3e; // >
|
|
38
|
-
const SLASH = 0x2f; // /
|
|
39
|
-
const QUESTION = 0x3f; // ?
|
|
40
|
-
const BANG = 0x21; // !
|
|
41
|
-
const EQUALS = 0x3d; // =
|
|
42
|
-
const DQUOTE = 0x22; // "
|
|
43
|
-
const SQUOTE = 0x27; // '
|
|
44
|
-
const AMP = 0x26; // &
|
|
45
|
-
const SEMI = 0x3b; // ;
|
|
46
|
-
const HASH = 0x23; // #
|
|
47
|
-
const COLON = 0x3a; // :
|
|
48
|
-
const DASH = 0x2d; // -
|
|
49
|
-
const DOT = 0x2e; // .
|
|
50
|
-
const UNDERSCORE = 0x5f; // _
|
|
51
|
-
const SPACE = 0x20;
|
|
52
|
-
const TAB = 0x09;
|
|
53
|
-
const CR = 0x0d;
|
|
54
|
-
const LF = 0x0a;
|
|
55
|
-
|
|
56
|
-
const XML_PREFIX_URI = "http://www.w3.org/XML/1998/namespace"; // xml: is predefined, never declared
|
|
57
|
-
|
|
58
|
-
const PREDEFINED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
59
|
-
|
|
60
|
-
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* @typedef {object} XmlAttribute
|
|
64
|
-
* @property {string} prefix raw prefix as written, "" if unprefixed
|
|
65
|
-
* @property {string} localName
|
|
66
|
-
* @property {string} uri "" for an unprefixed attribute — per the XML
|
|
67
|
-
* Namespaces spec, an unprefixed attribute is NEVER in the default
|
|
68
|
-
* namespace, unlike an unprefixed ELEMENT
|
|
69
|
-
* @property {string} value entity-decoded
|
|
70
|
-
*/
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* @typedef {object} XmlElement
|
|
74
|
-
* @property {"element"} kind
|
|
75
|
-
* @property {string} prefix
|
|
76
|
-
* @property {string} localName
|
|
77
|
-
* @property {string} uri resolved namespace URI
|
|
78
|
-
* @property {number} start offset of this element's opening '<'
|
|
79
|
-
* @property {number} end offset one past the closing '>' (own end
|
|
80
|
-
* tag, or the same self-closing tag's own '>')
|
|
81
|
-
* @property {number} contentStart offset right after the opening tag's '>'
|
|
82
|
-
* (== end for a self-closing element — no content)
|
|
83
|
-
* @property {number} contentEnd offset of the '<' starting the closing tag
|
|
84
|
-
* (== contentStart for a self-closing element)
|
|
85
|
-
* @property {XmlAttribute[]} attributes
|
|
86
|
-
* @property {XmlNode[]} children
|
|
87
|
-
*/
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* @typedef {object} XmlText
|
|
91
|
-
* @property {"text"} kind
|
|
92
|
-
* @property {number} start
|
|
93
|
-
* @property {number} end
|
|
94
|
-
* @property {string} value entity-decoded
|
|
95
|
-
*/
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* @typedef {object} XmlCData
|
|
99
|
-
* @property {"cdata"} kind
|
|
100
|
-
* @property {number} start
|
|
101
|
-
* @property {number} end
|
|
102
|
-
* @property {string} value raw — CDATA content is never entity-decoded
|
|
103
|
-
*/
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* @typedef {object} XmlComment
|
|
107
|
-
* @property {"comment"} kind
|
|
108
|
-
* @property {number} start
|
|
109
|
-
* @property {number} end
|
|
110
|
-
*/
|
|
111
|
-
|
|
112
|
-
/** @typedef {XmlElement | XmlText | XmlCData | XmlComment} XmlNode */
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* @param {XmlElement} element
|
|
116
|
-
* @param {string} uri
|
|
117
|
-
* @param {string} localName
|
|
118
|
-
* @returns {string | undefined}
|
|
119
|
-
*/
|
|
120
|
-
export function getAttr(element, uri, localName) {
|
|
121
|
-
for (const a of element.attributes) {
|
|
122
|
-
if (a.uri === uri && a.localName === localName) return a.value;
|
|
123
|
-
}
|
|
124
|
-
return undefined;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* @param {XmlNode} node
|
|
129
|
-
* @param {string} uri
|
|
130
|
-
* @param {string} localName
|
|
131
|
-
* @returns {boolean}
|
|
132
|
-
*/
|
|
133
|
-
export function isElement(node, uri, localName) {
|
|
134
|
-
return node.kind === "element" && node.uri === uri && node.localName === localName;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* The FIRST direct child matching (uri, localName) — mirrors
|
|
139
|
-
* ElementTree's `.find()` (direct children only, not descendants).
|
|
140
|
-
* @param {XmlElement} element
|
|
141
|
-
* @param {string} uri
|
|
142
|
-
* @param {string} localName
|
|
143
|
-
* @returns {XmlElement | undefined}
|
|
144
|
-
*/
|
|
145
|
-
export function findChild(element, uri, localName) {
|
|
146
|
-
return /** @type {XmlElement | undefined} */ (
|
|
147
|
-
element.children.find((c) => isElement(c, uri, localName))
|
|
148
|
-
);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
* Every direct child matching (uri, localName), in document order — mirrors
|
|
153
|
-
* ElementTree's `.findall()` (direct children only, not descendants).
|
|
154
|
-
* @param {XmlElement} element
|
|
155
|
-
* @param {string} uri
|
|
156
|
-
* @param {string} localName
|
|
157
|
-
* @returns {XmlElement[]}
|
|
158
|
-
*/
|
|
159
|
-
export function findChildren(element, uri, localName) {
|
|
160
|
-
return /** @type {XmlElement[]} */ (element.children.filter((c) => isElement(c, uri, localName)));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* @param {number} b
|
|
165
|
-
* @returns {boolean}
|
|
166
|
-
*/
|
|
167
|
-
function isNameStartByte(b) {
|
|
168
|
-
return (b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a) || b === UNDERSCORE;
|
|
169
|
-
}
|
|
170
|
-
/**
|
|
171
|
-
* @param {number} b
|
|
172
|
-
* @returns {boolean}
|
|
173
|
-
*/
|
|
174
|
-
function isNameByte(b) {
|
|
175
|
-
return isNameStartByte(b) || (b >= 0x30 && b <= 0x39) || b === DASH || b === DOT || b === COLON;
|
|
176
|
-
}
|
|
177
|
-
/**
|
|
178
|
-
* @param {number} b
|
|
179
|
-
* @returns {boolean}
|
|
180
|
-
*/
|
|
181
|
-
function isWhitespaceByte(b) {
|
|
182
|
-
return b === SPACE || b === TAB || b === CR || b === LF;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* @param {Uint8Array} bytes
|
|
187
|
-
* @param {number} offset
|
|
188
|
-
* @param {string} literal ASCII only
|
|
189
|
-
* @returns {boolean}
|
|
190
|
-
*/
|
|
191
|
-
function matchesAscii(bytes, offset, literal) {
|
|
192
|
-
if (offset + literal.length > bytes.length) return false;
|
|
193
|
-
for (let i = 0; i < literal.length; i++) {
|
|
194
|
-
if (bytes[offset + i] !== literal.charCodeAt(i)) return false;
|
|
195
|
-
}
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Decodes XML character escapes in an already UTF-8-decoded string. Manual
|
|
201
|
-
* single pass (not replace-then-recheck): a bare, unescaped '&' is a
|
|
202
|
-
* well-formedness violation and must be REJECTED, but a numeric reference
|
|
203
|
-
* that legitimately decodes TO '&' (e.g. `&`) must not be mistaken for
|
|
204
|
-
* one — a replace-then-scan-for-leftover-& approach cannot tell those apart.
|
|
205
|
-
* @param {string} text
|
|
206
|
-
* @param {number} baseOffset byte offset of the start of `text`, for error messages
|
|
207
|
-
* @returns {string}
|
|
208
|
-
*/
|
|
209
|
-
function decodeEntities(text, baseOffset) {
|
|
210
|
-
if (text.indexOf("&") < 0) return text;
|
|
211
|
-
let out = "";
|
|
212
|
-
let i = 0;
|
|
213
|
-
while (i < text.length) {
|
|
214
|
-
const ch = text[i];
|
|
215
|
-
if (ch !== "&") {
|
|
216
|
-
out += ch;
|
|
217
|
-
i++;
|
|
218
|
-
continue;
|
|
219
|
-
}
|
|
220
|
-
const semi = text.indexOf(";", i + 1);
|
|
221
|
-
const body = semi >= 0 ? text.slice(i + 1, semi) : "";
|
|
222
|
-
if (semi >= 0 && Object.prototype.hasOwnProperty.call(PREDEFINED_ENTITIES, body)) {
|
|
223
|
-
out += PREDEFINED_ENTITIES[/** @type {keyof typeof PREDEFINED_ENTITIES} */ (body)];
|
|
224
|
-
i = semi + 1;
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
if (semi >= 0 && /^#[0-9]+$/.test(body)) {
|
|
228
|
-
out += String.fromCodePoint(Number.parseInt(body.slice(1), 10));
|
|
229
|
-
i = semi + 1;
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
if (semi >= 0 && /^#x[0-9a-fA-F]+$/.test(body)) {
|
|
233
|
-
out += String.fromCodePoint(Number.parseInt(body.slice(2), 16));
|
|
234
|
-
i = semi + 1;
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
throw new Error(
|
|
238
|
-
`xml-scan: hatalı biçimli XML — kaçışsız veya tanınmayan '&' (yaklaşık bayt ${baseOffset}) — ` +
|
|
239
|
-
"geçerli kaçışlar: & < > " ' ve sayısal (&#NNN; / &#xHHH;)",
|
|
240
|
-
);
|
|
241
|
-
}
|
|
242
|
-
return out;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* @param {string | null} declaredEncoding the XML declaration's own encoding attribute, or null if absent
|
|
247
|
-
* @returns {void}
|
|
248
|
-
*/
|
|
249
|
-
export function rejectNonUtf8Encoding(declaredEncoding) {
|
|
250
|
-
if (declaredEncoding === null) return;
|
|
251
|
-
const normalized = declaredEncoding.toLowerCase();
|
|
252
|
-
if (normalized !== "utf-8" && normalized !== "utf8") {
|
|
253
|
-
throw new Error(
|
|
254
|
-
`xml-scan: UTF-8 dışı kodlama bildirimi (${JSON.stringify(declaredEncoding)}) — reddediliyor, ` +
|
|
255
|
-
"bu bayt dizisi UTF-8 olarak yorumlanacak ve başka bir kodlama tahmin edilmeyecek",
|
|
256
|
-
);
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* @param {Uint8Array} bytes
|
|
262
|
-
* @param {number} offset where the offending '<!' sequence starts
|
|
263
|
-
* @returns {never}
|
|
264
|
-
*/
|
|
265
|
-
export function rejectDoctype(bytes, offset) {
|
|
266
|
-
throw new Error(
|
|
267
|
-
`xml-scan: DOCTYPE/ENTITY bildirimi bulundu (bayt ${offset}) — dış varlık (XXE) riski, reddediliyor`,
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* @param {string} prefix
|
|
273
|
-
* @param {number} offset
|
|
274
|
-
* @returns {never}
|
|
275
|
-
*/
|
|
276
|
-
function rejectUnresolvedPrefix(prefix, offset) {
|
|
277
|
-
throw new Error(`xml-scan: çözülemeyen ad alanı öneki ${JSON.stringify(prefix + ":")} (bayt ${offset})`);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
/**
|
|
281
|
-
* Scans a decompressed word/document.xml byte buffer into a single-pass,
|
|
282
|
-
* offset-carrying tree. Pure, synchronous, no platform globals.
|
|
283
|
-
* @param {Uint8Array} bytes
|
|
284
|
-
* @returns {{ root: XmlElement, declaredEncoding: string | null }}
|
|
285
|
-
*/
|
|
286
|
-
export function scanXml(bytes) {
|
|
287
|
-
const len = bytes.length;
|
|
288
|
-
let pos = 0;
|
|
289
|
-
/** @type {Map<string, string>[]} */
|
|
290
|
-
const nsScopes = [new Map([["xml", XML_PREFIX_URI]])];
|
|
291
|
-
let declaredEncoding = null;
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* @param {string} message
|
|
295
|
-
* @returns {never}
|
|
296
|
-
*/
|
|
297
|
-
function fail(message) {
|
|
298
|
-
throw new Error(`xml-scan: hatalı biçimli XML — ${message} (bayt ${pos})`);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
function skipWs() {
|
|
302
|
-
while (pos < len && isWhitespaceByte(bytes[pos])) pos++;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function readName() {
|
|
306
|
-
const start = pos;
|
|
307
|
-
if (pos >= len || !isNameStartByte(bytes[pos])) fail("beklenen isim bulunamadı");
|
|
308
|
-
pos++;
|
|
309
|
-
while (pos < len && isNameByte(bytes[pos])) pos++;
|
|
310
|
-
return decoder.decode(bytes.subarray(start, pos));
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
/**
|
|
314
|
-
* @param {string} qname
|
|
315
|
-
* @returns {{prefix: string, localName: string}}
|
|
316
|
-
*/
|
|
317
|
-
function splitQName(qname) {
|
|
318
|
-
const idx = qname.indexOf(":");
|
|
319
|
-
return idx < 0 ? { prefix: "", localName: qname } : { prefix: qname.slice(0, idx), localName: qname.slice(idx + 1) };
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* @param {string} prefix
|
|
324
|
-
* @returns {string}
|
|
325
|
-
*/
|
|
326
|
-
function resolveElementUri(prefix) {
|
|
327
|
-
if (prefix === "") {
|
|
328
|
-
for (let i = nsScopes.length - 1; i >= 0; i--) {
|
|
329
|
-
const v = nsScopes[i].get("");
|
|
330
|
-
if (v !== undefined) return v;
|
|
331
|
-
}
|
|
332
|
-
return ""; // no default namespace declared — legal, means "no namespace"
|
|
333
|
-
}
|
|
334
|
-
for (let i = nsScopes.length - 1; i >= 0; i--) {
|
|
335
|
-
const v = nsScopes[i].get(prefix);
|
|
336
|
-
if (v !== undefined) return v;
|
|
337
|
-
}
|
|
338
|
-
return rejectUnresolvedPrefix(prefix, pos);
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* @param {string} prefix
|
|
343
|
-
* @returns {string}
|
|
344
|
-
*/
|
|
345
|
-
function resolveAttrUri(prefix) {
|
|
346
|
-
if (prefix === "") return ""; // unprefixed attribute: NEVER in a default namespace
|
|
347
|
-
for (let i = nsScopes.length - 1; i >= 0; i--) {
|
|
348
|
-
const v = nsScopes[i].get(prefix);
|
|
349
|
-
if (v !== undefined) return v;
|
|
350
|
-
}
|
|
351
|
-
return rejectUnresolvedPrefix(prefix, pos);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function skipComment() {
|
|
355
|
-
const start = pos;
|
|
356
|
-
pos += 4; // "<!--"
|
|
357
|
-
const end = indexOfAscii(bytes, "-->", pos);
|
|
358
|
-
if (end < 0) fail("yorum kapanmıyor (-->)");
|
|
359
|
-
pos = end + 3;
|
|
360
|
-
/** @type {XmlComment} */
|
|
361
|
-
const node = { kind: "comment", start, end: pos };
|
|
362
|
-
return node;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function readCData() {
|
|
366
|
-
const start = pos;
|
|
367
|
-
pos += 9; // "<![CDATA["
|
|
368
|
-
const end = indexOfAscii(bytes, "]]>", pos);
|
|
369
|
-
if (end < 0) fail("CDATA kapanmıyor (]]>)");
|
|
370
|
-
const value = decoder.decode(bytes.subarray(pos, end));
|
|
371
|
-
pos = end + 3;
|
|
372
|
-
/** @type {XmlCData} */
|
|
373
|
-
const node = { kind: "cdata", start, end: pos, value };
|
|
374
|
-
return node;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function skipProcessingInstruction() {
|
|
378
|
-
pos += 2; // "<?"
|
|
379
|
-
const end = indexOfAscii(bytes, "?>", pos);
|
|
380
|
-
if (end < 0) fail("işleme talimatı kapanmıyor (?>)");
|
|
381
|
-
pos = end + 2;
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
function readAttributeValue() {
|
|
385
|
-
const quote = bytes[pos];
|
|
386
|
-
if (quote !== DQUOTE && quote !== SQUOTE) fail("öznitelik değeri tırnaksız");
|
|
387
|
-
pos++;
|
|
388
|
-
const start = pos;
|
|
389
|
-
while (pos < len && bytes[pos] !== quote) {
|
|
390
|
-
if (bytes[pos] === LT) fail("öznitelik değeri içinde kaçışsız '<'");
|
|
391
|
-
pos++;
|
|
392
|
-
}
|
|
393
|
-
if (pos >= len) fail("öznitelik değeri kapanmıyor");
|
|
394
|
-
const raw = decoder.decode(bytes.subarray(start, pos));
|
|
395
|
-
pos++; // closing quote
|
|
396
|
-
return decodeEntities(raw, start);
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
function readTagAttributes() {
|
|
400
|
-
/** @type {{qname: string, value: string, offset: number}[]} */
|
|
401
|
-
const raw = [];
|
|
402
|
-
for (;;) {
|
|
403
|
-
const before = pos;
|
|
404
|
-
skipWs();
|
|
405
|
-
if (pos >= len) fail("etiket kapanmıyor");
|
|
406
|
-
if (bytes[pos] === SLASH || bytes[pos] === GT) break;
|
|
407
|
-
if (pos === before) fail("öznitelikler arasında boşluk bekleniyor");
|
|
408
|
-
const attrOffset = pos;
|
|
409
|
-
const qname = readName();
|
|
410
|
-
skipWs();
|
|
411
|
-
if (bytes[pos] !== EQUALS) fail("öznitelikte '=' bekleniyor");
|
|
412
|
-
pos++;
|
|
413
|
-
skipWs();
|
|
414
|
-
const value = readAttributeValue();
|
|
415
|
-
raw.push({ qname, value, offset: attrOffset });
|
|
416
|
-
}
|
|
417
|
-
return raw;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function parseElement() {
|
|
421
|
-
const start = pos;
|
|
422
|
-
pos++; // '<'
|
|
423
|
-
const qname = readName();
|
|
424
|
-
const { prefix, localName } = splitQName(qname);
|
|
425
|
-
const rawAttrs = readTagAttributes();
|
|
426
|
-
|
|
427
|
-
// New namespace scope: collect this element's own xmlns declarations
|
|
428
|
-
// before resolving ANYTHING (including the element's own prefix) — a
|
|
429
|
-
// tag may declare the very namespace its own prefix uses.
|
|
430
|
-
/** @type {Map<string, string>} */
|
|
431
|
-
const scope = new Map();
|
|
432
|
-
for (const { qname: aqname, value } of rawAttrs) {
|
|
433
|
-
if (aqname === "xmlns") scope.set("", value);
|
|
434
|
-
else if (aqname.startsWith("xmlns:")) scope.set(aqname.slice(6), value);
|
|
435
|
-
}
|
|
436
|
-
nsScopes.push(scope);
|
|
437
|
-
|
|
438
|
-
const uri = resolveElementUri(prefix);
|
|
439
|
-
/** @type {XmlAttribute[]} */
|
|
440
|
-
const attributes = [];
|
|
441
|
-
for (const { qname: aqname, value, offset } of rawAttrs) {
|
|
442
|
-
if (aqname === "xmlns" || aqname.startsWith("xmlns:")) continue; // declarations aren't data attributes
|
|
443
|
-
const split = splitQName(aqname);
|
|
444
|
-
const savedPos = pos;
|
|
445
|
-
pos = offset;
|
|
446
|
-
const attrUri = resolveAttrUri(split.prefix);
|
|
447
|
-
pos = savedPos;
|
|
448
|
-
attributes.push({ prefix: split.prefix, localName: split.localName, uri: attrUri, value });
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
skipWs();
|
|
452
|
-
const selfClosing = bytes[pos] === SLASH;
|
|
453
|
-
if (selfClosing) pos++;
|
|
454
|
-
if (bytes[pos] !== GT) fail("etiket '>' ile kapanmıyor");
|
|
455
|
-
pos++;
|
|
456
|
-
|
|
457
|
-
/** @type {XmlElement} */
|
|
458
|
-
const element = {
|
|
459
|
-
kind: "element",
|
|
460
|
-
prefix,
|
|
461
|
-
localName,
|
|
462
|
-
uri,
|
|
463
|
-
start,
|
|
464
|
-
end: pos,
|
|
465
|
-
contentStart: pos,
|
|
466
|
-
contentEnd: pos,
|
|
467
|
-
attributes,
|
|
468
|
-
children: [],
|
|
469
|
-
};
|
|
470
|
-
|
|
471
|
-
if (!selfClosing) {
|
|
472
|
-
element.children = parseContent(qname);
|
|
473
|
-
element.contentEnd = pos; // set by parseContent just before the closing tag
|
|
474
|
-
pos = skipClosingTag(qname);
|
|
475
|
-
element.end = pos;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
nsScopes.pop();
|
|
479
|
-
return element;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/**
|
|
483
|
-
* @param {string} expectedQName
|
|
484
|
-
* @returns {number}
|
|
485
|
-
*/
|
|
486
|
-
function skipClosingTag(expectedQName) {
|
|
487
|
-
const closeStart = pos;
|
|
488
|
-
pos += 2; // "</"
|
|
489
|
-
const qname = readName();
|
|
490
|
-
if (qname !== expectedQName) {
|
|
491
|
-
fail(`kapanış etiketi <${expectedQName}> ile eşleşmiyor, bulundu </${qname}> (bayt ${closeStart})`);
|
|
492
|
-
}
|
|
493
|
-
skipWs();
|
|
494
|
-
if (bytes[pos] !== GT) fail("kapanış etiketi '>' ile bitmiyor");
|
|
495
|
-
pos++;
|
|
496
|
-
return pos;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function readText() {
|
|
500
|
-
const start = pos;
|
|
501
|
-
while (pos < len && bytes[pos] !== LT) pos++;
|
|
502
|
-
const raw = decoder.decode(bytes.subarray(start, pos));
|
|
503
|
-
const value = decodeEntities(raw, start);
|
|
504
|
-
/** @type {XmlText} */
|
|
505
|
-
const node = { kind: "text", start, end: pos, value };
|
|
506
|
-
return node;
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* @param {string} parentQName
|
|
511
|
-
* @returns {XmlNode[]}
|
|
512
|
-
*/
|
|
513
|
-
function parseContent(parentQName) {
|
|
514
|
-
/** @type {XmlNode[]} */
|
|
515
|
-
const children = [];
|
|
516
|
-
for (;;) {
|
|
517
|
-
if (pos >= len) fail(`<${parentQName}> hiç kapanmadan girdi bitti`);
|
|
518
|
-
if (bytes[pos] !== LT) {
|
|
519
|
-
children.push(readText());
|
|
520
|
-
continue;
|
|
521
|
-
}
|
|
522
|
-
if (bytes[pos + 1] === SLASH) {
|
|
523
|
-
break; // closing tag for the current element — let the caller consume it
|
|
524
|
-
}
|
|
525
|
-
if (bytes[pos + 1] === BANG) {
|
|
526
|
-
if (matchesAscii(bytes, pos, "<!--")) {
|
|
527
|
-
children.push(skipComment());
|
|
528
|
-
continue;
|
|
529
|
-
}
|
|
530
|
-
if (matchesAscii(bytes, pos, "<![CDATA[")) {
|
|
531
|
-
children.push(readCData());
|
|
532
|
-
continue;
|
|
533
|
-
}
|
|
534
|
-
if (matchesAscii(bytes, pos, "<!DOCTYPE")) rejectDoctype(bytes, pos);
|
|
535
|
-
fail("bilinmeyen '<!' yapısı");
|
|
536
|
-
}
|
|
537
|
-
if (bytes[pos + 1] === QUESTION) {
|
|
538
|
-
skipProcessingInstruction();
|
|
539
|
-
continue;
|
|
540
|
-
}
|
|
541
|
-
children.push(parseElement());
|
|
542
|
-
}
|
|
543
|
-
return children;
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// ---- XML declaration (optional, must be first if present) ----
|
|
547
|
-
if (matchesAscii(bytes, 0, "<?xml") && isWhitespaceByte(bytes[5])) {
|
|
548
|
-
const declEnd = indexOfAscii(bytes, "?>", 5);
|
|
549
|
-
if (declEnd < 0) fail("XML bildirimi kapanmıyor (?>)");
|
|
550
|
-
const declText = decoder.decode(bytes.subarray(5, declEnd));
|
|
551
|
-
const encMatch = /encoding\s*=\s*["']([^"']+)["']/.exec(declText);
|
|
552
|
-
if (encMatch) declaredEncoding = encMatch[1];
|
|
553
|
-
pos = declEnd + 2;
|
|
554
|
-
}
|
|
555
|
-
rejectNonUtf8Encoding(declaredEncoding);
|
|
556
|
-
|
|
557
|
-
// ---- misc before the root element ----
|
|
558
|
-
/** @type {XmlElement | null} */
|
|
559
|
-
let root = null;
|
|
560
|
-
while (pos < len) {
|
|
561
|
-
skipWs();
|
|
562
|
-
if (pos >= len) break;
|
|
563
|
-
if (bytes[pos] !== LT) fail("kök elementten önce beklenmeyen içerik");
|
|
564
|
-
if (bytes[pos + 1] === BANG) {
|
|
565
|
-
if (matchesAscii(bytes, pos, "<!--")) {
|
|
566
|
-
skipComment();
|
|
567
|
-
continue;
|
|
568
|
-
}
|
|
569
|
-
if (matchesAscii(bytes, pos, "<!DOCTYPE")) rejectDoctype(bytes, pos);
|
|
570
|
-
fail("bilinmeyen '<!' yapısı");
|
|
571
|
-
}
|
|
572
|
-
if (bytes[pos + 1] === QUESTION) {
|
|
573
|
-
skipProcessingInstruction();
|
|
574
|
-
continue;
|
|
575
|
-
}
|
|
576
|
-
root = parseElement();
|
|
577
|
-
break;
|
|
578
|
-
}
|
|
579
|
-
if (root === null) fail("kök element bulunamadı");
|
|
580
|
-
|
|
581
|
-
// ---- misc after the root element (comments/PIs/whitespace only) ----
|
|
582
|
-
while (pos < len) {
|
|
583
|
-
skipWs();
|
|
584
|
-
if (pos >= len) break;
|
|
585
|
-
if (bytes[pos] !== LT) fail("kök elementten sonra beklenmeyen içerik");
|
|
586
|
-
if (matchesAscii(bytes, pos, "<!--")) {
|
|
587
|
-
skipComment();
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
if (bytes[pos + 1] === QUESTION) {
|
|
591
|
-
skipProcessingInstruction();
|
|
592
|
-
continue;
|
|
593
|
-
}
|
|
594
|
-
fail("kök elementten sonra ikinci bir element");
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
return { root, declaredEncoding };
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
/**
|
|
601
|
-
* @param {Uint8Array} bytes
|
|
602
|
-
* @param {string} literal ASCII only
|
|
603
|
-
* @param {number} from
|
|
604
|
-
* @returns {number}
|
|
605
|
-
*/
|
|
606
|
-
function indexOfAscii(bytes, literal, from) {
|
|
607
|
-
const first = literal.charCodeAt(0);
|
|
608
|
-
outer: for (let i = from; i <= bytes.length - literal.length; i++) {
|
|
609
|
-
if (bytes[i] !== first) continue;
|
|
610
|
-
for (let j = 1; j < literal.length; j++) {
|
|
611
|
-
if (bytes[i + j] !== literal.charCodeAt(j)) continue outer;
|
|
612
|
-
}
|
|
613
|
-
return i;
|
|
614
|
-
}
|
|
615
|
-
return -1;
|
|
616
|
-
}
|
|
1
|
+
// Forward, single-pass, byte-offset-preserving XML scanner (ADR-0020 Karar 1).
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS FILE EXISTS: §3.5's provenance model needs, for every anchor, the
|
|
4
|
+
// exact BYTE RANGE it occupies in the untouched word/document.xml — not a
|
|
5
|
+
// character index, not a DOM reference. No browser API gives that:
|
|
6
|
+
// - DOMParser is absent in Node (test/ssr-safety.test.js proves this on
|
|
7
|
+
// every src/ import), and even where present it returns a tree whose
|
|
8
|
+
// nodes carry no source offset at all — it does not solve the problem,
|
|
9
|
+
// it just re-creates option (a)'s flaw (two independent walks that must
|
|
10
|
+
// stay in lockstep) behind a different parser.
|
|
11
|
+
// - XMLSerializer's parse->serialize round-trip is not byte-preserving
|
|
12
|
+
// (entity spelling, attribute quoting, insignificant whitespace) — the
|
|
13
|
+
// exact thing §3.5 forbids for export.
|
|
14
|
+
// So this scanner produces node AND offset from the SAME event, in one pass
|
|
15
|
+
// — B1's bug class (two lists, indexed together, silently drifting apart)
|
|
16
|
+
// is structurally impossible here, not merely tested against.
|
|
17
|
+
//
|
|
18
|
+
// BYTE OFFSETS, NOT CHARACTER INDICES: this scanner walks bytes.Uint8Array,
|
|
19
|
+
// never decoding to a JS string until a leaf value (text/CDATA/attribute) is
|
|
20
|
+
// fully delimited. UTF-8 was designed so that every ASCII structural byte
|
|
21
|
+
// (<, >, /, =, ", ', &, ;, whitespace, all < 0x80) never appears as part of a
|
|
22
|
+
// multi-byte sequence's lead or continuation byte (those are always >= 0x80)
|
|
23
|
+
// — so scanning for structural bytes is correct UTF-8-wide without decoding
|
|
24
|
+
// a single character. Measured 2026-08-27: multi-byte UTF-8 (Turkish, e.g.
|
|
25
|
+
// "tankları" = 8 chars / 9 bytes, AA01.docx offset 7605) is real in this
|
|
26
|
+
// corpus. A scanner built on string indexOf/length would silently misalign
|
|
27
|
+
// on exactly this content — the bug §3.5 calls "sessizce kayar."
|
|
28
|
+
//
|
|
29
|
+
// SCOPE: this ONE module (per this piece's own instruction) covers scanning
|
|
30
|
+
// AND the reject checks ADR-0020 Karar 1/Karar 4 name for this layer —
|
|
31
|
+
// folding what Karar 8's file table called `xml-reject.js` into this file.
|
|
32
|
+
// No model transformation, no mc:AlternateContent interpretation (item 4:
|
|
33
|
+
// this scanner parses Choice AND Fallback identically and does not choose
|
|
34
|
+
// between them — that is drawingml.js's job, a later piece).
|
|
35
|
+
|
|
36
|
+
const LT = 0x3c; // <
|
|
37
|
+
const GT = 0x3e; // >
|
|
38
|
+
const SLASH = 0x2f; // /
|
|
39
|
+
const QUESTION = 0x3f; // ?
|
|
40
|
+
const BANG = 0x21; // !
|
|
41
|
+
const EQUALS = 0x3d; // =
|
|
42
|
+
const DQUOTE = 0x22; // "
|
|
43
|
+
const SQUOTE = 0x27; // '
|
|
44
|
+
const AMP = 0x26; // &
|
|
45
|
+
const SEMI = 0x3b; // ;
|
|
46
|
+
const HASH = 0x23; // #
|
|
47
|
+
const COLON = 0x3a; // :
|
|
48
|
+
const DASH = 0x2d; // -
|
|
49
|
+
const DOT = 0x2e; // .
|
|
50
|
+
const UNDERSCORE = 0x5f; // _
|
|
51
|
+
const SPACE = 0x20;
|
|
52
|
+
const TAB = 0x09;
|
|
53
|
+
const CR = 0x0d;
|
|
54
|
+
const LF = 0x0a;
|
|
55
|
+
|
|
56
|
+
const XML_PREFIX_URI = "http://www.w3.org/XML/1998/namespace"; // xml: is predefined, never declared
|
|
57
|
+
|
|
58
|
+
const PREDEFINED_ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
59
|
+
|
|
60
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {object} XmlAttribute
|
|
64
|
+
* @property {string} prefix raw prefix as written, "" if unprefixed
|
|
65
|
+
* @property {string} localName
|
|
66
|
+
* @property {string} uri "" for an unprefixed attribute — per the XML
|
|
67
|
+
* Namespaces spec, an unprefixed attribute is NEVER in the default
|
|
68
|
+
* namespace, unlike an unprefixed ELEMENT
|
|
69
|
+
* @property {string} value entity-decoded
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* @typedef {object} XmlElement
|
|
74
|
+
* @property {"element"} kind
|
|
75
|
+
* @property {string} prefix
|
|
76
|
+
* @property {string} localName
|
|
77
|
+
* @property {string} uri resolved namespace URI
|
|
78
|
+
* @property {number} start offset of this element's opening '<'
|
|
79
|
+
* @property {number} end offset one past the closing '>' (own end
|
|
80
|
+
* tag, or the same self-closing tag's own '>')
|
|
81
|
+
* @property {number} contentStart offset right after the opening tag's '>'
|
|
82
|
+
* (== end for a self-closing element — no content)
|
|
83
|
+
* @property {number} contentEnd offset of the '<' starting the closing tag
|
|
84
|
+
* (== contentStart for a self-closing element)
|
|
85
|
+
* @property {XmlAttribute[]} attributes
|
|
86
|
+
* @property {XmlNode[]} children
|
|
87
|
+
*/
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* @typedef {object} XmlText
|
|
91
|
+
* @property {"text"} kind
|
|
92
|
+
* @property {number} start
|
|
93
|
+
* @property {number} end
|
|
94
|
+
* @property {string} value entity-decoded
|
|
95
|
+
*/
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @typedef {object} XmlCData
|
|
99
|
+
* @property {"cdata"} kind
|
|
100
|
+
* @property {number} start
|
|
101
|
+
* @property {number} end
|
|
102
|
+
* @property {string} value raw — CDATA content is never entity-decoded
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @typedef {object} XmlComment
|
|
107
|
+
* @property {"comment"} kind
|
|
108
|
+
* @property {number} start
|
|
109
|
+
* @property {number} end
|
|
110
|
+
*/
|
|
111
|
+
|
|
112
|
+
/** @typedef {XmlElement | XmlText | XmlCData | XmlComment} XmlNode */
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {XmlElement} element
|
|
116
|
+
* @param {string} uri
|
|
117
|
+
* @param {string} localName
|
|
118
|
+
* @returns {string | undefined}
|
|
119
|
+
*/
|
|
120
|
+
export function getAttr(element, uri, localName) {
|
|
121
|
+
for (const a of element.attributes) {
|
|
122
|
+
if (a.uri === uri && a.localName === localName) return a.value;
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {XmlNode} node
|
|
129
|
+
* @param {string} uri
|
|
130
|
+
* @param {string} localName
|
|
131
|
+
* @returns {boolean}
|
|
132
|
+
*/
|
|
133
|
+
export function isElement(node, uri, localName) {
|
|
134
|
+
return node.kind === "element" && node.uri === uri && node.localName === localName;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The FIRST direct child matching (uri, localName) — mirrors
|
|
139
|
+
* ElementTree's `.find()` (direct children only, not descendants).
|
|
140
|
+
* @param {XmlElement} element
|
|
141
|
+
* @param {string} uri
|
|
142
|
+
* @param {string} localName
|
|
143
|
+
* @returns {XmlElement | undefined}
|
|
144
|
+
*/
|
|
145
|
+
export function findChild(element, uri, localName) {
|
|
146
|
+
return /** @type {XmlElement | undefined} */ (
|
|
147
|
+
element.children.find((c) => isElement(c, uri, localName))
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Every direct child matching (uri, localName), in document order — mirrors
|
|
153
|
+
* ElementTree's `.findall()` (direct children only, not descendants).
|
|
154
|
+
* @param {XmlElement} element
|
|
155
|
+
* @param {string} uri
|
|
156
|
+
* @param {string} localName
|
|
157
|
+
* @returns {XmlElement[]}
|
|
158
|
+
*/
|
|
159
|
+
export function findChildren(element, uri, localName) {
|
|
160
|
+
return /** @type {XmlElement[]} */ (element.children.filter((c) => isElement(c, uri, localName)));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {number} b
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
function isNameStartByte(b) {
|
|
168
|
+
return (b >= 0x41 && b <= 0x5a) || (b >= 0x61 && b <= 0x7a) || b === UNDERSCORE;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* @param {number} b
|
|
172
|
+
* @returns {boolean}
|
|
173
|
+
*/
|
|
174
|
+
function isNameByte(b) {
|
|
175
|
+
return isNameStartByte(b) || (b >= 0x30 && b <= 0x39) || b === DASH || b === DOT || b === COLON;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* @param {number} b
|
|
179
|
+
* @returns {boolean}
|
|
180
|
+
*/
|
|
181
|
+
function isWhitespaceByte(b) {
|
|
182
|
+
return b === SPACE || b === TAB || b === CR || b === LF;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* @param {Uint8Array} bytes
|
|
187
|
+
* @param {number} offset
|
|
188
|
+
* @param {string} literal ASCII only
|
|
189
|
+
* @returns {boolean}
|
|
190
|
+
*/
|
|
191
|
+
function matchesAscii(bytes, offset, literal) {
|
|
192
|
+
if (offset + literal.length > bytes.length) return false;
|
|
193
|
+
for (let i = 0; i < literal.length; i++) {
|
|
194
|
+
if (bytes[offset + i] !== literal.charCodeAt(i)) return false;
|
|
195
|
+
}
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Decodes XML character escapes in an already UTF-8-decoded string. Manual
|
|
201
|
+
* single pass (not replace-then-recheck): a bare, unescaped '&' is a
|
|
202
|
+
* well-formedness violation and must be REJECTED, but a numeric reference
|
|
203
|
+
* that legitimately decodes TO '&' (e.g. `&`) must not be mistaken for
|
|
204
|
+
* one — a replace-then-scan-for-leftover-& approach cannot tell those apart.
|
|
205
|
+
* @param {string} text
|
|
206
|
+
* @param {number} baseOffset byte offset of the start of `text`, for error messages
|
|
207
|
+
* @returns {string}
|
|
208
|
+
*/
|
|
209
|
+
function decodeEntities(text, baseOffset) {
|
|
210
|
+
if (text.indexOf("&") < 0) return text;
|
|
211
|
+
let out = "";
|
|
212
|
+
let i = 0;
|
|
213
|
+
while (i < text.length) {
|
|
214
|
+
const ch = text[i];
|
|
215
|
+
if (ch !== "&") {
|
|
216
|
+
out += ch;
|
|
217
|
+
i++;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const semi = text.indexOf(";", i + 1);
|
|
221
|
+
const body = semi >= 0 ? text.slice(i + 1, semi) : "";
|
|
222
|
+
if (semi >= 0 && Object.prototype.hasOwnProperty.call(PREDEFINED_ENTITIES, body)) {
|
|
223
|
+
out += PREDEFINED_ENTITIES[/** @type {keyof typeof PREDEFINED_ENTITIES} */ (body)];
|
|
224
|
+
i = semi + 1;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
if (semi >= 0 && /^#[0-9]+$/.test(body)) {
|
|
228
|
+
out += String.fromCodePoint(Number.parseInt(body.slice(1), 10));
|
|
229
|
+
i = semi + 1;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (semi >= 0 && /^#x[0-9a-fA-F]+$/.test(body)) {
|
|
233
|
+
out += String.fromCodePoint(Number.parseInt(body.slice(2), 16));
|
|
234
|
+
i = semi + 1;
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
throw new Error(
|
|
238
|
+
`xml-scan: hatalı biçimli XML — kaçışsız veya tanınmayan '&' (yaklaşık bayt ${baseOffset}) — ` +
|
|
239
|
+
"geçerli kaçışlar: & < > " ' ve sayısal (&#NNN; / &#xHHH;)",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* @param {string | null} declaredEncoding the XML declaration's own encoding attribute, or null if absent
|
|
247
|
+
* @returns {void}
|
|
248
|
+
*/
|
|
249
|
+
export function rejectNonUtf8Encoding(declaredEncoding) {
|
|
250
|
+
if (declaredEncoding === null) return;
|
|
251
|
+
const normalized = declaredEncoding.toLowerCase();
|
|
252
|
+
if (normalized !== "utf-8" && normalized !== "utf8") {
|
|
253
|
+
throw new Error(
|
|
254
|
+
`xml-scan: UTF-8 dışı kodlama bildirimi (${JSON.stringify(declaredEncoding)}) — reddediliyor, ` +
|
|
255
|
+
"bu bayt dizisi UTF-8 olarak yorumlanacak ve başka bir kodlama tahmin edilmeyecek",
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @param {Uint8Array} bytes
|
|
262
|
+
* @param {number} offset where the offending '<!' sequence starts
|
|
263
|
+
* @returns {never}
|
|
264
|
+
*/
|
|
265
|
+
export function rejectDoctype(bytes, offset) {
|
|
266
|
+
throw new Error(
|
|
267
|
+
`xml-scan: DOCTYPE/ENTITY bildirimi bulundu (bayt ${offset}) — dış varlık (XXE) riski, reddediliyor`,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* @param {string} prefix
|
|
273
|
+
* @param {number} offset
|
|
274
|
+
* @returns {never}
|
|
275
|
+
*/
|
|
276
|
+
function rejectUnresolvedPrefix(prefix, offset) {
|
|
277
|
+
throw new Error(`xml-scan: çözülemeyen ad alanı öneki ${JSON.stringify(prefix + ":")} (bayt ${offset})`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Scans a decompressed word/document.xml byte buffer into a single-pass,
|
|
282
|
+
* offset-carrying tree. Pure, synchronous, no platform globals.
|
|
283
|
+
* @param {Uint8Array} bytes
|
|
284
|
+
* @returns {{ root: XmlElement, declaredEncoding: string | null }}
|
|
285
|
+
*/
|
|
286
|
+
export function scanXml(bytes) {
|
|
287
|
+
const len = bytes.length;
|
|
288
|
+
let pos = 0;
|
|
289
|
+
/** @type {Map<string, string>[]} */
|
|
290
|
+
const nsScopes = [new Map([["xml", XML_PREFIX_URI]])];
|
|
291
|
+
let declaredEncoding = null;
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* @param {string} message
|
|
295
|
+
* @returns {never}
|
|
296
|
+
*/
|
|
297
|
+
function fail(message) {
|
|
298
|
+
throw new Error(`xml-scan: hatalı biçimli XML — ${message} (bayt ${pos})`);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function skipWs() {
|
|
302
|
+
while (pos < len && isWhitespaceByte(bytes[pos])) pos++;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function readName() {
|
|
306
|
+
const start = pos;
|
|
307
|
+
if (pos >= len || !isNameStartByte(bytes[pos])) fail("beklenen isim bulunamadı");
|
|
308
|
+
pos++;
|
|
309
|
+
while (pos < len && isNameByte(bytes[pos])) pos++;
|
|
310
|
+
return decoder.decode(bytes.subarray(start, pos));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @param {string} qname
|
|
315
|
+
* @returns {{prefix: string, localName: string}}
|
|
316
|
+
*/
|
|
317
|
+
function splitQName(qname) {
|
|
318
|
+
const idx = qname.indexOf(":");
|
|
319
|
+
return idx < 0 ? { prefix: "", localName: qname } : { prefix: qname.slice(0, idx), localName: qname.slice(idx + 1) };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* @param {string} prefix
|
|
324
|
+
* @returns {string}
|
|
325
|
+
*/
|
|
326
|
+
function resolveElementUri(prefix) {
|
|
327
|
+
if (prefix === "") {
|
|
328
|
+
for (let i = nsScopes.length - 1; i >= 0; i--) {
|
|
329
|
+
const v = nsScopes[i].get("");
|
|
330
|
+
if (v !== undefined) return v;
|
|
331
|
+
}
|
|
332
|
+
return ""; // no default namespace declared — legal, means "no namespace"
|
|
333
|
+
}
|
|
334
|
+
for (let i = nsScopes.length - 1; i >= 0; i--) {
|
|
335
|
+
const v = nsScopes[i].get(prefix);
|
|
336
|
+
if (v !== undefined) return v;
|
|
337
|
+
}
|
|
338
|
+
return rejectUnresolvedPrefix(prefix, pos);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* @param {string} prefix
|
|
343
|
+
* @returns {string}
|
|
344
|
+
*/
|
|
345
|
+
function resolveAttrUri(prefix) {
|
|
346
|
+
if (prefix === "") return ""; // unprefixed attribute: NEVER in a default namespace
|
|
347
|
+
for (let i = nsScopes.length - 1; i >= 0; i--) {
|
|
348
|
+
const v = nsScopes[i].get(prefix);
|
|
349
|
+
if (v !== undefined) return v;
|
|
350
|
+
}
|
|
351
|
+
return rejectUnresolvedPrefix(prefix, pos);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function skipComment() {
|
|
355
|
+
const start = pos;
|
|
356
|
+
pos += 4; // "<!--"
|
|
357
|
+
const end = indexOfAscii(bytes, "-->", pos);
|
|
358
|
+
if (end < 0) fail("yorum kapanmıyor (-->)");
|
|
359
|
+
pos = end + 3;
|
|
360
|
+
/** @type {XmlComment} */
|
|
361
|
+
const node = { kind: "comment", start, end: pos };
|
|
362
|
+
return node;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function readCData() {
|
|
366
|
+
const start = pos;
|
|
367
|
+
pos += 9; // "<![CDATA["
|
|
368
|
+
const end = indexOfAscii(bytes, "]]>", pos);
|
|
369
|
+
if (end < 0) fail("CDATA kapanmıyor (]]>)");
|
|
370
|
+
const value = decoder.decode(bytes.subarray(pos, end));
|
|
371
|
+
pos = end + 3;
|
|
372
|
+
/** @type {XmlCData} */
|
|
373
|
+
const node = { kind: "cdata", start, end: pos, value };
|
|
374
|
+
return node;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function skipProcessingInstruction() {
|
|
378
|
+
pos += 2; // "<?"
|
|
379
|
+
const end = indexOfAscii(bytes, "?>", pos);
|
|
380
|
+
if (end < 0) fail("işleme talimatı kapanmıyor (?>)");
|
|
381
|
+
pos = end + 2;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function readAttributeValue() {
|
|
385
|
+
const quote = bytes[pos];
|
|
386
|
+
if (quote !== DQUOTE && quote !== SQUOTE) fail("öznitelik değeri tırnaksız");
|
|
387
|
+
pos++;
|
|
388
|
+
const start = pos;
|
|
389
|
+
while (pos < len && bytes[pos] !== quote) {
|
|
390
|
+
if (bytes[pos] === LT) fail("öznitelik değeri içinde kaçışsız '<'");
|
|
391
|
+
pos++;
|
|
392
|
+
}
|
|
393
|
+
if (pos >= len) fail("öznitelik değeri kapanmıyor");
|
|
394
|
+
const raw = decoder.decode(bytes.subarray(start, pos));
|
|
395
|
+
pos++; // closing quote
|
|
396
|
+
return decodeEntities(raw, start);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function readTagAttributes() {
|
|
400
|
+
/** @type {{qname: string, value: string, offset: number}[]} */
|
|
401
|
+
const raw = [];
|
|
402
|
+
for (;;) {
|
|
403
|
+
const before = pos;
|
|
404
|
+
skipWs();
|
|
405
|
+
if (pos >= len) fail("etiket kapanmıyor");
|
|
406
|
+
if (bytes[pos] === SLASH || bytes[pos] === GT) break;
|
|
407
|
+
if (pos === before) fail("öznitelikler arasında boşluk bekleniyor");
|
|
408
|
+
const attrOffset = pos;
|
|
409
|
+
const qname = readName();
|
|
410
|
+
skipWs();
|
|
411
|
+
if (bytes[pos] !== EQUALS) fail("öznitelikte '=' bekleniyor");
|
|
412
|
+
pos++;
|
|
413
|
+
skipWs();
|
|
414
|
+
const value = readAttributeValue();
|
|
415
|
+
raw.push({ qname, value, offset: attrOffset });
|
|
416
|
+
}
|
|
417
|
+
return raw;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function parseElement() {
|
|
421
|
+
const start = pos;
|
|
422
|
+
pos++; // '<'
|
|
423
|
+
const qname = readName();
|
|
424
|
+
const { prefix, localName } = splitQName(qname);
|
|
425
|
+
const rawAttrs = readTagAttributes();
|
|
426
|
+
|
|
427
|
+
// New namespace scope: collect this element's own xmlns declarations
|
|
428
|
+
// before resolving ANYTHING (including the element's own prefix) — a
|
|
429
|
+
// tag may declare the very namespace its own prefix uses.
|
|
430
|
+
/** @type {Map<string, string>} */
|
|
431
|
+
const scope = new Map();
|
|
432
|
+
for (const { qname: aqname, value } of rawAttrs) {
|
|
433
|
+
if (aqname === "xmlns") scope.set("", value);
|
|
434
|
+
else if (aqname.startsWith("xmlns:")) scope.set(aqname.slice(6), value);
|
|
435
|
+
}
|
|
436
|
+
nsScopes.push(scope);
|
|
437
|
+
|
|
438
|
+
const uri = resolveElementUri(prefix);
|
|
439
|
+
/** @type {XmlAttribute[]} */
|
|
440
|
+
const attributes = [];
|
|
441
|
+
for (const { qname: aqname, value, offset } of rawAttrs) {
|
|
442
|
+
if (aqname === "xmlns" || aqname.startsWith("xmlns:")) continue; // declarations aren't data attributes
|
|
443
|
+
const split = splitQName(aqname);
|
|
444
|
+
const savedPos = pos;
|
|
445
|
+
pos = offset;
|
|
446
|
+
const attrUri = resolveAttrUri(split.prefix);
|
|
447
|
+
pos = savedPos;
|
|
448
|
+
attributes.push({ prefix: split.prefix, localName: split.localName, uri: attrUri, value });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
skipWs();
|
|
452
|
+
const selfClosing = bytes[pos] === SLASH;
|
|
453
|
+
if (selfClosing) pos++;
|
|
454
|
+
if (bytes[pos] !== GT) fail("etiket '>' ile kapanmıyor");
|
|
455
|
+
pos++;
|
|
456
|
+
|
|
457
|
+
/** @type {XmlElement} */
|
|
458
|
+
const element = {
|
|
459
|
+
kind: "element",
|
|
460
|
+
prefix,
|
|
461
|
+
localName,
|
|
462
|
+
uri,
|
|
463
|
+
start,
|
|
464
|
+
end: pos,
|
|
465
|
+
contentStart: pos,
|
|
466
|
+
contentEnd: pos,
|
|
467
|
+
attributes,
|
|
468
|
+
children: [],
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
if (!selfClosing) {
|
|
472
|
+
element.children = parseContent(qname);
|
|
473
|
+
element.contentEnd = pos; // set by parseContent just before the closing tag
|
|
474
|
+
pos = skipClosingTag(qname);
|
|
475
|
+
element.end = pos;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
nsScopes.pop();
|
|
479
|
+
return element;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* @param {string} expectedQName
|
|
484
|
+
* @returns {number}
|
|
485
|
+
*/
|
|
486
|
+
function skipClosingTag(expectedQName) {
|
|
487
|
+
const closeStart = pos;
|
|
488
|
+
pos += 2; // "</"
|
|
489
|
+
const qname = readName();
|
|
490
|
+
if (qname !== expectedQName) {
|
|
491
|
+
fail(`kapanış etiketi <${expectedQName}> ile eşleşmiyor, bulundu </${qname}> (bayt ${closeStart})`);
|
|
492
|
+
}
|
|
493
|
+
skipWs();
|
|
494
|
+
if (bytes[pos] !== GT) fail("kapanış etiketi '>' ile bitmiyor");
|
|
495
|
+
pos++;
|
|
496
|
+
return pos;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function readText() {
|
|
500
|
+
const start = pos;
|
|
501
|
+
while (pos < len && bytes[pos] !== LT) pos++;
|
|
502
|
+
const raw = decoder.decode(bytes.subarray(start, pos));
|
|
503
|
+
const value = decodeEntities(raw, start);
|
|
504
|
+
/** @type {XmlText} */
|
|
505
|
+
const node = { kind: "text", start, end: pos, value };
|
|
506
|
+
return node;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @param {string} parentQName
|
|
511
|
+
* @returns {XmlNode[]}
|
|
512
|
+
*/
|
|
513
|
+
function parseContent(parentQName) {
|
|
514
|
+
/** @type {XmlNode[]} */
|
|
515
|
+
const children = [];
|
|
516
|
+
for (;;) {
|
|
517
|
+
if (pos >= len) fail(`<${parentQName}> hiç kapanmadan girdi bitti`);
|
|
518
|
+
if (bytes[pos] !== LT) {
|
|
519
|
+
children.push(readText());
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (bytes[pos + 1] === SLASH) {
|
|
523
|
+
break; // closing tag for the current element — let the caller consume it
|
|
524
|
+
}
|
|
525
|
+
if (bytes[pos + 1] === BANG) {
|
|
526
|
+
if (matchesAscii(bytes, pos, "<!--")) {
|
|
527
|
+
children.push(skipComment());
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
if (matchesAscii(bytes, pos, "<![CDATA[")) {
|
|
531
|
+
children.push(readCData());
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
if (matchesAscii(bytes, pos, "<!DOCTYPE")) rejectDoctype(bytes, pos);
|
|
535
|
+
fail("bilinmeyen '<!' yapısı");
|
|
536
|
+
}
|
|
537
|
+
if (bytes[pos + 1] === QUESTION) {
|
|
538
|
+
skipProcessingInstruction();
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
children.push(parseElement());
|
|
542
|
+
}
|
|
543
|
+
return children;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// ---- XML declaration (optional, must be first if present) ----
|
|
547
|
+
if (matchesAscii(bytes, 0, "<?xml") && isWhitespaceByte(bytes[5])) {
|
|
548
|
+
const declEnd = indexOfAscii(bytes, "?>", 5);
|
|
549
|
+
if (declEnd < 0) fail("XML bildirimi kapanmıyor (?>)");
|
|
550
|
+
const declText = decoder.decode(bytes.subarray(5, declEnd));
|
|
551
|
+
const encMatch = /encoding\s*=\s*["']([^"']+)["']/.exec(declText);
|
|
552
|
+
if (encMatch) declaredEncoding = encMatch[1];
|
|
553
|
+
pos = declEnd + 2;
|
|
554
|
+
}
|
|
555
|
+
rejectNonUtf8Encoding(declaredEncoding);
|
|
556
|
+
|
|
557
|
+
// ---- misc before the root element ----
|
|
558
|
+
/** @type {XmlElement | null} */
|
|
559
|
+
let root = null;
|
|
560
|
+
while (pos < len) {
|
|
561
|
+
skipWs();
|
|
562
|
+
if (pos >= len) break;
|
|
563
|
+
if (bytes[pos] !== LT) fail("kök elementten önce beklenmeyen içerik");
|
|
564
|
+
if (bytes[pos + 1] === BANG) {
|
|
565
|
+
if (matchesAscii(bytes, pos, "<!--")) {
|
|
566
|
+
skipComment();
|
|
567
|
+
continue;
|
|
568
|
+
}
|
|
569
|
+
if (matchesAscii(bytes, pos, "<!DOCTYPE")) rejectDoctype(bytes, pos);
|
|
570
|
+
fail("bilinmeyen '<!' yapısı");
|
|
571
|
+
}
|
|
572
|
+
if (bytes[pos + 1] === QUESTION) {
|
|
573
|
+
skipProcessingInstruction();
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
root = parseElement();
|
|
577
|
+
break;
|
|
578
|
+
}
|
|
579
|
+
if (root === null) fail("kök element bulunamadı");
|
|
580
|
+
|
|
581
|
+
// ---- misc after the root element (comments/PIs/whitespace only) ----
|
|
582
|
+
while (pos < len) {
|
|
583
|
+
skipWs();
|
|
584
|
+
if (pos >= len) break;
|
|
585
|
+
if (bytes[pos] !== LT) fail("kök elementten sonra beklenmeyen içerik");
|
|
586
|
+
if (matchesAscii(bytes, pos, "<!--")) {
|
|
587
|
+
skipComment();
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
if (bytes[pos + 1] === QUESTION) {
|
|
591
|
+
skipProcessingInstruction();
|
|
592
|
+
continue;
|
|
593
|
+
}
|
|
594
|
+
fail("kök elementten sonra ikinci bir element");
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return { root, declaredEncoding };
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* @param {Uint8Array} bytes
|
|
602
|
+
* @param {string} literal ASCII only
|
|
603
|
+
* @param {number} from
|
|
604
|
+
* @returns {number}
|
|
605
|
+
*/
|
|
606
|
+
function indexOfAscii(bytes, literal, from) {
|
|
607
|
+
const first = literal.charCodeAt(0);
|
|
608
|
+
outer: for (let i = from; i <= bytes.length - literal.length; i++) {
|
|
609
|
+
if (bytes[i] !== first) continue;
|
|
610
|
+
for (let j = 1; j < literal.length; j++) {
|
|
611
|
+
if (bytes[i + j] !== literal.charCodeAt(j)) continue outer;
|
|
612
|
+
}
|
|
613
|
+
return i;
|
|
614
|
+
}
|
|
615
|
+
return -1;
|
|
616
|
+
}
|