@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +108 -70
  2. package/LICENSE +21 -21
  3. package/README.md +286 -247
  4. package/dist/canvas/box-clipboard.js +238 -238
  5. package/dist/canvas/box-selection.js +80 -80
  6. package/dist/canvas/canvas-editor.js +502 -502
  7. package/dist/canvas/controller.js +717 -717
  8. package/dist/canvas/geometry.js +131 -131
  9. package/dist/canvas/gesture.d.ts +0 -1
  10. package/dist/canvas/gesture.js +289 -289
  11. package/dist/canvas/handles.js +212 -212
  12. package/dist/canvas/menu.js +82 -82
  13. package/dist/canvas/mode-controller.js +81 -81
  14. package/dist/canvas/nudge.js +108 -108
  15. package/dist/canvas/overlay.js +149 -149
  16. package/dist/canvas/snap.js +132 -132
  17. package/dist/canvas/viewport.js +112 -112
  18. package/dist/canvas/z-order.js +171 -171
  19. package/dist/commands/alignment.js +150 -150
  20. package/dist/commands/box.js +272 -272
  21. package/dist/commands/clipboard.js +93 -93
  22. package/dist/commands/graphemes.js +112 -112
  23. package/dist/commands/marks.js +153 -153
  24. package/dist/commands/paragraph-props.js +176 -176
  25. package/dist/commands/size.js +368 -368
  26. package/dist/commands/text.js +291 -291
  27. package/dist/docx.d.ts +1 -0
  28. package/dist/docx.js +15 -13
  29. package/dist/editor/editing-host.js +434 -434
  30. package/dist/element.js +41 -41
  31. package/dist/fonts/OFL-Carlito.txt +94 -0
  32. package/dist/fonts/calibri-metrics.d.ts +27 -0
  33. package/dist/fonts/calibri-metrics.js +88 -0
  34. package/dist/history/history.js +433 -433
  35. package/dist/index.js +63 -63
  36. package/dist/input/beforeinput.js +224 -224
  37. package/dist/input/clipboard.js +107 -107
  38. package/dist/input/composition.js +341 -341
  39. package/dist/input/keymap.js +54 -54
  40. package/dist/io/ooxml/custgeom.js +72 -72
  41. package/dist/io/ooxml/drawingml.js +232 -232
  42. package/dist/io/ooxml/edits.js +67 -67
  43. package/dist/io/ooxml/export-splice.js +880 -880
  44. package/dist/io/ooxml/export.js +365 -365
  45. package/dist/io/ooxml/fill.d.ts +93 -8
  46. package/dist/io/ooxml/fill.js +618 -562
  47. package/dist/io/ooxml/fit.d.ts +252 -0
  48. package/dist/io/ooxml/fit.js +598 -0
  49. package/dist/io/ooxml/import-gate.js +200 -200
  50. package/dist/io/ooxml/import.js +691 -691
  51. package/dist/io/ooxml/inflate.js +126 -126
  52. package/dist/io/ooxml/media.js +87 -87
  53. package/dist/io/ooxml/ns.d.ts +2 -0
  54. package/dist/io/ooxml/ns.js +26 -21
  55. package/dist/io/ooxml/numbering.js +290 -290
  56. package/dist/io/ooxml/opc-write.js +171 -171
  57. package/dist/io/ooxml/provenance.js +93 -93
  58. package/dist/io/ooxml/recipe.js +708 -708
  59. package/dist/io/ooxml/relationships.js +59 -59
  60. package/dist/io/ooxml/run-fonts.d.ts +89 -0
  61. package/dist/io/ooxml/run-fonts.js +378 -0
  62. package/dist/io/ooxml/slots.d.ts +4 -0
  63. package/dist/io/ooxml/slots.js +582 -581
  64. package/dist/io/ooxml/styles-resolve.js +257 -257
  65. package/dist/io/ooxml/units.js +43 -43
  66. package/dist/io/ooxml/value.js +120 -120
  67. package/dist/io/ooxml/xml-scan.js +616 -616
  68. package/dist/io/ooxml/zip-container.js +228 -228
  69. package/dist/io/ooxml/zip-reject.js +106 -106
  70. package/dist/measure/placement.js +83 -83
  71. package/dist/measure/report.js +75 -75
  72. package/dist/model/ids.js +70 -70
  73. package/dist/model/normalize.js +146 -146
  74. package/dist/model/position.js +283 -283
  75. package/dist/model/transaction.js +209 -209
  76. package/dist/platform/capabilities.js +25 -25
  77. package/dist/react/index.js +21 -21
  78. package/dist/selection/controller.js +315 -315
  79. package/dist/selection/dom-map.js +334 -334
  80. package/dist/state/editor-state.js +109 -109
  81. package/dist/styles.css +66 -66
  82. 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. `&#38;`) 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: &amp; &lt; &gt; &quot; &apos; 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. `&#38;`) 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: &amp; &lt; &gt; &quot; &apos; 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
+ }