diff --git a/services/web/test/frontend/features/outline/outline-parser.test.js b/services/web/test/frontend/features/outline/outline-parser.test.js new file mode 100644 index 0000000000..dbb78598aa --- /dev/null +++ b/services/web/test/frontend/features/outline/outline-parser.test.js @@ -0,0 +1,182 @@ +import { expect } from 'chai' + +import { + matchOutline, + nestOutline +} from '../../../../frontend/js/features/outline/outline-parser' + +describe.only('OutlineParser', function() { + describe('matchOutline', function() { + it('matches all levels', function() { + const content = ` + \\book{Book} + \\part{Part} + \\chapter{Chapter} + \\section{Section 1} + \\subsection{Subsection} + \\subsubsection{Subsubsection} + \\section{Section 2} + \\subsubsection{Subsubsection without subsection} + \\paragraph{a paragraph} Here is some text. + \\subparagraph{a subparagraph} Here is some more text. + ` + const outline = matchOutline(content) + expect(outline).to.deep.equal([ + { line: 2, title: 'Book', level: 10 }, + { line: 3, title: 'Part', level: 20 }, + { line: 4, title: 'Chapter', level: 30 }, + { line: 5, title: 'Section 1', level: 40 }, + { line: 6, title: 'Subsection', level: 50 }, + { line: 7, title: 'Subsubsection', level: 60 }, + { line: 8, title: 'Section 2', level: 40 }, + { line: 9, title: 'Subsubsection without subsection', level: 60 }, + { line: 10, title: 'a paragraph', level: 70 }, + { line: 11, title: 'a subparagraph', level: 80 } + ]) + }) + + it('matches display titles', function() { + const content = ` + \\section{\\label{foo} Label before} + \\section{Label after \\label{foo}} + \\section{Label \\label{foo} between} + \\section{TT \\texttt{Bar}} + ` + const outline = matchOutline(content) + expect(outline).to.deep.equal([ + { line: 2, title: ' Label before', level: 40 }, + { line: 3, title: 'Label after ', level: 40 }, + { line: 4, title: 'Label between', level: 40 }, + { line: 5, title: 'TT Bar', level: 40 } + ]) + }) + + it('matches empty sections', function() { + const outline = matchOutline('\\section{}') + expect(outline).to.deep.equal([{ line: 1, title: '', level: 40 }]) + }) + + it('matches indented sections', function() { + const outline = matchOutline('\t\\section{Indented}') + expect(outline).to.deep.equal([{ line: 1, title: 'Indented', level: 40 }]) + }) + + it('matches unnumbered sections', function() { + const outline = matchOutline('\\section*{Unnumbered}') + expect(outline).to.deep.equal([ + { line: 1, title: 'Unnumbered', level: 40 } + ]) + }) + + it('matches short titles', function() { + const outline = matchOutline( + '\\chapter[Short Title For TOC]{Very Long Title for Text}' + ) + expect(outline).to.deep.equal([ + { line: 1, title: 'Short Title For TOC', level: 30 } + ]) + }) + + it('handles spacing', function() { + const content = ` + \\section {Weird Spacing} + \\section * {Weird Spacing Unnumbered} + \\section [Weird Spacing for TOC] {Weird Spacing} + ` + const outline = matchOutline(content) + expect(outline).to.deep.equal([ + { line: 2, title: 'Weird Spacing', level: 40 }, + { line: 3, title: 'Weird Spacing Unnumbered', level: 40 }, + { line: 4, title: 'Weird Spacing for TOC', level: 40 } + ]) + }) + + it("doesn't match commented lines", function() { + const content = ` + % \\section{I should not appear in the outline} + ` + const outline = matchOutline(content) + expect(outline).to.deep.equal([]) + }) + + it("doesn't match inline sections", function() { + const content = ` + I like to write \\section{inline} on one line. + ` + const outline = matchOutline(content) + expect(outline).to.deep.equal([]) + }) + }) + + describe('nestOutline', function() { + it('matches all levels', function() { + const flatOutline = [ + { line: 10, title: 'Book', level: 10 }, + { line: 20, title: 'Part A', level: 20 }, + { line: 30, title: 'Section A 1', level: 40 }, + { line: 40, title: 'Subsection A 1 1', level: 50 }, + { line: 50, title: 'Subsection A 1 2', level: 50 }, + { line: 60, title: 'Section A 2', level: 40 }, + { line: 70, title: 'Section A 3', level: 40 }, + { line: 80, title: 'Subsection A 3 1', level: 50 }, + { line: 90, title: 'Chapter', level: 30 }, + { line: 100, title: 'Part B', level: 20 }, + { line: 110, title: 'Section 2', level: 40 }, + { line: 120, title: 'Subsubsection without subsection', level: 60 } + ] + const nestedOutline = nestOutline(flatOutline) + expect(nestedOutline).to.deep.equal([ + { + line: 10, + title: 'Book', + level: 10, + children: [ + { + line: 20, + title: 'Part A', + level: 20, + children: [ + { + line: 30, + title: 'Section A 1', + level: 40, + children: [ + { line: 40, title: 'Subsection A 1 1', level: 50 }, + { line: 50, title: 'Subsection A 1 2', level: 50 } + ] + }, + { line: 60, title: 'Section A 2', level: 40 }, + { + line: 70, + title: 'Section A 3', + level: 40, + children: [{ line: 80, title: 'Subsection A 3 1', level: 50 }] + }, + { line: 90, title: 'Chapter', level: 30 } + ] + }, + { + line: 100, + title: 'Part B', + level: 20, + children: [ + { + line: 110, + title: 'Section 2', + level: 40, + children: [ + { + line: 120, + title: 'Subsubsection without subsection', + level: 60 + } + ] + } + ] + } + ] + } + ]) + }) + }) +})