1 | const pkg = require('../package.json')
|
2 | const Api = require('./api.js')
|
3 |
|
4 | let parser = require('posthtml-parser')
|
5 | let 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 | */
|
18 | class 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 | */
|
258 | module.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 | */
|
269 | function _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 | */
|
283 | function 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 | */
|
296 | function 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 | */
|
313 | function lazyResult (render, tree) {
|
314 | return {
|
315 | get html () {
|
316 | return render(tree, tree.options)
|
317 | },
|
318 | tree,
|
319 | messages: tree.messages
|
320 | }
|
321 | }
|