ruler.mjs 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340
  1. /**
  2. * class Ruler
  3. *
  4. * Helper class, used by [[MarkdownIt#core]], [[MarkdownIt#block]] and
  5. * [[MarkdownIt#inline]] to manage sequences of functions (rules):
  6. *
  7. * - keep rules in defined order
  8. * - assign the name to each rule
  9. * - enable/disable rules
  10. * - add/replace rules
  11. * - allow assign rules to additional named chains (in the same)
  12. * - cacheing lists of active rules
  13. *
  14. * You will not need use this class directly until write plugins. For simple
  15. * rules control use [[MarkdownIt.disable]], [[MarkdownIt.enable]] and
  16. * [[MarkdownIt.use]].
  17. **/
  18. /**
  19. * new Ruler()
  20. **/
  21. function Ruler () {
  22. // List of added rules. Each element is:
  23. //
  24. // {
  25. // name: XXX,
  26. // enabled: Boolean,
  27. // fn: Function(),
  28. // alt: [ name2, name3 ]
  29. // }
  30. //
  31. this.__rules__ = []
  32. // Cached rule chains.
  33. //
  34. // First level - chain name, '' for default.
  35. // Second level - diginal anchor for fast filtering by charcodes.
  36. //
  37. this.__cache__ = null
  38. }
  39. // Helper methods, should not be used directly
  40. // Find rule index by name
  41. //
  42. Ruler.prototype.__find__ = function (name) {
  43. for (let i = 0; i < this.__rules__.length; i++) {
  44. if (this.__rules__[i].name === name) {
  45. return i
  46. }
  47. }
  48. return -1
  49. }
  50. // Build rules lookup cache
  51. //
  52. Ruler.prototype.__compile__ = function () {
  53. const self = this
  54. const chains = ['']
  55. // collect unique names
  56. self.__rules__.forEach(function (rule) {
  57. if (!rule.enabled) { return }
  58. rule.alt.forEach(function (altName) {
  59. if (chains.indexOf(altName) < 0) {
  60. chains.push(altName)
  61. }
  62. })
  63. })
  64. self.__cache__ = {}
  65. chains.forEach(function (chain) {
  66. self.__cache__[chain] = []
  67. self.__rules__.forEach(function (rule) {
  68. if (!rule.enabled) { return }
  69. if (chain && rule.alt.indexOf(chain) < 0) { return }
  70. self.__cache__[chain].push(rule.fn)
  71. })
  72. })
  73. }
  74. /**
  75. * Ruler.at(name, fn [, options])
  76. * - name (String): rule name to replace.
  77. * - fn (Function): new rule function.
  78. * - options (Object): new rule options (not mandatory).
  79. *
  80. * Replace rule by name with new function & options. Throws error if name not
  81. * found.
  82. *
  83. * ##### Options:
  84. *
  85. * - __alt__ - array with names of "alternate" chains.
  86. *
  87. * ##### Example
  88. *
  89. * Replace existing typographer replacement rule with new one:
  90. *
  91. * ```javascript
  92. * var md = require('markdown-it')();
  93. *
  94. * md.core.ruler.at('replacements', function replace(state) {
  95. * //...
  96. * });
  97. * ```
  98. **/
  99. Ruler.prototype.at = function (name, fn, options) {
  100. const index = this.__find__(name)
  101. const opt = options || {}
  102. if (index === -1) { throw new Error('Parser rule not found: ' + name) }
  103. this.__rules__[index].fn = fn
  104. this.__rules__[index].alt = opt.alt || []
  105. this.__cache__ = null
  106. }
  107. /**
  108. * Ruler.before(beforeName, ruleName, fn [, options])
  109. * - beforeName (String): new rule will be added before this one.
  110. * - ruleName (String): name of added rule.
  111. * - fn (Function): rule function.
  112. * - options (Object): rule options (not mandatory).
  113. *
  114. * Add new rule to chain before one with given name. See also
  115. * [[Ruler.after]], [[Ruler.push]].
  116. *
  117. * ##### Options:
  118. *
  119. * - __alt__ - array with names of "alternate" chains.
  120. *
  121. * ##### Example
  122. *
  123. * ```javascript
  124. * var md = require('markdown-it')();
  125. *
  126. * md.block.ruler.before('paragraph', 'my_rule', function replace(state) {
  127. * //...
  128. * });
  129. * ```
  130. **/
  131. Ruler.prototype.before = function (beforeName, ruleName, fn, options) {
  132. const index = this.__find__(beforeName)
  133. const opt = options || {}
  134. if (index === -1) { throw new Error('Parser rule not found: ' + beforeName) }
  135. this.__rules__.splice(index, 0, {
  136. name: ruleName,
  137. enabled: true,
  138. fn,
  139. alt: opt.alt || []
  140. })
  141. this.__cache__ = null
  142. }
  143. /**
  144. * Ruler.after(afterName, ruleName, fn [, options])
  145. * - afterName (String): new rule will be added after this one.
  146. * - ruleName (String): name of added rule.
  147. * - fn (Function): rule function.
  148. * - options (Object): rule options (not mandatory).
  149. *
  150. * Add new rule to chain after one with given name. See also
  151. * [[Ruler.before]], [[Ruler.push]].
  152. *
  153. * ##### Options:
  154. *
  155. * - __alt__ - array with names of "alternate" chains.
  156. *
  157. * ##### Example
  158. *
  159. * ```javascript
  160. * var md = require('markdown-it')();
  161. *
  162. * md.inline.ruler.after('text', 'my_rule', function replace(state) {
  163. * //...
  164. * });
  165. * ```
  166. **/
  167. Ruler.prototype.after = function (afterName, ruleName, fn, options) {
  168. const index = this.__find__(afterName)
  169. const opt = options || {}
  170. if (index === -1) { throw new Error('Parser rule not found: ' + afterName) }
  171. this.__rules__.splice(index + 1, 0, {
  172. name: ruleName,
  173. enabled: true,
  174. fn,
  175. alt: opt.alt || []
  176. })
  177. this.__cache__ = null
  178. }
  179. /**
  180. * Ruler.push(ruleName, fn [, options])
  181. * - ruleName (String): name of added rule.
  182. * - fn (Function): rule function.
  183. * - options (Object): rule options (not mandatory).
  184. *
  185. * Push new rule to the end of chain. See also
  186. * [[Ruler.before]], [[Ruler.after]].
  187. *
  188. * ##### Options:
  189. *
  190. * - __alt__ - array with names of "alternate" chains.
  191. *
  192. * ##### Example
  193. *
  194. * ```javascript
  195. * var md = require('markdown-it')();
  196. *
  197. * md.core.ruler.push('my_rule', function replace(state) {
  198. * //...
  199. * });
  200. * ```
  201. **/
  202. Ruler.prototype.push = function (ruleName, fn, options) {
  203. const opt = options || {}
  204. this.__rules__.push({
  205. name: ruleName,
  206. enabled: true,
  207. fn,
  208. alt: opt.alt || []
  209. })
  210. this.__cache__ = null
  211. }
  212. /**
  213. * Ruler.enable(list [, ignoreInvalid]) -> Array
  214. * - list (String|Array): list of rule names to enable.
  215. * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
  216. *
  217. * Enable rules with given names. If any rule name not found - throw Error.
  218. * Errors can be disabled by second param.
  219. *
  220. * Returns list of found rule names (if no exception happened).
  221. *
  222. * See also [[Ruler.disable]], [[Ruler.enableOnly]].
  223. **/
  224. Ruler.prototype.enable = function (list, ignoreInvalid) {
  225. if (!Array.isArray(list)) { list = [list] }
  226. const result = []
  227. // Search by name and enable
  228. list.forEach(function (name) {
  229. const idx = this.__find__(name)
  230. if (idx < 0) {
  231. if (ignoreInvalid) { return }
  232. throw new Error('Rules manager: invalid rule name ' + name)
  233. }
  234. this.__rules__[idx].enabled = true
  235. result.push(name)
  236. }, this)
  237. this.__cache__ = null
  238. return result
  239. }
  240. /**
  241. * Ruler.enableOnly(list [, ignoreInvalid])
  242. * - list (String|Array): list of rule names to enable (whitelist).
  243. * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
  244. *
  245. * Enable rules with given names, and disable everything else. If any rule name
  246. * not found - throw Error. Errors can be disabled by second param.
  247. *
  248. * See also [[Ruler.disable]], [[Ruler.enable]].
  249. **/
  250. Ruler.prototype.enableOnly = function (list, ignoreInvalid) {
  251. if (!Array.isArray(list)) { list = [list] }
  252. this.__rules__.forEach(function (rule) { rule.enabled = false })
  253. this.enable(list, ignoreInvalid)
  254. }
  255. /**
  256. * Ruler.disable(list [, ignoreInvalid]) -> Array
  257. * - list (String|Array): list of rule names to disable.
  258. * - ignoreInvalid (Boolean): set `true` to ignore errors when rule not found.
  259. *
  260. * Disable rules with given names. If any rule name not found - throw Error.
  261. * Errors can be disabled by second param.
  262. *
  263. * Returns list of found rule names (if no exception happened).
  264. *
  265. * See also [[Ruler.enable]], [[Ruler.enableOnly]].
  266. **/
  267. Ruler.prototype.disable = function (list, ignoreInvalid) {
  268. if (!Array.isArray(list)) { list = [list] }
  269. const result = []
  270. // Search by name and disable
  271. list.forEach(function (name) {
  272. const idx = this.__find__(name)
  273. if (idx < 0) {
  274. if (ignoreInvalid) { return }
  275. throw new Error('Rules manager: invalid rule name ' + name)
  276. }
  277. this.__rules__[idx].enabled = false
  278. result.push(name)
  279. }, this)
  280. this.__cache__ = null
  281. return result
  282. }
  283. /**
  284. * Ruler.getRules(chainName) -> Array
  285. *
  286. * Return array of active functions (rules) for given chain name. It analyzes
  287. * rules configuration, compiles caches if not exists and returns result.
  288. *
  289. * Default chain name is `''` (empty string). It can't be skipped. That's
  290. * done intentionally, to keep signature monomorphic for high speed.
  291. **/
  292. Ruler.prototype.getRules = function (chainName) {
  293. if (this.__cache__ === null) {
  294. this.__compile__()
  295. }
  296. // Chain can be empty, if rules disabled. But we still have to return Array.
  297. return this.__cache__[chainName] || []
  298. }
  299. export default Ruler