index.js 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. 'use strict'
  2. var toString = require('mdast-util-to-string')
  3. function findIndex (array, fn) {
  4. for (var i = 0; i < array.length; i++) {
  5. if (fn(array[i], i)) {
  6. return i
  7. }
  8. }
  9. return -1
  10. }
  11. module.exports = inject
  12. /**
  13. * Inject some markdown into some other markdown at a desired heading. Heading
  14. * levels in the source markdown are adjusted to match the target document
  15. * based on the target heading's level. targetAst is modified in place
  16. *
  17. * @param {string} targetHeadingText The heading to look for in the target ast
  18. * @param {object} targetAst The target markdown document, as an mdast
  19. * @param {object} toInjectAst The source markdown to be injected into the target, also as an mdast.
  20. * @return {boolean} whether the specified section was found and content inserted
  21. * @example
  22. * var mdast = require('mdast')
  23. * var inject = require('mdast-util-inject')
  24. *
  25. * var target = mdast.parse('# A document\n## Section1\nBlah\n## Section2\nBlargh')
  26. * var newStuff = mdast.parse('# Some other document\nwith some content')
  27. * inject('Section1', target, newStuff)
  28. *
  29. * console.log(mdast.stringify(target))
  30. * // outputs:
  31. * // # A document
  32. * //
  33. * // ## Section1
  34. * //
  35. * // ### Some other document
  36. * //
  37. * // with some content
  38. * //
  39. * // ## Section2
  40. * //
  41. * // Blargh
  42. */
  43. function inject (targetHeadingText, targetAst, toInjectAst) {
  44. // find the heading after which to inject the new content
  45. var head = findIndex(targetAst.children, function (node) {
  46. return isHeading(node, targetHeadingText)
  47. })
  48. if (head === -1) {
  49. return false
  50. }
  51. // find the next heading at the same heading level, which is where we'll
  52. // STOP inserting
  53. var depth = targetAst.children[head].depth
  54. var nextHead = findIndex(targetAst.children, function (node, i) {
  55. return isHeading(node, false, depth) && i > head
  56. })
  57. // bump heading levels so they fall within the parent documents' heirarchy
  58. bumpHeadings(toInjectAst, depth)
  59. // insert content
  60. targetAst.children.splice.apply(targetAst.children, [
  61. head + 1, // start splice
  62. (nextHead >= 0 ? nextHead - head : targetAst.children.length - head) - 1 // items to delete
  63. ].concat(toInjectAst.children))
  64. return true
  65. }
  66. /*
  67. * Test if the given node is a heading, optionally with the given text,
  68. * or <= the given depth
  69. */
  70. function isHeading (node, text, depth) {
  71. if (node.type !== 'heading') {
  72. return false
  73. }
  74. if (text) {
  75. var headingText = toString(node)
  76. // TODO: more flexible match?
  77. return text.trim().toLowerCase() === headingText.trim().toLowerCase()
  78. }
  79. if (depth) {
  80. return node.depth <= depth
  81. }
  82. return true
  83. }
  84. var MAX_HEADING_DEPTH = 99999
  85. function bumpHeadings (root, baseDepth) {
  86. var headings = []
  87. walk(root, function (node) {
  88. if (node.type === 'heading') {
  89. headings.push(node)
  90. }
  91. })
  92. var minDepth = headings.reduce(function (memo, h) {
  93. return Math.min(memo, h.depth)
  94. }, MAX_HEADING_DEPTH)
  95. var diff = baseDepth + 1 - minDepth
  96. headings.forEach(function (h) {
  97. h.depth += diff
  98. })
  99. }
  100. function walk (node, fn) {
  101. fn(node)
  102. if (node.children) {
  103. node.children.forEach(function (n) {
  104. walk(n, fn)
  105. })
  106. }
  107. }