outline-parser.js 3.4 KB

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