Roel Spilker
Committed by David PHAM-VAN

Improve outlines containing non-sequential level increments

@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 - Depreciate Font.stringSize 6 - Depreciate Font.stringSize
7 - Implement fallback font 7 - Implement fallback font
8 - Implement Emoji support 8 - Implement Emoji support
  9 +- Improve outlines containing non-sequential level increments [Roel Spilker]
9 10
10 ## 3.6.6 11 ## 3.6.6
11 12
@@ -623,24 +623,28 @@ class Outline extends Anchor { @@ -623,24 +623,28 @@ class Outline extends Anchor {
623 color: color, 623 color: color,
624 style: style, 624 style: style,
625 page: context.pageNumber, 625 page: context.pageNumber,
626 - ); 626 + )..effectiveLevel = level;
627 627
628 - var parent = context.document.outline;  
629 - var l = level; 628 + final root = context.document.outline;
630 629
631 - while (l > 0) {  
632 - if (parent.effectiveLevel == l) {  
633 - break;  
634 - } 630 + // find the most recently added outline
  631 + var actualLevel= -1;
  632 + var candidate = root;
  633 + while (candidate.outlines.isNotEmpty) {
  634 + candidate = candidate.outlines.last;
  635 + actualLevel++;
  636 + }
635 637
636 - if (parent.outlines.isEmpty) {  
637 - parent.effectiveLevel = level; 638 + // find the latest added outline with a level lower than ours
  639 + while (candidate != root) {
  640 + final candidateLevel = candidate.effectiveLevel ?? actualLevel;
  641 + if (candidateLevel < level) {
638 break; 642 break;
639 } 643 }
640 - parent = parent.outlines.last;  
641 - l--; 644 + candidate = candidate.parent!;
  645 + actualLevel--;
642 } 646 }
643 647
644 - parent.add(_outline!); 648 + candidate.add(_outline!);
645 } 649 }
646 } 650 }
@@ -83,8 +83,144 @@ void main() { @@ -83,8 +83,144 @@ void main() {
83 ); 83 );
84 }); 84 });
85 85
  86 + group('Levels', () {
  87 + test('Well-formed', () {
  88 + final generated = _OutlineBuilder()
  89 + .add('Part 1', 0)
  90 + .add('Chapter 1', 1)
  91 + .add('Paragraph 1.1', 2)
  92 + .add('Paragraph 1.2', 2)
  93 + .add('Paragraph 1.3', 2)
  94 + .add('Chapter 2', 1)
  95 + .add('Paragraph 2.1', 2)
  96 + .add('Paragraph 2.2', 2)
  97 + .render()
  98 + .join('\n');
  99 + const expected = '''null
  100 + Part 1
  101 + Chapter 1
  102 + Paragraph 1.1
  103 + Paragraph 1.2
  104 + Paragraph 1.3
  105 + Chapter 2
  106 + Paragraph 2.1
  107 + Paragraph 2.2''';
  108 +
  109 + expect(generated, expected);
  110 + });
  111 +
  112 + test('Does not start with level 0', () {
  113 + final generated = _OutlineBuilder()
  114 + .add('Part 1', 1)
  115 + .add('Chapter 1', 2)
  116 + .add('Chapter 2', 2)
  117 + .render()
  118 + .join('\n');
  119 + const expected = '''null
  120 + Part 1
  121 + Chapter 1
  122 + Chapter 2''';
  123 +
  124 + expect(generated, expected);
  125 + });
  126 +
  127 + test('Contains non-sequential level increment', () {
  128 + final generated = _OutlineBuilder()
  129 + .add('Part 1', 0)
  130 + .add('Chapter 1', 2)
  131 + .add('Paragraph 1.1', 4)
  132 + .add('Paragraph 1.2', 4)
  133 + .add('Paragraph 1.3', 4)
  134 + .add('Chapter 2', 2)
  135 + .add('Paragraph 2.1', 4)
  136 + .add('Paragraph 2.2', 4)
  137 + .render()
  138 + .join('\n');
  139 +
  140 + const expected = '''null
  141 + Part 1
  142 + Chapter 1
  143 + Paragraph 1.1
  144 + Paragraph 1.2
  145 + Paragraph 1.3
  146 + Chapter 2
  147 + Paragraph 2.1
  148 + Paragraph 2.2''';
  149 +
  150 + expect(generated, expected);
  151 + });
  152 +
  153 + test('Reverse leveling', () {
  154 + final generated = _OutlineBuilder()
  155 + .add('Paragraph 2.2', 2)
  156 + .add('Paragraph 2.1', 2)
  157 + .add('Chapter 2', 1)
  158 + .add('Paragraph 1.3', 2)
  159 + .add('Paragraph 1.2', 2)
  160 + .add('Paragraph 1.1', 2)
  161 + .add('Chapter 1', 1)
  162 + .add('Part 1', 0)
  163 + .render()
  164 + .join('\n');
  165 +
  166 + const expected = '''null
  167 + Paragraph 2.2
  168 + Paragraph 2.1
  169 + Chapter 2
  170 + Paragraph 1.3
  171 + Paragraph 1.2
  172 + Paragraph 1.1
  173 + Chapter 1
  174 + Part 1''';
  175 +
  176 + expect(generated, expected);
  177 + });
  178 + });
  179 +
86 tearDownAll(() async { 180 tearDownAll(() async {
87 final file = File('widgets-outline.pdf'); 181 final file = File('widgets-outline.pdf');
88 await file.writeAsBytes(await pdf.save()); 182 await file.writeAsBytes(await pdf.save());
89 }); 183 });
90 } 184 }
  185 +
  186 +class _OutlineBuilder {
  187 + final List<String> _titles = <String>[];
  188 + final List<int> _levels = <int>[];
  189 +
  190 + _OutlineBuilder add(String text, int level) {
  191 + _titles.add(text);
  192 + _levels.add(level);
  193 + return this;
  194 + }
  195 +
  196 + List<String> render() {
  197 + final pdf = Document();
  198 + pdf.addPage(
  199 + MultiPage(
  200 + build: (Context context) => <Widget>[
  201 + for (int i = 0; i < _titles.length; i++)
  202 + Outline(
  203 + name: 'anchor$i',
  204 + title: _titles[i],
  205 + level: _levels[i],
  206 + )
  207 + ],
  208 + ),
  209 + );
  210 + return _collectOutlines(pdf.document.catalog.outlines!);
  211 + }
  212 +}
  213 +
  214 +List<String> _collectOutlines(
  215 + PdfOutline outline, [
  216 + List<String>? output,
  217 + int indent = 0,
  218 +]) {
  219 + final result = output ?? <String>[];
  220 + final intentation = List.filled(indent, ' ').join();
  221 + result.add('$intentation${outline.title}');
  222 + for (var child in outline.outlines) {
  223 + _collectOutlines(child, result, indent + 1);
  224 + }
  225 + return result;
  226 +}