outline-parser.js 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  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. /*
  50. * Attempt to improve the display of the outline title for titles with commands.
  51. * Either skip the command (for labels) or display the command's content instead
  52. * of the entire command.
  53. *
  54. * e.g. "Label \\label{foo} between" => "Label between"
  55. * e.g. "TT \\texttt{Bar}" => "TT Bar"
  56. *
  57. */
  58. function matchDisplayTitle(title) {
  59. const closingBracketPosition = title.indexOf('}')
  60. if (closingBracketPosition < 0) {
  61. // simple title (no commands)
  62. return title
  63. }
  64. const titleMatch = title.match(DISPLAY_TITLE_REGEX)
  65. if (!titleMatch) {
  66. // no contained commands; strip everything after the first closing bracket
  67. return title.substring(0, closingBracketPosition)
  68. }
  69. const [, textBefore, command, commandContent, textAfter] = titleMatch
  70. if (command === 'label') {
  71. // label: don't display them at all
  72. title = `${textBefore}${textAfter}`
  73. } else {
  74. // display the content of the command. Works well for formatting commands
  75. title = `${textBefore}${commandContent}${textAfter}`
  76. }
  77. return title
  78. }
  79. function nestOutline(flatOutline) {
  80. let parentOutlines = {}
  81. let nestedOutlines = []
  82. flatOutline.forEach(outline => {
  83. const parentOutlineLevels = Object.keys(parentOutlines)
  84. // find the nearest parent outline
  85. const nearestParentLevel = parentOutlineLevels
  86. .reverse()
  87. .find(level => level < outline.level)
  88. const parentOutline = parentOutlines[nearestParentLevel]
  89. if (!parentOutline) {
  90. // top level
  91. nestedOutlines.push(outline)
  92. } else if (!parentOutline.children) {
  93. // first outline in this node
  94. parentOutline.children = [outline]
  95. } else {
  96. // push outline to node
  97. parentOutline.children.push(outline)
  98. }
  99. // store the outline as new parent at its level and forget lower-level
  100. // outlines (if any) as they shouldn't get any children anymore
  101. parentOutlines[outline.level] = outline
  102. parentOutlineLevels
  103. .filter(level => level > outline.level)
  104. .forEach(level => delete parentOutlines[level])
  105. })
  106. return nestedOutlines
  107. }
  108. export { matchOutline, nestOutline }