Something went wrong. Try again.
Something went wrong. Try again.
7.8 kB · 198 lines
JavaScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199// XML/XHTML parsing + serialization on top of linkedom.//// Replaces the browser DOMParser/XMLSerializer used by FilesPage.html.// safeSerialize is a verbatim port of source line 3997.//// ---------------------------------------------------------------------------// linkedom quirks measured on the installed version (probe results, not guesses).// EVERY port site must go through the shims below — never call linkedom's NS// APIs directly, they are wrong in ways that silently return nothing://// 1. `getElementsByTagName('*')` returns an EMPTY NodeList. Unusable.// `querySelectorAll('*')` works and is what byLocalName uses instead.// 2. `getElementsByTagNameNS('*', name)` matches the *qualified* nodeName, so// `getElementsByTagNameNS('*','creator')` finds 0 elements in an OPF whose// metadata uses `<dc:creator>`. This is the single most dangerous quirk —// it fails silently and would break title/author/identifier extraction.// 3. `element.localName` returns the QUALIFIED name including the prefix// ("dc:creator", "svg:svg"), not the local part. byLocalName strips it.// 4. `getAttributeNS(ns, local)` always returns null; attributes are keyed by// their qualified name. getAttrNS therefore tries getAttribute('local')// then getAttribute('prefix:local').// 5. `querySelector('image[*|href]')` THROWS ("Namespaced attributes are not// yet supported by css-select"). Never use namespaced CSS selectors.// 6. `namespaceURI` is unreliable (an OPF <manifest> reports the XHTML NS), so// createElementNS callers pass an explicit constant NS instead of reading// it off a neighbouring node.// 7. `doc.toString()` already emits the <?xml?> declaration and produces no// `xmlns:ns0` prefix noise, so safeSerialize's fix-ups are harmless no-ops// here. They are kept verbatim anyway per the port map.// 8. There is no `parsererror` element convention, and linkedom recovers from// mismatched/unclosed tags rather than failing. Only truly element-less// input yields a null documentElement. Detection is therefore: it threw,// documentElement is null, or documentElement is a <parsererror>.// ---------------------------------------------------------------------------
import { DOMParser } from 'linkedom';
/** Strip a namespace prefix from a qualified name: "dc:creator" -> "creator". */function localPart(name) { const n = name || ''; const i = n.indexOf(':'); return i === -1 ? n : n.slice(i + 1);}
/** Prefix of a qualified name, or '' when unprefixed. */function prefixPart(name) { const n = name || ''; const i = n.indexOf(':'); return i === -1 ? '' : n.slice(0, i);}
/** The qualified tag name of an element, tolerating either linkedom accessor. */function qualifiedName(el) { return el.localName || el.nodeName || '';}
function parse(text, mimeType) { try { const doc = new DOMParser().parseFromString(text, mimeType); if (!doc || !doc.documentElement) return { doc: null, ok: false }; if (localPart(qualifiedName(doc.documentElement)) === 'parsererror') { return { doc: null, ok: false }; } return { doc, ok: true }; } catch (e) { return { doc: null, ok: false }; }}
/** Parse OPF/NCX/generic XML. Returns {doc, ok}; ok:false routes to regex fallbacks. */export function parseXml(text) { return parse(text, 'text/xml');}
/** Parse XHTML content documents. Returns {doc, ok}. */export function parseXhtml(text) { return parse(text, 'application/xhtml+xml');}
/** * Every element under `root` whose local name (prefix stripped) equals `name`. * Replaces every `getElementsByTagNameNS('*', name)` call in the source * (sites: 4021/4024/4028, 4057, 4082-4083, 4303/4323/4329, 3905-3906). * * When `root` is a Document the document element itself is included (matching * the source's document-level lookups for <package>); when `root` is an Element * only descendants are considered (matching `svg.getElementsByTagName('image')`). */export function byLocalName(root, name) { if (!root) return []; let nodes; try { nodes = root.querySelectorAll('*'); } catch (e) { return []; } const out = []; for (const el of nodes) { if (localPart(qualifiedName(el)) === name) out.push(el); } return out;}
/** * Read a namespaced attribute the way the source's fallback chains do * (3907-3920 role/opf:role, 4204-4206 and 4268 xlink:href). * * Tries the bare local name, then each candidate prefix, then any attribute * whose qualified name ends in `:localName`, then getAttributeNS in a * try/catch (which linkedom always answers with null, but a future/other * implementation may not). * * `preferPrefixed` flips the first two steps. The source's two chains differ in * precedence: the opf:role chain (3907-3920) reads the bare attribute first, * while the xlink:href chain (4204-4206, 4268) reads the namespaced one first * and only falls back to a bare `href`. Pass true at the xlink sites. */export function getAttrNS(el, localName, nsUri, prefixes = [], preferPrefixed = false) { if (!el) return null; const tryGet = n => { try { const v = el.getAttribute(n); return v === null || v === undefined ? null : v; } catch (e) { return null; } }; const tryPrefixes = () => { for (const p of prefixes) { const v = tryGet(`${p}:${localName}`); if (v) return v; } return null; };
if (preferPrefixed) { const pre = tryPrefixes(); if (pre) return pre; const bare = tryGet(localName); if (bare) return bare; } else { const direct = tryGet(localName); if (direct) return direct; const pre = tryPrefixes(); if (pre) return pre; }
// Any prefix at all — covers documents using a non-conventional prefix. try { for (const attr of el.attributes || []) { const n = attr.name || ''; if (prefixPart(n) && localPart(n) === localName) { if (attr.value) return attr.value; } } } catch (e) { /* ignore */ }
if (nsUri) { try { const v = el.getAttributeNS(nsUri, localName); if (v) return v; } catch (e) { /* linkedom: always null, never throws — belt and braces */ } }
return null;}
/** * Serialize an XML doc back to string, preserving the original <?xml?> declaration * and cleaning up XMLSerializer namespace prefix noise (xmlns:ns0 etc). (3997) */export function safeSerialize(doc, originalContent) { let result = doc.toString();
// Restore <?xml?> declaration if original had one if (originalContent && /^\s*<\?xml\b/.test(originalContent) && !/^\s*<\?xml\b/.test(result)) { const declMatch = originalContent.match(/^\s*(<\?xml[^?]*\?>)/); if (declMatch) result = declMatch[1] + '\n' + result; }
// Clean up XMLSerializer namespace prefix noise (xmlns:ns0="..." ns0:attr="...") result = result.replace(/ xmlns:ns\d+="[^"]*"/g, ''); result = result.replace(/ ns\d+:/g, ' ');
// linkedom-specific: unlike the browser XMLSerializer (which emits no // declaration at all, hence the restore above), linkedom INVENTS an // `<?xml version="1.0" encoding="utf-8"?>` even for input that had none. // Drop it again so the contract matches the source's: the output carries a // declaration exactly when the input did. // // Note the encoding is deliberately NOT restored verbatim when the original // did have one. safeReadText decodes legacy encodings and everything is // re-encoded as UTF-8 on write, so linkedom's `utf-8` is the truthful label — // echoing back an original `encoding="windows-1252"` would mis-declare it. if (originalContent && !/^\s*<\?xml\b/.test(originalContent)) { result = result.replace(/^\s*<\?xml[^?]*\?>\s*/, ''); }
return result;}