123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- /**
- * @typedef {import('micromark-util-types').Options} Options
- * @typedef {import('micromark-util-types').Value} Value
- * @typedef {import('micromark-util-types').Encoding} Encoding
- */
- /**
- * @callback Callback
- * Function called when write was successful.
- * @returns {undefined}
- * Nothing.
- *
- * @typedef PipeOptions
- * @property {boolean | null | undefined} [end]
- *
- * @typedef {Omit<NodeJS.ReadableStream & NodeJS.WritableStream, 'isPaused' | 'pause' | 'read' | 'resume' | 'setEncoding' | 'unpipe' | 'unshift' | 'wrap'>} MinimalDuplex
- */
- import {EventEmitter} from 'node:events'
- import {compile} from './lib/compile.js'
- import {parse} from './lib/parse.js'
- import {postprocess} from './lib/postprocess.js'
- import {preprocess} from './lib/preprocess.js'
- /**
- * Create a duplex (readable and writable) stream.
- *
- * Some of the work to parse markdown can be done streaming, but in the
- * end buffering is required.
- *
- * micromark does not handle errors for you, so you must handle errors on whatever
- * streams you pipe into it.
- * As markdown does not know errors, `micromark` itself does not emit errors.
- *
- * @param {Options | null | undefined} [options]
- * Configuration (optional).
- * @returns {MinimalDuplex}
- * Duplex stream.
- */
- export function stream(options) {
- const prep = preprocess()
- const tokenize = parse(options).document().write
- const comp = compile(options)
- /** @type {boolean} */
- let ended
- /** @type {MinimalDuplex} */
- // @ts-expect-error `addListener` is fine.
- const emitter = Object.assign(new EventEmitter(), {
- end,
- pipe,
- readable: true,
- writable: true,
- write
- })
- return emitter
- /**
- * Write a chunk into memory.
- *
- * @overload
- * @param {Value | null | undefined} [chunk]
- * Slice of markdown to parse (`string` or `Uint8Array`).
- * @param {Encoding | null | undefined} [encoding]
- * Character encoding to understand `chunk` as when it’s a `Uint8Array`
- * (`string`, default: `'utf8'`).
- * @param {Callback | null | undefined} [callback]
- * Function called when write was successful.
- * @returns {boolean}
- * Whether write was successful.
- *
- * @overload
- * @param {Value | null | undefined} [chunk]
- * Slice of markdown to parse (`string` or `Uint8Array`).
- * @param {Callback | null | undefined} [callback]
- * Function called when write was successful.
- * @returns {boolean}
- * Whether write was successful.
- *
- * @param {Value | null | undefined} [chunk]
- * Slice of markdown to parse (`string` or `Uint8Array`).
- * @param {Callback | Encoding | null | undefined} [encoding]
- * Character encoding to understand `chunk` as when it’s a `Uint8Array`
- * (`string`, default: `'utf8'`).
- * @param {Callback | null | undefined} [callback]
- * Function called when write was successful.
- * @returns {boolean}
- * Whether write was successful.
- */
- function write(chunk, encoding, callback) {
- if (typeof encoding === 'function') {
- callback = encoding
- encoding = undefined
- }
- if (ended) {
- throw new Error('Did not expect `write` after `end`')
- }
- tokenize(prep(chunk || '', encoding))
- if (callback) {
- callback()
- }
- // Signal successful write.
- return true
- }
- /**
- * End the writing.
- *
- * Passes all arguments as a final `write`.
- *
- * @overload
- * @param {Value | null | undefined} [chunk]
- * Slice of markdown to parse (`string` or `Uint8Array`).
- * @param {Encoding | null | undefined} [encoding]
- * Character encoding to understand `chunk` as when it’s a `Uint8Array`
- * (`string`, default: `'utf8'`).
- * @param {Callback | null | undefined} [callback]
- * Function called when write was successful.
- * @returns {boolean}
- * Whether write was successful.
- *
- * @overload
- * @param {Value | null | undefined} [chunk]
- * Slice of markdown to parse (`string` or `Uint8Array`).
- * @param {Callback | null | undefined} [callback]
- * Function called when write was successful.
- * @returns {boolean}
- * Whether write was successful.
- *
- * @overload
- * @param {Callback | null | undefined} [callback]
- * Function called when write was successful.
- * @returns {boolean}
- *
- * @param {Callback | Value | null | undefined} [chunk]
- * Slice of markdown to parse (`string` or `Uint8Array`).
- * @param {Callback | Encoding | null | undefined} [encoding]
- * Character encoding to understand `chunk` as when it’s a `Uint8Array`
- * (`string`, default: `'utf8'`).
- * @param {Callback | null | undefined} [callback]
- * Function called when write was successful.
- * @returns {boolean}
- * Whether write was successful.
- */
- function end(chunk, encoding, callback) {
- if (typeof chunk === 'function') {
- encoding = chunk
- chunk = undefined
- }
- if (typeof encoding === 'function') {
- callback = encoding
- encoding = undefined
- }
- write(chunk, encoding, callback)
- emitter.emit('data', comp(postprocess(tokenize(prep('', encoding, true)))))
- emitter.emit('end')
- ended = true
- return true
- }
- /**
- * Pipe the processor into a writable stream.
- *
- * Basically `Stream#pipe`, but inlined and simplified to keep the bundled
- * size down.
- * See: <https://github.com/nodejs/node/blob/43a5170/lib/internal/streams/legacy.js#L13>.
- *
- * @template {NodeJS.WritableStream} Stream
- * @param {Stream} dest
- * @param {PipeOptions | null | undefined} [options]
- * @returns {Stream}
- */
- function pipe(dest, options) {
- emitter.on('data', ondata)
- emitter.on('error', onerror)
- emitter.on('end', cleanup)
- emitter.on('close', cleanup)
- // If the `end` option is not supplied, `dest.end()` will be
- // called when the `end` or `close` events are received.
- // @ts-expect-error `_isStdio` is available on `std{err,out}`
- if (!dest._isStdio && (!options || options.end !== false)) {
- emitter.on('end', onend)
- }
- dest.on('error', onerror)
- dest.on('close', cleanup)
- dest.emit('pipe', emitter)
- return dest
- /**
- * End destination stream.
- *
- * @returns {undefined}
- */
- function onend() {
- if (dest.end) {
- dest.end()
- }
- }
- /**
- * Handle data.
- *
- * @param {string} chunk
- * @returns {undefined}
- */
- function ondata(chunk) {
- if (dest.writable) {
- dest.write(chunk)
- }
- }
- /**
- * Clean listeners.
- *
- * @returns {undefined}
- */
- function cleanup() {
- emitter.removeListener('data', ondata)
- emitter.removeListener('end', onend)
- emitter.removeListener('error', onerror)
- emitter.removeListener('end', cleanup)
- emitter.removeListener('close', cleanup)
- dest.removeListener('error', onerror)
- dest.removeListener('close', cleanup)
- }
- /**
- * Close dangling pipes and handle unheard errors.
- *
- * @param {Error | null | undefined} [error]
- * @returns {undefined}
- */
- function onerror(error) {
- cleanup()
- if (!emitter.listenerCount('error')) {
- throw error // Unhandled stream error in pipe.
- }
- }
- }
- }
|