outline-parser.js 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. const COMMAND_LEVELS = {
  2. book: 10,
  3. part: 20,
  4. addpart: 20,
  5. chapter: 30,
  6. addchap: 30,
  7. section: 40,
  8. addsect: 40,
  9. subsection: 50,
  10. subsubsection: 60,
  11. paragraph: 70,
  12. subparagraph: 80
  13. }
  14. /*
  15. *
  16. * RegExp matcher parts:
  17. *
  18. * REGEX_START: begining of line, any number of spaces, double \ (required)
  19. * REGEX_COMMAND: any of the listed commands (required)
  20. * REGEX_SPACING: spaces and * between groups (optional)
  21. * REGEX_SHORT_TITLE: a text between square brackets (optional)
  22. * REGEX_TITLE: a text between curly brackets (required)
  23. *
  24. */
  25. const REGEX_START = '^\\s*\\\\'
  26. const REGEX_COMMAND = `(${Object.keys(COMMAND_LEVELS).join('|')})`
  27. const REGEX_SPACING = '\\s?\\*?\\s?'
  28. const REGEX_SHORT_TITLE = '(\\[([^\\]]+)\\])?'
  29. const REGEX_TITLE = '{(.*)}'
  30. const MATCHER = new RegExp(
  31. `${REGEX_START}${REGEX_COMMAND}${REGEX_SPACING}${REGEX_SHORT_TITLE}${REGEX_SPACING}${REGEX_TITLE}`
  32. )
  33. function matchOutline(content) {
  34. const lines = content.split('\n')
  35. const flatOutline = []
  36. lines.forEach((line, lineId) => {
  37. const match = line.match(MATCHER)
  38. if (!match) return
  39. const [, command, , shortTitle, title] = match
  40. flatOutline.push({
  41. line: lineId + 1,
  42. title: matchDisplayTitle(shortTitle || title),
  43. level: COMMAND_LEVELS[command]
  44. })
  45. })
  46. return flatOutline
  47. }
  48. const DISPLAY_TITLE_REGEX = new RegExp('([^\\\\]*)\\\\([^{]+){([^}]+)}(.*)')
  49. const END_OF_TITLE_REGEX = new RegExp('^([^{}]*?({[^{}]*?}[^{}]*?)*)}')
  50. /*
  51. * Attempt to improve the display of the outline title for titles with commands.
  52. * Either skip the command (for labels) or display the command's content instead
  53. * of the entire command.
  54. *
  55. * e.g. "Label \\label{foo} between" => "Label between"
  56. * e.g. "TT \\texttt{Bar}" => "TT Bar"
  57. *
  58. */
  59. function matchDisplayTitle(title) {
  60. const closingBracketPosition = title.indexOf('}')
  61. if (closingBracketPosition < 0) {
  62. // simple title (no commands)
  63. return title
  64. }
  65. // if there is anything outside the title def on the line, remove it
  66. // before proceeding
  67. const titleOnlyMatch = title.match(END_OF_TITLE_REGEX)
  68. if (titleOnlyMatch) {
  69. title = titleOnlyMatch[1]
  70. }
  71. const titleMatch = title.match(DISPLAY_TITLE_REGEX)
  72. if (!titleMatch) {
  73. // no contained commands; strip everything after the first closing bracket
  74. return title.substring(0, closingBracketPosition)
  75. }
  76. const [, textBefore, command, commandContent, textAfter] = titleMatch
  77. if (command === 'label') {
  78. // label: don't display them at all
  79. title = `${textBefore}${textAfter}`
  80. } else {
  81. // display the content of the command. Works well for formatting commands
  82. title = `${textBefore}${commandContent}${textAfter}`
  83. }
  84. return title
  85. }
  86. function nestOutline(flatOutline) {
  87. let parentOutlines = {}
  88. let nestedOutlines = []
  89. flatOutline.forEach(outline => {
  90. const parentOutlineLevels = Object.keys(parentOutlines)
  91. // find the nearest parent outline
  92. const nearestParentLevel = parentOutlineLevels
  93. .reverse()
  94. .find(level => level < outline.level)
  95. const parentOutline = parentOutlines[nearestParentLevel]
  96. if (!parentOutline) {
  97. // top level
  98. nestedOutlines.push(outline)
  99. } else if (!parentOutline.children) {
  100. // first outline in this node
  101. parentOutline.children = [outline]
  102. } else {
  103. // push outline to node
  104. parentOutline.children.push(outline)
  105. }
  106. // store the outline as new parent at its level and forget lower-level
  107. // outlines (if any) as they shouldn't get any children anymore
  108. parentOutlines[outline.level] = outline
  109. parentOutlineLevels
  110. .filter(level => level > outline.level)
  111. .forEach(level => delete parentOutlines[level])
  112. })
  113. return nestedOutlines
  114. }
  115. export { matchOutline, nestOutline }