{"version":3,"file":"utf8-D91g1XTG.mjs","names":[],"sources":["../src/xml/iterparse.ts","../src/utils/utf8.ts"],"sourcesContent":["// SAX iterator over OOXML XML payloads. Wraps `saxes` (XMLNS-aware) and yields\n// a flat stream of {start | end | text} events with names already converted to\n// Clark notation (`{ns}local`) — same shape as the DOM parser produces for\n// static XmlNode trees, so consumers can switch between bulk and streaming\n// reads without retouching name comparison.\n//\n// Phase 1 §3 acceptance: 1 k–row sheetData walked end-to-end with cell counts\n// matching the source. The phase-4 read-only worksheet drives real-world use;\n// this layer just produces the events.\n//\n// DOCTYPE / external entity declarations are forbidden. saxes does not expand\n// external entities, but a prescan also rejects DTDs in non-streaming inputs.\n// Streaming inputs are checked on the first chunk before being fed to the\n// parser.\n\nimport { SaxesParser } from 'saxes';\nimport { OpenXmlSchemaError } from '../utils/exceptions';\nimport { qname } from './namespaces';\n\nexport type SaxEvent =\n  | { kind: 'start'; name: string; attrs: Record<string, string> }\n  | { kind: 'end'; name: string }\n  | { kind: 'text'; text: string };\n\n/**\n * Streamable input: `Uint8Array`, plain string, or a Web `ReadableStream` of\n * `Uint8Array` chunks (produced by xlsx zip entries via fflate, fetch, file\n * streams, etc.).\n */\nexport type SaxInput = Uint8Array | string | ReadableStream<Uint8Array>;\n\nconst DOCTYPE_RE = /<!DOCTYPE\\b/;\nconst ENTITY_RE = /<!ENTITY\\b/;\n\nconst checkDoctype = (text: string): void => {\n  if (DOCTYPE_RE.test(text)) {\n    throw new OpenXmlSchemaError('DTD declarations are not permitted in OOXML payloads');\n  }\n  if (ENTITY_RE.test(text)) {\n    throw new OpenXmlSchemaError('Entity declarations are not permitted in OOXML payloads');\n  }\n};\n\nconst isReadableStream = (v: unknown): v is ReadableStream<Uint8Array> => {\n  return typeof v === 'object' && v !== null && typeof (v as ReadableStream).getReader === 'function';\n};\n\nconst decoder = (): TextDecoder => new TextDecoder('utf-8', { fatal: false });\n\ninterface SaxesOpenTag {\n  name: string;\n  uri: string;\n  local: string;\n  prefix: string;\n  attributes: Record<string, { value: string; uri: string; local: string; prefix: string }>;\n  isSelfClosing?: boolean;\n}\n\ninterface SaxesCloseTag {\n  name: string;\n  uri: string;\n  local: string;\n  prefix: string;\n}\n\nconst buildAttrsClark = (attrs: SaxesOpenTag['attributes']): Record<string, string> => {\n  const out: Record<string, string> = {};\n  for (const [, info] of Object.entries(attrs)) {\n    // saxes already resolved the namespace when xmlns: true is set; raw xmlns /\n    // xmlns:* declarations have prefix='xmlns' (or local==='xmlns' when\n    // default) and we drop those — they're rebuilt by the serializer.\n    if (info.prefix === 'xmlns' || (info.prefix === '' && info.local === 'xmlns')) continue;\n    const key = qname(info.uri, info.local);\n    out[key] = info.value;\n  }\n  return out;\n};\n\n/**\n * Parse the input as a stream of SAX events. Element / attribute names are\n * returned in Clark notation (`{ns}local`).\n */\nexport async function* iterParse(input: SaxInput): AsyncIterableIterator<SaxEvent> {\n  // Set up the parser. xmlns: true gives us resolved {uri, local, prefix} on\n  // every open / close tag and on every attribute.\n  const parser = new SaxesParser({ xmlns: true, fragment: false });\n\n  // Head-pointer ring instead of Array#shift: each saxes write() can produce\n  // hundreds of events in a single synchronous batch (a `<row>` with dozens of\n  // cells flushes one opentag + one text + one closetag per cell). `shift()`\n  // is O(n) per element in V8, so a single-batch drain of N events would be\n  // O(N²) before iteration. The head advances on yield; the queue is reset\n  // (head + length) once it drains so memory stays bounded.\n  let queue: SaxEvent[] = [];\n  let head = 0;\n  let pending: Error | undefined;\n\n  parser.on('error', (err: Error) => {\n    pending = err;\n  });\n  parser.on('doctype', () => {\n    pending = new OpenXmlSchemaError('DTD declarations are not permitted in OOXML payloads');\n  });\n  parser.on('opentag', (node: SaxesOpenTag) => {\n    queue.push({ kind: 'start', name: qname(node.uri, node.local), attrs: buildAttrsClark(node.attributes) });\n  });\n  parser.on('closetag', (node: SaxesCloseTag) => {\n    queue.push({ kind: 'end', name: qname(node.uri, node.local) });\n  });\n  parser.on('text', (text: string) => {\n    if (text.length > 0) queue.push({ kind: 'text', text });\n  });\n\n  const drain = function* (): IterableIterator<SaxEvent> {\n    for (;;) {\n      const ev = head < queue.length ? queue[head] : undefined;\n      if (ev === undefined) {\n        // Reset rather than grow forever; the next batch starts at index 0.\n        queue = [];\n        head = 0;\n        return;\n      }\n      head++;\n      yield ev;\n    }\n  };\n\n  const feed = (chunk: string): void => {\n    parser.write(chunk);\n    if (pending !== undefined) throw pending;\n  };\n\n  if (typeof input === 'string') {\n    checkDoctype(input);\n    feed(input);\n  } else if (input instanceof Uint8Array) {\n    const text = decoder().decode(input);\n    checkDoctype(text);\n    feed(text);\n    yield* drain();\n  } else if (isReadableStream(input)) {\n    const reader = input.getReader();\n    const td = decoder();\n    let firstChunkChecked = false;\n    let firstChunkBuffer = '';\n    while (true) {\n      const { done, value } = await reader.read();\n      if (done) break;\n      const chunk = td.decode(value, { stream: true });\n      if (!firstChunkChecked) {\n        // We need to see enough of the prologue to be sure no DOCTYPE is\n        // hiding. Buffer until we have ~256 chars; if the stream ends before\n        // we reach the threshold the tail handler below runs `checkDoctype`\n        // on the accumulated prologue. (The previous `|| done` here was dead:\n        // a true `done` short-circuits at the top of the loop.)\n        firstChunkBuffer += chunk;\n        if (firstChunkBuffer.length >= 256) {\n          checkDoctype(firstChunkBuffer);\n          firstChunkChecked = true;\n          feed(firstChunkBuffer);\n          yield* drain();\n        }\n      } else {\n        feed(chunk);\n        yield* drain();\n      }\n    }\n    // Stream ended; flush decoder + any buffered prologue.\n    const tail = td.decode();\n    if (!firstChunkChecked) {\n      const all = firstChunkBuffer + tail;\n      checkDoctype(all);\n      feed(all);\n      yield* drain();\n    } else if (tail.length > 0) {\n      feed(tail);\n      yield* drain();\n    }\n  } else {\n    throw new OpenXmlSchemaError('iterParse: unsupported input type');\n  }\n\n  parser.close();\n  if (pending !== undefined) throw pending;\n  yield* drain();\n}\n","// Fast UTF-8 byte-length scan. Used by streaming writers to decide when their\n// pending string buffer should be encoded + flushed.\n//\n// `s.length` returns UTF-16 code units, which undercounts BMP characters above\n// U+007F (1 code unit, 2 UTF-8 bytes for U+0080–U+07FF, 3 bytes for the rest\n// of the BMP). For CJK-heavy payloads the discrepancy is ~3× — large enough\n// to push a \"64 KB\" flush threshold to 192 KB of resident text. We scan the\n// string once and account for each codepoint instead of running a full\n// TextEncoder, which would also have to materialise the byte buffer we don't\n// need yet.\nexport function utf8ByteLength(s: string): number {\n  let n = 0;\n  for (let i = 0; i < s.length; i++) {\n    const c = s.charCodeAt(i);\n    if (c < 0x80) {\n      n += 1;\n    } else if (c < 0x800) {\n      n += 2;\n    } else if (c >= 0xd800 && c <= 0xdbff) {\n      // High surrogate. Only consume the next code unit when it actually\n      // is a low surrogate — an unpaired high surrogate (followed by a\n      // BMP char or by EOS) would otherwise swallow the next code unit\n      // and undercount the string. TextEncoder replaces a lone high\n      // surrogate with U+FFFD (3 bytes), so use 3 here too. A paired\n      // surrogate encodes one 4-byte codepoint.\n      const next = i + 1 < s.length ? s.charCodeAt(i + 1) : 0;\n      if (next >= 0xdc00 && next <= 0xdfff) {\n        n += 4;\n        i++;\n      } else {\n        n += 3;\n      }\n    } else {\n      // Includes unpaired low surrogates (0xDC00-0xDFFF), which encode as\n      // U+FFFD = 3 UTF-8 bytes through TextEncoder.\n      n += 3;\n    }\n  }\n  return n;\n}\n"],"mappings":"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"}