stream.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. /**
  2. * @import {Encoding, Value} from 'micromark-util-types'
  3. */
  4. /**
  5. * @typedef {import('micromark-util-types').Options} Options
  6. */
  7. /**
  8. * @callback Callback
  9. * Function called when write was successful.
  10. * @returns {undefined}
  11. * Nothing.
  12. *
  13. * @typedef PipeOptions
  14. * Configuration for piping.
  15. * @property {boolean | null | undefined} [end]
  16. * Whether to end the destination stream when the source stream ends.
  17. *
  18. * @typedef {Omit<NodeJS.ReadableStream & NodeJS.WritableStream, 'isPaused' | 'pause' | 'read' | 'resume' | 'setEncoding' | 'unpipe' | 'unshift' | 'wrap'>} MinimalDuplex
  19. * Duplex stream.
  20. */
  21. import {EventEmitter} from 'node:events'
  22. import {compile} from './lib/compile.js'
  23. import {parse} from './lib/parse.js'
  24. import {postprocess} from './lib/postprocess.js'
  25. import {preprocess} from './lib/preprocess.js'
  26. /**
  27. * Create a duplex (readable and writable) stream.
  28. *
  29. * Some of the work to parse markdown can be done streaming, but in the
  30. * end buffering is required.
  31. *
  32. * micromark does not handle errors for you, so you must handle errors on whatever
  33. * streams you pipe into it.
  34. * As markdown does not know errors, `micromark` itself does not emit errors.
  35. *
  36. * @param {Options | null | undefined} [options]
  37. * Configuration (optional).
  38. * @returns {MinimalDuplex}
  39. * Duplex stream.
  40. */
  41. export function stream(options) {
  42. const prep = preprocess()
  43. const tokenize = parse(options).document().write
  44. const comp = compile(options)
  45. /** @type {boolean} */
  46. let ended
  47. const emitter = /** @type {MinimalDuplex} */ (new EventEmitter())
  48. // @ts-expect-error: fine.
  49. emitter.end = end
  50. emitter.pipe = pipe
  51. emitter.readable = true
  52. emitter.writable = true
  53. // @ts-expect-error: fine.
  54. emitter.write = write
  55. return emitter
  56. /**
  57. * Write a chunk into memory.
  58. *
  59. * @overload
  60. * @param {Value | null | undefined} [chunk]
  61. * Slice of markdown to parse (`string` or `Uint8Array`).
  62. * @param {Encoding | null | undefined} [encoding]
  63. * Character encoding to understand `chunk` as when it’s a `Uint8Array`
  64. * (`string`, default: `'utf8'`).
  65. * @param {Callback | null | undefined} [callback]
  66. * Function called when write was successful.
  67. * @returns {boolean}
  68. * Whether write was successful.
  69. *
  70. * @overload
  71. * @param {Value | null | undefined} [chunk]
  72. * Slice of markdown to parse (`string` or `Uint8Array`).
  73. * @param {Callback | null | undefined} [callback]
  74. * Function called when write was successful.
  75. * @returns {boolean}
  76. * Whether write was successful.
  77. *
  78. * @param {Value | null | undefined} [chunk]
  79. * Slice of markdown to parse (`string` or `Uint8Array`).
  80. * @param {Callback | Encoding | null | undefined} [encoding]
  81. * Character encoding to understand `chunk` as when it’s a `Uint8Array`
  82. * (`string`, default: `'utf8'`).
  83. * @param {Callback | null | undefined} [callback]
  84. * Function called when write was successful.
  85. * @returns {boolean}
  86. * Whether write was successful.
  87. */
  88. function write(chunk, encoding, callback) {
  89. if (typeof encoding === 'function') {
  90. callback = encoding
  91. encoding = undefined
  92. }
  93. if (ended) {
  94. throw new Error('Did not expect `write` after `end`')
  95. }
  96. tokenize(prep(chunk || '', encoding))
  97. if (callback) {
  98. callback()
  99. }
  100. // Signal successful write.
  101. return true
  102. }
  103. /**
  104. * End the writing.
  105. *
  106. * Passes all arguments as a final `write`.
  107. *
  108. * @overload
  109. * @param {Value | null | undefined} [chunk]
  110. * Slice of markdown to parse (`string` or `Uint8Array`).
  111. * @param {Encoding | null | undefined} [encoding]
  112. * Character encoding to understand `chunk` as when it’s a `Uint8Array`
  113. * (`string`, default: `'utf8'`).
  114. * @param {Callback | null | undefined} [callback]
  115. * Function called when write was successful.
  116. * @returns {boolean}
  117. * Whether write was successful.
  118. *
  119. * @overload
  120. * @param {Value | null | undefined} [chunk]
  121. * Slice of markdown to parse (`string` or `Uint8Array`).
  122. * @param {Callback | null | undefined} [callback]
  123. * Function called when write was successful.
  124. * @returns {boolean}
  125. * Whether write was successful.
  126. *
  127. * @overload
  128. * @param {Callback | null | undefined} [callback]
  129. * Function called when write was successful.
  130. * @returns {boolean}
  131. * Whether write was successful.
  132. *
  133. * @param {Callback | Value | null | undefined} [chunk]
  134. * Slice of markdown to parse (`string` or `Uint8Array`).
  135. * @param {Callback | Encoding | null | undefined} [encoding]
  136. * Character encoding to understand `chunk` as when it’s a `Uint8Array`
  137. * (`string`, default: `'utf8'`).
  138. * @param {Callback | null | undefined} [callback]
  139. * Function called when write was successful.
  140. * @returns {boolean}
  141. * Whether write was successful.
  142. */
  143. function end(chunk, encoding, callback) {
  144. if (typeof chunk === 'function') {
  145. encoding = chunk
  146. chunk = undefined
  147. }
  148. if (typeof encoding === 'function') {
  149. callback = encoding
  150. encoding = undefined
  151. }
  152. write(chunk, encoding, callback)
  153. emitter.emit('data', comp(postprocess(tokenize(prep('', encoding, true)))))
  154. emitter.emit('end')
  155. ended = true
  156. return true
  157. }
  158. /**
  159. * Pipe the processor into a writable stream.
  160. *
  161. * Basically `Stream#pipe`, but inlined and simplified to keep the bundled
  162. * size down.
  163. * See: <https://github.com/nodejs/node/blob/43a5170/lib/internal/streams/legacy.js#L13>.
  164. *
  165. * @template {NodeJS.WritableStream} Stream
  166. * Writable stream.
  167. * @param {Stream} destination
  168. * Stream to pipe into.
  169. * @param {PipeOptions | null | undefined} [options]
  170. * Configuration.
  171. * @returns {Stream}
  172. * Destination stream.
  173. */
  174. function pipe(destination, options) {
  175. emitter.on('data', ondata)
  176. emitter.on('error', onerror)
  177. emitter.on('end', cleanup)
  178. emitter.on('close', cleanup)
  179. // If the `end` option is not supplied, `destination.end()` will be
  180. // called when the `end` or `close` events are received.
  181. // @ts-expect-error `_isStdio` is available on `std{err,out}`
  182. if (!destination._isStdio && (!options || options.end !== false)) {
  183. emitter.on('end', onend)
  184. }
  185. destination.on('error', onerror)
  186. destination.on('close', cleanup)
  187. destination.emit('pipe', emitter)
  188. return destination
  189. /**
  190. * End destination stream.
  191. *
  192. * @returns {undefined}
  193. * Nothing.
  194. */
  195. function onend() {
  196. if (destination.end) {
  197. destination.end()
  198. }
  199. }
  200. /**
  201. * Handle data.
  202. *
  203. * @param {string} chunk
  204. * Data.
  205. * @returns {undefined}
  206. * Nothing.
  207. */
  208. function ondata(chunk) {
  209. if (destination.writable) {
  210. destination.write(chunk)
  211. }
  212. }
  213. /**
  214. * Clean listeners.
  215. *
  216. * @returns {undefined}
  217. * Nothing.
  218. */
  219. function cleanup() {
  220. emitter.removeListener('data', ondata)
  221. emitter.removeListener('end', onend)
  222. emitter.removeListener('error', onerror)
  223. emitter.removeListener('end', cleanup)
  224. emitter.removeListener('close', cleanup)
  225. destination.removeListener('error', onerror)
  226. destination.removeListener('close', cleanup)
  227. }
  228. /**
  229. * Close dangling pipes and handle unheard errors.
  230. *
  231. * @param {Error | null | undefined} [error]
  232. * Error, if any.
  233. * @returns {undefined}
  234. * Nothing.
  235. */
  236. function onerror(error) {
  237. cleanup()
  238. if (!emitter.listenerCount('error')) {
  239. throw error // Unhandled stream error in pipe.
  240. }
  241. }
  242. }
  243. }