243 lines
7.1 KiB
JavaScript
243 lines
7.1 KiB
JavaScript
|
/**
|
|||
|
* @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.
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
}
|