index.js 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. var pkg = require('../package.json')
  2. var api = require('./api.js')
  3. var parser = require('posthtml-parser')
  4. var render = require('posthtml-render')
  5. /**
  6. * @author Ivan Voischev (@voischev),
  7. * Anton Winogradov (@awinogradov),
  8. * Alexej Yaroshevich (@zxqfox),
  9. * Vasiliy (@Yeti-or)
  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. function PostHTML (plugins) {
  19. /**
  20. * PostHTML Instance
  21. *
  22. * @prop plugins
  23. * @prop options
  24. */
  25. this.version = pkg.version
  26. this.name = pkg.name
  27. this.plugins = typeof plugins === 'function' ? [plugins] : plugins || []
  28. }
  29. /**
  30. * @requires posthtml-parser
  31. *
  32. * @param {String} html - Input (HTML)
  33. * @returns {Array} tree - PostHTMLTree (JSON)
  34. */
  35. PostHTML.parser = parser
  36. /**
  37. * @requires posthtml-render
  38. *
  39. * @param {Array} tree - PostHTMLTree (JSON)
  40. * @returns {String} html - HTML
  41. */
  42. PostHTML.render = render
  43. /**
  44. * @this posthtml
  45. * @param {Function} plugin - A PostHTML plugin
  46. * @returns {Constructor} - this(PostHTML)
  47. *
  48. * **Usage**
  49. * ```js
  50. * ph.use((tree) => { tag: 'div', content: tree })
  51. * .process('<html>..</html>', {})
  52. * .then((result) => result))
  53. * ```
  54. */
  55. PostHTML.prototype.use = function () {
  56. [].push.apply(this.plugins, arguments)
  57. return this
  58. }
  59. /**
  60. * @param {String} html - Input (HTML)
  61. * @param {?Object} options - PostHTML Options
  62. * @returns {Object<{html: String, tree: PostHTMLTree}>} - Sync Mode
  63. * @returns {Promise<{html: String, tree: PostHTMLTree}>} - Async Mode (default)
  64. *
  65. * **Usage**
  66. *
  67. * **Sync**
  68. * ```js
  69. * ph.process('<html>..</html>', { sync: true }).html
  70. * ```
  71. *
  72. * **Async**
  73. * ```js
  74. * ph.process('<html>..</html>', {}).then((result) => result))
  75. * ```
  76. */
  77. PostHTML.prototype.process = function (tree, options) {
  78. /**
  79. * ## PostHTML Options
  80. *
  81. * @type {Object}
  82. * @prop {?Boolean} options.sync - enables sync mode, plugins will run synchronously, throws an error when used with async plugins
  83. * @prop {?Function} options.parser - use custom parser, replaces default (posthtml-parser)
  84. * @prop {?Function} options.render - use custom render, replaces default (posthtml-render)
  85. * @prop {?Boolean} options.skipParse - disable parsing
  86. */
  87. options = options || {}
  88. if (options.parser) parser = options.parser
  89. if (options.render) render = options.render
  90. tree = options.skipParse ? tree : parser(tree)
  91. tree.options = options
  92. tree.processor = this
  93. // sync mode
  94. if (options.sync === true) {
  95. this.plugins.forEach(function (plugin) {
  96. apiExtend(tree)
  97. var result
  98. if (plugin.length === 2 || isPromise(result = plugin(tree))) {
  99. throw new Error(
  100. 'Can’t process contents in sync mode because of async plugin: ' + plugin.name
  101. )
  102. }
  103. // return the previous tree unless result is fulfilled
  104. tree = result || tree
  105. })
  106. return lazyResult(render, tree)
  107. }
  108. // async mode
  109. var i = 0
  110. var next = function (result, cb) {
  111. // all plugins called
  112. if (this.plugins.length <= i) {
  113. cb(null, result)
  114. return
  115. }
  116. // little helper to go to the next iteration
  117. function _next (res) {
  118. return next(res || result, cb)
  119. }
  120. // (re)extend the object
  121. apiExtend(result)
  122. // call next
  123. var plugin = this.plugins[i++]
  124. if (plugin.length === 2) {
  125. plugin(result, function (err, res) {
  126. if (err) return cb(err)
  127. _next(res)
  128. })
  129. return
  130. }
  131. // sync and promised plugins
  132. var err = null
  133. var res = tryCatch(function () {
  134. return plugin(result)
  135. }, function (err) {
  136. return err
  137. })
  138. if (err) {
  139. cb(err)
  140. return
  141. }
  142. if (isPromise(res)) {
  143. res.then(_next).catch(cb)
  144. return
  145. }
  146. _next(res)
  147. }.bind(this)
  148. return new Promise(function (resolve, reject) {
  149. next(tree, function (err, tree) {
  150. if (err) reject(err)
  151. else resolve(lazyResult(render, tree))
  152. })
  153. })
  154. }
  155. /**
  156. * @exports posthtml
  157. *
  158. * @param {Array} plugins
  159. * @return {Function} posthtml
  160. *
  161. * **Usage**
  162. * ```js
  163. * import posthtml from 'posthtml'
  164. * import plugin from 'posthtml-plugin'
  165. *
  166. * const ph = posthtml([ plugin() ])
  167. * ```
  168. */
  169. module.exports = function (plugins) {
  170. return new PostHTML(plugins)
  171. }
  172. /**
  173. * Checks if parameter is a Promise (or thenable) object.
  174. *
  175. * @private
  176. *
  177. * @param {*} promise - Target `{}` to test
  178. * @returns {Boolean}
  179. */
  180. function isPromise (promise) {
  181. return !!promise && typeof promise.then === 'function'
  182. }
  183. /**
  184. * Simple try/catch helper, if exists, returns result
  185. *
  186. * @private
  187. *
  188. * @param {Function} tryFn - try block
  189. * @param {Function} catchFn - catch block
  190. * @returns {?*}
  191. */
  192. function tryCatch (tryFn, catchFn) {
  193. try {
  194. return tryFn()
  195. } catch (err) {
  196. catchFn(err)
  197. }
  198. }
  199. /**
  200. * Extends the PostHTMLTree with the Tree API
  201. *
  202. * @private
  203. *
  204. * @param {Array} tree - PostHTMLTree
  205. * @returns {Array} tree - PostHTMLTree with API
  206. */
  207. function apiExtend (tree) {
  208. tree.walk = api.walk
  209. tree.match = api.match
  210. }
  211. /**
  212. * Wraps the PostHTMLTree within an object using a getter to render HTML on demand.
  213. *
  214. * @private
  215. *
  216. * @param {Function} render
  217. * @param {Array} tree
  218. * @returns {Object<{html: String, tree: Array}>}
  219. */
  220. function lazyResult (render, tree) {
  221. return {
  222. get html () {
  223. return render(tree, tree.options)
  224. },
  225. tree: tree
  226. }
  227. }