UNPKG

@file-type/xml

Version:
244 lines (243 loc) 7.77 kB
import sax from 'sax'; import { parseDoctype } from "./xmlDocTypeParser.js"; function startsWith(array, prefix) { if (prefix.length > array.length) { return false; } for (let i = 0; i < prefix.length; i++) { if (array[i] !== prefix[i]) { return false; } } return true; } function hasXmlTag(xmlString) { return /^<\s*\w+(?=\s+[^<>]*=|>)/.test(xmlString); } function hasArrayXmlTag(array, encoding) { const textDecoder = new TextDecoder(encoding); return hasXmlTag(textDecoder.decode(array)); } export function isXml(array) { if (startsWith(array, [60, 63, 120, 109, 108, 32])) { return { xml: true, encoding: 'utf-8', offset: 0 }; } if (startsWith(array, [0xEF, 0xBB, 0xBF])) { // UTF-8 BOM const encoding = 'utf-8'; if (startsWith(array.subarray(3), [60, 63, 120, 109, 108, 32]) || hasArrayXmlTag(array, encoding)) { return { xml: true, encoding, offset: 3 }; } } if (startsWith(array, [0xFE, 0xFF])) { const encoding = 'utf-16be'; if (startsWith(array.subarray(2), [0, 60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32]) || hasArrayXmlTag(array, encoding)) { return { xml: true, encoding, offset: 2 }; } } if (startsWith(array, [0xFF, 0xFE])) { const encoding = 'utf-16le'; if (startsWith(array.subarray(2), [60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32, 0]) || hasArrayXmlTag(array, encoding)) { return { xml: true, encoding, offset: 2 }; } } if (startsWith(array, [0, 60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32])) { return { xml: true, encoding: 'utf-16be', offset: 0 }; } if (startsWith(array, [60, 0, 63, 0, 120, 0, 109, 0, 108, 0, 32, 0])) { return { xml: true, encoding: 'utf-16le', offset: 0 }; } if (hasArrayXmlTag(array, 'utf-8')) { return { xml: true, encoding: 'utf-8', offset: 0 }; } return { xml: false }; } export const fileType = { docBook: { ext: 'dbk', mime: 'application/docbook+xml' }, gpx: { ext: 'gpx', mime: 'application/gpx+xml' }, musicXml: { ext: 'musicxml', mime: 'application/vnd.recordare.musicxml+xml' }, plist: { ext: 'plist', mime: 'application/x-plist' }, smil: { ext: 'smil', mime: 'application/smil+xml' }, svg: { ext: 'svg', mime: 'image/svg+xml' }, x3d: { ext: 'x3d', mime: 'model/x3d+xml' } }; /** * Maps the root element namespace to the corresponding file-type */ const namespaceMapping = { 'http://www.w3.org/2000/svg': fileType.svg, 'http://www.w3.org/1999/xhtml': { ext: 'xhtml', mime: 'application/xhtml+xml' }, 'http://www.opengis.net/kml/2.2': { ext: 'kml', mime: 'application/vnd.google-earth.kml+xml' }, 'http://www.opengis.net/gml': { ext: 'gml', mime: 'application/gml+xml' }, 'http://www.w3.org/ns/ttml': { ext: 'ttml', mime: 'application/ttml+xml' }, 'http://www.w3.org/2001/SMIL20/Language': fileType.smil, 'http://www.w3.org/2005/Atom': { ext: 'atom', mime: 'application/atom+xml' }, 'urn:oasis:names:tc:xliff:document:2.0': { ext: 'xlf', mime: 'application/xliff+xml' }, 'http://docbook.org/ns/docbook': fileType.docBook, 'http://www.tei-c.org/ns/1.0': { ext: 'tei', mime: 'application/tei+xml' }, 'http://www.w3.org/1998/Math/MathML': { ext: 'mml', mime: 'application/mathml+xml' }, 'http://www.topografix.com/GPX/1/0': fileType.gpx, 'http://www.topografix.com/GPX/1/1': fileType.gpx, }; /** * Maps the root element name to the corresponding file-type. * Used for Non-namespaced XML */ const rootNameMapping = { opml: { ext: 'opml', mime: 'text/x-opml' }, plist: fileType.plist, rss: { ext: 'rss', mime: 'application/rss+xml' }, 'score-partwise': fileType.musicXml, smil: fileType.smil, svg: fileType.svg }; /** * Maps DOCTYPE public identifier to the corresponding file-type */ const docTypeMapping = { '-//OASIS//DTD DocBook XML V4.0//EN': fileType.docBook, '-//OASIS//DTD DocBook XML V4.1//EN': fileType.docBook, '-//OASIS//DTD DocBook XML V4.2//EN': fileType.docBook, '-//OASIS//DTD DocBook XML V4.3//EN': fileType.docBook, '-//OASIS//DTD DocBook XML V4.4//EN': fileType.docBook, '-//OASIS//DTD DocBook XML V4.5//EN': fileType.docBook, '-//Recordare//DTD MusicXML 4.0 Partwise//EN': fileType.musicXml, '-//Apple//DTD PLIST 1.0//EN': fileType.plist, 'ISO//Web3D//DTD X3D 3.3//EN': fileType.x3d, }; export class XmlTextDetector { constructor(options) { this.options = options ?? {}; this.firstTag = true; this.onEnd = false; this.parser = sax.parser(true, { xmlns: true }); this.nesting = 0; this.parser.onerror = e => { if (e.message.startsWith('Invalid character entity')) { // Allow entity reference return; } this.fileType = undefined; this.onEnd = true; }; this.parser.onopentag = node => { ++this.nesting; if (!this.firstTag || this.onEnd) { return; } this.firstTag = false; if (node.uri) { // Resolve file-type boot root element namespace this.fileType = namespaceMapping[node.uri]; } else if (node.name) { // Fall back on element name if there is no namespace this.fileType = rootNameMapping[node.name.toLowerCase()]; } if (this.fileType && !this.options.fullScan) { this.onEnd = true; } }; this.parser.ondoctype = rawDocType => { if (this.fileType || this.onEnd) { return; } const docType = parseDoctype(rawDocType); if (docType.kind === 'PUBLIC' && docType.publicId) { this.fileType = docTypeMapping[docType.publicId]; if (this.fileType && !this.options.fullScan) { this.onEnd = true; } } }; this.parser.onclosetag = () => { --this.nesting; }; } write(text) { this.parser.write(text); } close() { this.parser.close(); this.onEnd = true; } isValid() { return this.nesting === 0; } } export const detectXml = { id: 'xml', detect: async (tokenizer) => { const buffer = new Uint8Array(512); // Increase the sample size from 12 to 128. await tokenizer.peekBuffer(buffer, { length: 128, mayBeLess: true }); const xmlDetection = isXml(buffer); if (xmlDetection.xml) { await tokenizer.ignore(xmlDetection.offset); const xmlTextDetector = new XmlTextDetector(); const textDecoder = new TextDecoder(xmlDetection.encoding); do { const len = await tokenizer.readBuffer(buffer, { mayBeLess: true }); const portion = buffer.subarray(0, len); const text = textDecoder.decode(portion); xmlTextDetector.write(text); if (len < buffer.length) { xmlTextDetector.close(); } } while (!xmlTextDetector.onEnd); return xmlTextDetector.fileType ?? { ext: 'xml', mime: 'application/xml' }; } } };