outline-parser.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. const COMMAND_LEVELS = {
  2. book: 10,
  3. part: 20,
  4. addpart: 20,
  5. chapter: 30,
  6. addchap: 30,
  7. section: 40,
  8. addsec: 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 = matchOutlineFromLines(lines)
  36. return flatOutline
  37. }
  38. function matchOutlineFromLines(lines) {
  39. const flatOutline = []
  40. lines.forEach((line, lineId) => {
  41. const match = line.match(MATCHER)
  42. if (!match) return
  43. const [, command, , shortTitle, title] = match
  44. flatOutline.push({
  45. line: lineId + 1,
  46. title: matchDisplayTitle(shortTitle || title),
  47. level: COMMAND_LEVELS[command],
  48. })
  49. })
  50. return flatOutline
  51. }
  52. const DISPLAY_TITLE_REGEX = /([^\\]*)\\([^{]+){([^}]+)}(.*)/
  53. const END_OF_TITLE_REGEX = /^([^{}]*?({[^{}]*?}[^{}]*?)*)}/
  54. /*
  55. * Attempt to improve the display of the outline title for titles with commands.
  56. * Either skip the command (for labels) or display the command's content instead
  57. * of the entire command.
  58. *
  59. * e.g. "Label \\label{foo} between" => "Label between"
  60. * e.g. "TT \\texttt{Bar}" => "TT Bar"
  61. *
  62. */
  63. function matchDisplayTitle(title) {
  64. const closingBracketPosition = title.indexOf('}')
  65. if (closingBracketPosition < 0) {
  66. // simple title (no commands)
  67. return title
  68. }
  69. // if there is anything outside the title def on the line, remove it
  70. // before proceeding
  71. const titleOnlyMatch = title.match(END_OF_TITLE_REGEX)
  72. if (titleOnlyMatch) {
  73. title = titleOnlyMatch[1]
  74. }
  75. const titleMatch = title.match(DISPLAY_TITLE_REGEX)
  76. if (!titleMatch) {
  77. // no contained commands; strip everything after the first closing bracket
  78. return title.substring(0, closingBracketPosition)
  79. }
  80. const [, textBefore, command, commandContent, textAfter] = titleMatch
  81. if (command === 'label') {
  82. // label: don't display them at all
  83. title = `${textBefore}${textAfter}`
  84. } else {
  85. // display the content of the command. Works well for formatting commands
  86. title = `${textBefore}${commandContent}${textAfter}`
  87. }
  88. return title
  89. }
  90. function nestOutline(flatOutline) {
  91. const parentOutlines = {}
  92. const nestedOutlines = []
  93. flatOutline.forEach(outline => {
  94. const parentOutlineLevels = Object.keys(parentOutlines)
  95. // find the nearest parent outline
  96. const nearestParentLevel = parentOutlineLevels
  97. .reverse()
  98. .find(level => level < outline.level)
  99. const parentOutline = parentOutlines[nearestParentLevel]
  100. if (!parentOutline) {
  101. // top level
  102. nestedOutlines.push(outline)
  103. } else if (!parentOutline.children) {
  104. // first outline in this node
  105. parentOutline.children = [outline]
  106. } else {
  107. // push outline to node
  108. parentOutline.children.push(outline)
  109. }
  110. // store the outline as new parent at its level and forget lower-level
  111. // outlines (if any) as they shouldn't get any children anymore
  112. parentOutlines[outline.level] = outline
  113. parentOutlineLevels
  114. .filter(level => level > outline.level)
  115. .forEach(level => delete parentOutlines[level])
  116. })
  117. return nestedOutlines
  118. }
  119. export { matchOutline, matchOutlineFromLines, nestOutline }