UNPKG

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