UNPKG

thenavisapp

Version:

This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).

151 lines (134 loc) 5.27 kB
import { Writable } from 'node:stream'; import { Parser, type ParserOptions, type TreeAdapterTypeMap, type DefaultTreeAdapterMap } from 'parse5'; /* eslint-disable unicorn/consistent-function-scoping -- The rule seems to be broken here. */ /** * Streaming HTML parser with scripting support. * A [writable stream](https://nodejs.org/api/stream.html#stream_class_stream_writable). * * @example * * ```js * const ParserStream = require('parse5-parser-stream'); * const http = require('http'); * const { finished } = require('node:stream'); * * // Fetch the page content and obtain it's <head> node * http.get('http://inikulin.github.io/parse5/', res => { * const parser = new ParserStream(); * * finished(parser, () => { * console.log(parser.document.childNodes[1].childNodes[0].tagName); //> 'head' * }); * * res.pipe(parser); * }); * ``` * */ export class ParserStream<T extends TreeAdapterTypeMap = DefaultTreeAdapterMap> extends Writable { static getFragmentStream<T extends TreeAdapterTypeMap>( fragmentContext?: T['parentNode'] | null, options?: ParserOptions<T>, ): ParserStream<T> { const parser = Parser.getFragmentParser(fragmentContext, options); const stream = new ParserStream(options, parser); return stream; } private lastChunkWritten = false; private writeCallback: undefined | (() => void) = undefined; private pendingHtmlInsertions: string[] = []; /** The resulting document node. */ public get document(): T['document'] { return this.parser.document; } public getFragment(): T['documentFragment'] { return this.parser.getFragment(); } /** * @param options Parsing options. */ constructor( options?: ParserOptions<T>, public parser: Parser<T> = new Parser(options), ) { super({ decodeStrings: false }); const resume = (): void => { for (let i = this.pendingHtmlInsertions.length - 1; i >= 0; i--) { this.parser.tokenizer.insertHtmlAtCurrentPos(this.pendingHtmlInsertions[i]); } this.pendingHtmlInsertions.length = 0; //NOTE: keep parsing if we don't wait for the next input chunk this.parser.tokenizer.resume(this.writeCallback); }; const documentWrite = (html: string): void => { if (!this.parser.stopped) { this.pendingHtmlInsertions.push(html); } }; const scriptHandler = (scriptElement: T['element']): void => { if (this.listenerCount('script') > 0) { this.parser.tokenizer.pause(); this.emit('script', scriptElement, documentWrite, resume); } }; this.parser.scriptHandler = scriptHandler; } //WritableStream implementation override _write(chunk: string, _encoding: string, callback: () => void): void { if (typeof chunk !== 'string') { throw new TypeError('Parser can work only with string streams.'); } this.writeCallback = callback; this.parser.tokenizer.write(chunk, this.lastChunkWritten, this.writeCallback); } // TODO [engine:node@>=16]: Due to issues with Node < 16, we are overriding `end` instead of `_final`. // eslint-disable-next-line @typescript-eslint/no-explicit-any override end(chunk?: any, encoding?: any, callback?: any): any { this.lastChunkWritten = true; super.end(chunk || '', encoding, callback); } } export interface ParserStream<T extends TreeAdapterTypeMap = DefaultTreeAdapterMap> { /** * Raised when parser encounters a `<script>` element. If this event has listeners, parsing will be suspended once * it is emitted. So, if `<script>` has the `src` attribute, you can fetch it, execute and then resume parsing just * like browsers do. * * @example * * ```js * const ParserStream = require('parse5-parser-stream'); * const http = require('http'); * * const parser = new ParserStream(); * * parser.on('script', (scriptElement, documentWrite, resume) => { * const src = scriptElement.attrs.find(({ name }) => name === 'src').value; * * http.get(src, res => { * // Fetch the script content, execute it with DOM built around `parser.document` and * // `document.write` implemented using `documentWrite`. * ... * // Then resume parsing. * resume(); * }); * }); * * parser.end('<script src="example.com/script.js"></script>'); * ``` * * @param event Name of the event * @param handler */ on( event: 'script', handler: (scriptElement: T['element'], documentWrite: (html: string) => void, resume: () => void) => void, ): void; /** * Base event handler. * * @param event Name of the event * @param handler Event handler */ on(event: string, handler: (...args: any[]) => void): this; }