index.js 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. 'use strict'
  2. /* Dependencies. */
  3. var extend = require('extend')
  4. var bail = require('bail')
  5. var vfile = require('vfile')
  6. var trough = require('trough')
  7. var string = require('x-is-string')
  8. var plain = require('is-plain-obj')
  9. /* Expose a frozen processor. */
  10. module.exports = unified().freeze()
  11. var slice = [].slice
  12. var own = {}.hasOwnProperty
  13. /* Process pipeline. */
  14. var pipeline = trough()
  15. .use(pipelineParse)
  16. .use(pipelineRun)
  17. .use(pipelineStringify)
  18. function pipelineParse(p, ctx) {
  19. ctx.tree = p.parse(ctx.file)
  20. }
  21. function pipelineRun(p, ctx, next) {
  22. p.run(ctx.tree, ctx.file, done)
  23. function done(err, tree, file) {
  24. if (err) {
  25. next(err)
  26. } else {
  27. ctx.tree = tree
  28. ctx.file = file
  29. next()
  30. }
  31. }
  32. }
  33. function pipelineStringify(p, ctx) {
  34. ctx.file.contents = p.stringify(ctx.tree, ctx.file)
  35. }
  36. /* Function to create the first processor. */
  37. function unified() {
  38. var attachers = []
  39. var transformers = trough()
  40. var namespace = {}
  41. var frozen = false
  42. var freezeIndex = -1
  43. /* Data management. */
  44. processor.data = data
  45. /* Lock. */
  46. processor.freeze = freeze
  47. /* Plug-ins. */
  48. processor.attachers = attachers
  49. processor.use = use
  50. /* API. */
  51. processor.parse = parse
  52. processor.stringify = stringify
  53. processor.run = run
  54. processor.runSync = runSync
  55. processor.process = process
  56. processor.processSync = processSync
  57. /* Expose. */
  58. return processor
  59. /* Create a new processor based on the processor
  60. * in the current scope. */
  61. function processor() {
  62. var destination = unified()
  63. var length = attachers.length
  64. var index = -1
  65. while (++index < length) {
  66. destination.use.apply(null, attachers[index])
  67. }
  68. destination.data(extend(true, {}, namespace))
  69. return destination
  70. }
  71. /* Freeze: used to signal a processor that has finished
  72. * configuration.
  73. *
  74. * For example, take unified itself. It’s frozen.
  75. * Plug-ins should not be added to it. Rather, it should
  76. * be extended, by invoking it, before modifying it.
  77. *
  78. * In essence, always invoke this when exporting a
  79. * processor. */
  80. function freeze() {
  81. var values
  82. var plugin
  83. var options
  84. var transformer
  85. if (frozen) {
  86. return processor
  87. }
  88. while (++freezeIndex < attachers.length) {
  89. values = attachers[freezeIndex]
  90. plugin = values[0]
  91. options = values[1]
  92. transformer = null
  93. if (options === false) {
  94. continue
  95. }
  96. if (options === true) {
  97. values[1] = undefined
  98. }
  99. transformer = plugin.apply(processor, values.slice(1))
  100. if (typeof transformer === 'function') {
  101. transformers.use(transformer)
  102. }
  103. }
  104. frozen = true
  105. freezeIndex = Infinity
  106. return processor
  107. }
  108. /* Data management.
  109. * Getter / setter for processor-specific informtion. */
  110. function data(key, value) {
  111. if (string(key)) {
  112. /* Set `key`. */
  113. if (arguments.length === 2) {
  114. assertUnfrozen('data', frozen)
  115. namespace[key] = value
  116. return processor
  117. }
  118. /* Get `key`. */
  119. return (own.call(namespace, key) && namespace[key]) || null
  120. }
  121. /* Set space. */
  122. if (key) {
  123. assertUnfrozen('data', frozen)
  124. namespace = key
  125. return processor
  126. }
  127. /* Get space. */
  128. return namespace
  129. }
  130. /* Plug-in management.
  131. *
  132. * Pass it:
  133. * * an attacher and options,
  134. * * a preset,
  135. * * a list of presets, attachers, and arguments (list
  136. * of attachers and options). */
  137. function use(value) {
  138. var settings
  139. assertUnfrozen('use', frozen)
  140. if (value === null || value === undefined) {
  141. /* Empty */
  142. } else if (typeof value === 'function') {
  143. addPlugin.apply(null, arguments)
  144. } else if (typeof value === 'object') {
  145. if ('length' in value) {
  146. addList(value)
  147. } else {
  148. addPreset(value)
  149. }
  150. } else {
  151. throw new Error('Expected usable value, not `' + value + '`')
  152. }
  153. if (settings) {
  154. namespace.settings = extend(namespace.settings || {}, settings)
  155. }
  156. return processor
  157. function addPreset(result) {
  158. addList(result.plugins)
  159. if (result.settings) {
  160. settings = extend(settings || {}, result.settings)
  161. }
  162. }
  163. function add(value) {
  164. if (typeof value === 'function') {
  165. addPlugin(value)
  166. } else if (typeof value === 'object') {
  167. if ('length' in value) {
  168. addPlugin.apply(null, value)
  169. } else {
  170. addPreset(value)
  171. }
  172. } else {
  173. throw new Error('Expected usable value, not `' + value + '`')
  174. }
  175. }
  176. function addList(plugins) {
  177. var length
  178. var index
  179. if (plugins === null || plugins === undefined) {
  180. /* Empty */
  181. } else if (typeof plugins === 'object' && 'length' in plugins) {
  182. length = plugins.length
  183. index = -1
  184. while (++index < length) {
  185. add(plugins[index])
  186. }
  187. } else {
  188. throw new Error('Expected a list of plugins, not `' + plugins + '`')
  189. }
  190. }
  191. function addPlugin(plugin, value) {
  192. var entry = find(plugin)
  193. if (entry) {
  194. if (plain(entry[1]) && plain(value)) {
  195. value = extend(entry[1], value)
  196. }
  197. entry[1] = value
  198. } else {
  199. attachers.push(slice.call(arguments))
  200. }
  201. }
  202. }
  203. function find(plugin) {
  204. var length = attachers.length
  205. var index = -1
  206. var entry
  207. while (++index < length) {
  208. entry = attachers[index]
  209. if (entry[0] === plugin) {
  210. return entry
  211. }
  212. }
  213. }
  214. /* Parse a file (in string or VFile representation)
  215. * into a Unist node using the `Parser` on the
  216. * processor. */
  217. function parse(doc) {
  218. var file = vfile(doc)
  219. var Parser
  220. freeze()
  221. Parser = processor.Parser
  222. assertParser('parse', Parser)
  223. if (newable(Parser)) {
  224. return new Parser(String(file), file).parse()
  225. }
  226. return Parser(String(file), file) // eslint-disable-line new-cap
  227. }
  228. /* Run transforms on a Unist node representation of a file
  229. * (in string or VFile representation), async. */
  230. function run(node, file, cb) {
  231. assertNode(node)
  232. freeze()
  233. if (!cb && typeof file === 'function') {
  234. cb = file
  235. file = null
  236. }
  237. if (!cb) {
  238. return new Promise(executor)
  239. }
  240. executor(null, cb)
  241. function executor(resolve, reject) {
  242. transformers.run(node, vfile(file), done)
  243. function done(err, tree, file) {
  244. tree = tree || node
  245. if (err) {
  246. reject(err)
  247. } else if (resolve) {
  248. resolve(tree)
  249. } else {
  250. cb(null, tree, file)
  251. }
  252. }
  253. }
  254. }
  255. /* Run transforms on a Unist node representation of a file
  256. * (in string or VFile representation), sync. */
  257. function runSync(node, file) {
  258. var complete = false
  259. var result
  260. run(node, file, done)
  261. assertDone('runSync', 'run', complete)
  262. return result
  263. function done(err, tree) {
  264. complete = true
  265. bail(err)
  266. result = tree
  267. }
  268. }
  269. /* Stringify a Unist node representation of a file
  270. * (in string or VFile representation) into a string
  271. * using the `Compiler` on the processor. */
  272. function stringify(node, doc) {
  273. var file = vfile(doc)
  274. var Compiler
  275. freeze()
  276. Compiler = processor.Compiler
  277. assertCompiler('stringify', Compiler)
  278. assertNode(node)
  279. if (newable(Compiler)) {
  280. return new Compiler(node, file).compile()
  281. }
  282. return Compiler(node, file) // eslint-disable-line new-cap
  283. }
  284. /* Parse a file (in string or VFile representation)
  285. * into a Unist node using the `Parser` on the processor,
  286. * then run transforms on that node, and compile the
  287. * resulting node using the `Compiler` on the processor,
  288. * and store that result on the VFile. */
  289. function process(doc, cb) {
  290. freeze()
  291. assertParser('process', processor.Parser)
  292. assertCompiler('process', processor.Compiler)
  293. if (!cb) {
  294. return new Promise(executor)
  295. }
  296. executor(null, cb)
  297. function executor(resolve, reject) {
  298. var file = vfile(doc)
  299. pipeline.run(processor, {file: file}, done)
  300. function done(err) {
  301. if (err) {
  302. reject(err)
  303. } else if (resolve) {
  304. resolve(file)
  305. } else {
  306. cb(null, file)
  307. }
  308. }
  309. }
  310. }
  311. /* Process the given document (in string or VFile
  312. * representation), sync. */
  313. function processSync(doc) {
  314. var complete = false
  315. var file
  316. freeze()
  317. assertParser('processSync', processor.Parser)
  318. assertCompiler('processSync', processor.Compiler)
  319. file = vfile(doc)
  320. process(file, done)
  321. assertDone('processSync', 'process', complete)
  322. return file
  323. function done(err) {
  324. complete = true
  325. bail(err)
  326. }
  327. }
  328. }
  329. /* Check if `func` is a constructor. */
  330. function newable(value) {
  331. return typeof value === 'function' && keys(value.prototype)
  332. }
  333. /* Check if `value` is an object with keys. */
  334. function keys(value) {
  335. var key
  336. for (key in value) {
  337. return true
  338. }
  339. return false
  340. }
  341. /* Assert a parser is available. */
  342. function assertParser(name, Parser) {
  343. if (typeof Parser !== 'function') {
  344. throw new Error('Cannot `' + name + '` without `Parser`')
  345. }
  346. }
  347. /* Assert a compiler is available. */
  348. function assertCompiler(name, Compiler) {
  349. if (typeof Compiler !== 'function') {
  350. throw new Error('Cannot `' + name + '` without `Compiler`')
  351. }
  352. }
  353. /* Assert the processor is not frozen. */
  354. function assertUnfrozen(name, frozen) {
  355. if (frozen) {
  356. throw new Error(
  357. [
  358. 'Cannot invoke `' + name + '` on a frozen processor.\nCreate a new ',
  359. 'processor first, by invoking it: use `processor()` instead of ',
  360. '`processor`.'
  361. ].join('')
  362. )
  363. }
  364. }
  365. /* Assert `node` is a Unist node. */
  366. function assertNode(node) {
  367. if (!node || !string(node.type)) {
  368. throw new Error('Expected node, got `' + node + '`')
  369. }
  370. }
  371. /* Assert that `complete` is `true`. */
  372. function assertDone(name, asyncName, complete) {
  373. if (!complete) {
  374. throw new Error(
  375. '`' + name + '` finished async. Use `' + asyncName + '` instead'
  376. )
  377. }
  378. }