UNPKG

7.33 kBJavaScriptView Raw
1const pkg = require('../package.json')
2const Api = require('./api.js')
3
4let parser = require('posthtml-parser')
5let render = require('posthtml-render')
6
7/**
8 * @author Ivan Voischev (@voischev),
9 * Ivan Demidov (@scrum)
10 *
11 * @requires api
12 * @requires posthtml-parser
13 * @requires posthtml-render
14 *
15 * @constructor PostHTML
16 * @param {Array} plugins - An array of PostHTML plugins
17 */
18class PostHTML {
19 constructor (plugins) {
20 /**
21 * PostHTML Instance
22 *
23 * @prop plugins
24 * @prop options
25 */
26 this.version = pkg.version
27 this.name = pkg.name
28 this.plugins = typeof plugins === 'function' ? [plugins] : plugins || []
29 this.source = ''
30
31 /**
32 * Tree messages to store and pass metadata between plugins
33 *
34 * @memberof tree
35 * @type {Array} messages
36 *
37 * @example
38 * ```js
39 * export default function plugin (options = {}) {
40 * return function (tree) {
41 * tree.messages.push({
42 * type: 'dependency',
43 * file: 'path/to/dependency.html',
44 * from: tree.options.from
45 * })
46 *
47 * return tree
48 * }
49 * }
50 * ```
51 */
52 this.messages = []
53
54 /**
55 * Tree method parsing string inside plugins.
56 *
57 * @memberof tree
58 * @type {Function} parser
59 *
60 * @example
61 * ```js
62 * export default function plugin (options = {}) {
63 * return function (tree) {
64 * tree.match({ tag: 'include' }, function(node) {
65 * node.tag = false;
66 * node.content = tree.parser(fs.readFileSync(node.attr.src))
67 * return node
68 * })
69 *
70 * return tree
71 * }
72 * }
73 * ```
74 */
75 this.parser = parser
76
77 /**
78 * Tree method rendering tree to string inside plugins.
79 *
80 * @memberof tree
81 * @type {Function} render
82 *
83 * @example
84 * ```js
85 * export default function plugin (options = {}) {
86 * return function (tree) {
87 * var outherTree = ['\n', {tag: 'div', content: ['1']}, '\n\t', {tag: 'div', content: ['2']}, '\n'];
88 * var htmlWitchoutSpaceless = tree.render(outherTree).replace(/[\n|\t]/g, '');
89 * return tree.parser(htmlWitchoutSpaceless)
90 * }
91 * }
92 * ```
93 */
94 this.render = render
95
96 // extend api methods
97 Api.call(this)
98 }
99
100 /**
101 * @this posthtml
102 * @param {Function} plugin - A PostHTML plugin
103 * @returns {Constructor} - this(PostHTML)
104 *
105 * **Usage**
106 * ```js
107 * ph.use((tree) => { tag: 'div', content: tree })
108 * .process('<html>..</html>', {})
109 * .then((result) => result))
110 * ```
111 */
112 use (...args) {
113 this.plugins.push(...args)
114
115 return this
116 }
117
118 /**
119 * @param {String} html - Input (HTML)
120 * @param {?Object} options - PostHTML Options
121 * @returns {Object<{html: String, tree: PostHTMLTree}>} - Sync Mode
122 * @returns {Promise<{html: String, tree: PostHTMLTree}>} - Async Mode (default)
123 *
124 * **Usage**
125 *
126 * **Sync**
127 * ```js
128 * ph.process('<html>..</html>', { sync: true }).html
129 * ```
130 *
131 * **Async**
132 * ```js
133 * ph.process('<html>..</html>', {}).then((result) => result))
134 * ```
135 */
136 process (tree, options = {}) {
137 /**
138 * ## PostHTML Options
139 *
140 * @type {Object}
141 * @prop {?Boolean} options.sync - enables sync mode, plugins will run synchronously, throws an error when used with async plugins
142 * @prop {?Function} options.parser - use custom parser, replaces default (posthtml-parser)
143 * @prop {?Function} options.render - use custom render, replaces default (posthtml-render)
144 * @prop {?Boolean} options.skipParse - disable parsing
145 * @prop {?Array} options.directives - Adds processing of custom [directives](https://github.com/posthtml/posthtml-parser#directives).
146 */
147 this.options = options
148 this.source = tree
149
150 if (options.parser) parser = this.parser = options.parser
151 if (options.render) render = this.render = options.render
152
153 tree = options.skipParse
154 ? tree || []
155 : parser(tree, options)
156
157 // sync mode
158 if (options.sync === true) {
159 this.plugins.forEach((plugin, index) => {
160 _treeExtendApi(tree, this)
161
162 let result
163
164 if (plugin.length === 2 || isPromise(result = plugin(tree))) {
165 throw new Error(
166 `Can’t process contents in sync mode because of async plugin: ${plugin.name}`
167 )
168 }
169
170 // clearing the tree of options
171 if (index !== this.plugins.length - 1 && !options.skipParse) {
172 tree = [].concat(tree)
173 }
174
175 // return the previous tree unless result is fulfilled
176 tree = result || tree
177 })
178
179 return lazyResult(render, tree)
180 }
181
182 // async mode
183 let i = 0
184
185 const next = (result, cb) => {
186 _treeExtendApi(result, this)
187
188 // all plugins called
189 if (this.plugins.length <= i) {
190 cb(null, result)
191 return
192 }
193
194 // little helper to go to the next iteration
195 function _next (res) {
196 if (res && !options.skipParse) {
197 res = [].concat(res)
198 }
199
200 return next(res || result, cb)
201 }
202
203 // call next
204 const plugin = this.plugins[i++]
205
206 if (plugin.length === 2) {
207 plugin(result, (err, res) => {
208 if (err) return cb(err)
209 _next(res)
210 })
211 return
212 }
213
214 // sync and promised plugins
215 let err = null
216
217 const res = tryCatch(() => plugin(result), e => {
218 err = e
219 return e
220 })
221
222 if (err) {
223 cb(err)
224 return
225 }
226
227 if (isPromise(res)) {
228 res.then(_next).catch(cb)
229 return
230 }
231
232 _next(res)
233 }
234
235 return new Promise((resolve, reject) => {
236 next(tree, (err, tree) => {
237 if (err) reject(err)
238 else resolve(lazyResult(render, tree))
239 })
240 })
241 }
242}
243
244/**
245 * @exports posthtml
246 *
247 * @param {Array} plugins
248 * @return {Function} posthtml
249 *
250 * **Usage**
251 * ```js
252 * import posthtml from 'posthtml'
253 * import plugin from 'posthtml-plugin'
254 *
255 * const ph = posthtml([ plugin() ])
256 * ```
257 */
258module.exports = plugins => new PostHTML(plugins)
259
260/**
261 * Extension of options tree
262 *
263 * @private
264 *
265 * @param {Array} tree
266 * @param {Object} PostHTML
267 * @returns {?*}
268 */
269function _treeExtendApi (t, _t) {
270 if (typeof t === 'object') {
271 t = Object.assign(t, _t)
272 }
273}
274
275/**
276 * Checks if parameter is a Promise (or thenable) object.
277 *
278 * @private
279 *
280 * @param {*} promise - Target `{}` to test
281 * @returns {Boolean}
282 */
283function isPromise (promise) {
284 return !!promise && typeof promise.then === 'function'
285}
286
287/**
288 * Simple try/catch helper, if exists, returns result
289 *
290 * @private
291 *
292 * @param {Function} tryFn - try block
293 * @param {Function} catchFn - catch block
294 * @returns {?*}
295 */
296function tryCatch (tryFn, catchFn) {
297 try {
298 return tryFn()
299 } catch (err) {
300 catchFn(err)
301 }
302}
303
304/**
305 * Wraps the PostHTMLTree within an object using a getter to render HTML on demand.
306 *
307 * @private
308 *
309 * @param {Function} render
310 * @param {Array} tree
311 * @returns {Object<{html: String, tree: Array}>}
312 */
313function lazyResult (render, tree) {
314 return {
315 get html () {
316 return render(tree, tree.options)
317 },
318 tree,
319 messages: tree.messages
320 }
321}