reffy 6.2.0 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +158 -158
  3. package/index.js +11 -11
  4. package/package.json +53 -53
  5. package/reffy.js +248 -248
  6. package/src/browserlib/canonicalize-url.mjs +50 -50
  7. package/src/browserlib/create-outline.mjs +352 -352
  8. package/src/browserlib/extract-cssdfn.mjs +319 -319
  9. package/src/browserlib/extract-dfns.mjs +686 -686
  10. package/src/browserlib/extract-elements.mjs +205 -205
  11. package/src/browserlib/extract-headings.mjs +48 -48
  12. package/src/browserlib/extract-ids.mjs +28 -28
  13. package/src/browserlib/extract-links.mjs +28 -28
  14. package/src/browserlib/extract-references.mjs +203 -203
  15. package/src/browserlib/extract-webidl.mjs +134 -134
  16. package/src/browserlib/get-absolute-url.mjs +21 -21
  17. package/src/browserlib/get-generator.mjs +26 -26
  18. package/src/browserlib/get-lastmodified-date.mjs +13 -13
  19. package/src/browserlib/get-title.mjs +11 -11
  20. package/src/browserlib/informative-selector.mjs +16 -16
  21. package/src/browserlib/map-ids-to-headings.mjs +136 -136
  22. package/src/browserlib/reffy.json +53 -53
  23. package/src/cli/check-missing-dfns.js +609 -609
  24. package/src/cli/generate-idlnames.js +430 -430
  25. package/src/cli/generate-idlparsed.js +139 -139
  26. package/src/cli/merge-crawl-results.js +128 -128
  27. package/src/cli/parse-webidl.js +430 -430
  28. package/src/lib/css-grammar-parse-tree.schema.json +109 -109
  29. package/src/lib/css-grammar-parser.js +440 -440
  30. package/src/lib/fetch.js +56 -56
  31. package/src/lib/nock-server.js +127 -120
  32. package/src/lib/specs-crawler.js +622 -603
  33. package/src/lib/util.js +943 -898
  34. package/src/specs/missing-css-rules.json +197 -197
  35. package/src/specs/spec-equivalents.json +149 -149
  36. package/src/browserlib/extract-editors.mjs~ +0 -14
  37. package/src/browserlib/generate-es-dfn-report.sh~ +0 -4
  38. package/src/cli/csstree-grammar-check.js +0 -28
  39. package/src/cli/csstree-grammar-check.js~ +0 -10
  40. package/src/cli/csstree-grammar-parser.js +0 -11
  41. package/src/cli/csstree-grammar-parser.js~ +0 -1
  42. package/src/cli/extract-editors.js~ +0 -38
  43. package/src/cli/process-specs.js~ +0 -28
@@ -1,352 +1,352 @@
1
- /**
2
- * Creates an outline for the DOM subtree rooted at the given sectioning content
3
- * or sectioning root element.
4
- *
5
- * This function implements the "creating an outline" algorithm in HTML:
6
- * https://html.spec.whatwg.org/multipage/sections.html#outlines
7
- *
8
- * As a by-product of generating the outline, the function also generates a
9
- * mapping between elements and the (conceptual) section that contains them in
10
- * the outline. To save memory, this mapping is only done for elements that have
11
- * an ID.
12
- *
13
- * Both the outline and the mapping are returned.
14
- */
15
- export default function (root) {
16
- const headingContent = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HGROUP'];
17
- const sectioningContent = ['ARTICLE', 'ASIDE', 'NAV', 'SECTION'];
18
- const sectioningRoot = ['BLOCKQUOTE', 'BODY', 'DETAILS', 'DIALOG', 'FIELDSET', 'FIGURE', 'TD'];
19
-
20
- // A conceptual section has:
21
- // - a heading element, which may be the string "__implied" when there is no
22
- // real heading element
23
- // - an explicit sectioning content element that gave birth to the section,
24
- // unless the section was implicitly created through a heading element
25
- // - a list of nested sections
26
- // - a list of nested outlines, generated by sectioning root elements that
27
- // this section may contain
28
- function createSection() {
29
- return {
30
- heading: null,
31
- root: null,
32
- subSections: [],
33
- subRoots: []
34
- };
35
- }
36
-
37
- function flattenSections(outline) {
38
- return outline.concat(outline.flatMap(section =>
39
- flattenSections(section.subSections)));
40
- }
41
-
42
- // 1. Let current outline target be null. (It holds the element whose outline
43
- // is being created.)
44
- let currentOutlineTarget = null;
45
-
46
- // 2. Let current section be null. (It holds a pointer to a section, so that
47
- // elements in the DOM can all be associated with a section.)
48
- let currentSection = null;
49
-
50
- // 3. Create a stack to hold elements, which is used to handle nesting.
51
- // Initialize this stack to empty.
52
- let stack = [];
53
-
54
- let nodeToOutline = new Map();
55
- let nodeToParentSection = new Map();
56
- let nodeToSection = new Map();
57
-
58
- // Compute the rank of the given node
59
- function rank(node) {
60
- switch (node.nodeName) {
61
- case 'H1': return -1;
62
- case 'H2': return -2;
63
- case 'H3': return -3;
64
- case 'H4': return -4;
65
- case 'H5': return -5;
66
- case 'H6': return -6;
67
- case 'HGROUP': return Math.max(...[...node.childNodes].map(rank));
68
- default: return -100;
69
- }
70
- }
71
-
72
- // Process node when walk enters it
73
- function enter(node) {
74
- if (node.nodeType !== Node.ELEMENT_NODE) {
75
- return;
76
- }
77
-
78
- // If the top of the stack is a heading content element or an element with a
79
- // hidden attribute, do nothing.
80
- const topOfStack = (stack.length > 0) ? stack[stack.length - 1] : null;
81
- if (topOfStack &&
82
- (headingContent.includes(topOfStack.nodeName) ||
83
- topOfStack.hasAttribute('hidden'))) {
84
- return;
85
- }
86
-
87
- // When entering an element with a hidden attribute, push the element being
88
- // entered onto the stack (This causes the algorithm to skip that element
89
- // and any descendants of the element).
90
- if (node.hasAttribute('hidden')) {
91
- stack.push(node);
92
- return;
93
- }
94
-
95
- // When entering a sectioning content element
96
- if (sectioningContent.includes(node.nodeName)) {
97
- // 1. If current outline target is not null, then:
98
- if (currentOutlineTarget) {
99
- // 1.1 If the current section has no heading, create an implied heading and
100
- // let that be the heading for the current section.
101
- if (!currentSection.heading) {
102
- currentSection.heading = '__implied';
103
- }
104
-
105
- // 1.2 Push current outline target onto the stack.
106
- stack.push(currentOutlineTarget);
107
- }
108
-
109
- // 2. Let current outline target be the element that is being entered.
110
- currentOutlineTarget = node;
111
-
112
- // 3. Let current section be a newly created section for the current
113
- // outline target element.
114
- currentSection = createSection();
115
- currentSection.root = currentOutlineTarget;
116
-
117
- // 4. Associate current outline target with current section.
118
- nodeToSection.set(currentOutlineTarget, currentSection);
119
-
120
- // 5. Let there be a new outline for the new current outline target,
121
- // initialized with just the new current section as the only section in
122
- // the outline.
123
- nodeToOutline.set(currentOutlineTarget, [currentSection]);
124
-
125
- return;
126
- }
127
-
128
- // When entering a sectioning root element
129
- if (sectioningRoot.includes(node.nodeName)) {
130
- // 1. If current outline target is not null, push current outline target
131
- // onto the stack.
132
- if (currentOutlineTarget) {
133
- stack.push(currentOutlineTarget);
134
- }
135
-
136
- // 2. Let current outline target be the element that is being entered.
137
- currentOutlineTarget = node;
138
-
139
- // 3. Let current outline target's parent section be current section.
140
- nodeToParentSection.set(currentOutlineTarget, currentSection);
141
-
142
- // 4. Let current section be a newly created section for the current
143
- // outline target element.
144
- currentSection = createSection();
145
- currentSection.root = currentOutlineTarget;
146
-
147
- // 5. Let there be a new outline for the new current outline target,
148
- // initialized with just the new current section as the only section in
149
- // the outline.
150
- nodeToOutline.set(currentOutlineTarget, [currentSection]);
151
-
152
- return;
153
- }
154
-
155
- // When entering a heading content element
156
- if (headingContent.includes(node.nodeName)) {
157
- const outline = nodeToOutline.get(currentOutlineTarget);
158
- const lastSection = outline[outline.length - 1];
159
-
160
- // If the current section has no heading, let the element being entered be
161
- // the heading for the current section.
162
- if (!currentSection.heading) {
163
- currentSection.heading = node;
164
- }
165
-
166
- // Otherwise, if the element being entered has a rank equal to or higher
167
- // than the heading of the last section of the outline of the current
168
- // outline target, or if the heading of the last section of the outline of
169
- // the current outline target is an implied heading, then create a new
170
- // section and append it to the outline of the current outline target
171
- // element, so that this new section is the new last section of that
172
- // outline. Let current section be that new section. Let the element being
173
- // entered be the new heading for the current section.
174
- else if ((lastSection.heading === '__implied') ||
175
- (rank(node) >= rank(lastSection.heading))) {
176
- currentSection = createSection();
177
- currentSection.heading = node;
178
- outline.push(currentSection);
179
- }
180
-
181
- // Otherwise, run these substeps:
182
- else {
183
- // 1. Let candidate section be current section.
184
- let candidateSection = currentSection;
185
- while (candidateSection) {
186
- // 2. Heading loop: If the element being entered has a rank lower than
187
- // the rank of the heading of the candidate section, then create a new
188
- // section, and append it to candidate section. (This does not change
189
- // which section is the last section in the outline.) Let current
190
- // section be this new section. Let the element being entered be the
191
- // new heading for the current section. Abort these substeps.
192
- if (rank(node) < rank(candidateSection.heading)) {
193
- currentSection = createSection();
194
- currentSection.heading = node;
195
- candidateSection.subSections.push(currentSection);
196
- break;
197
- }
198
-
199
- // 3. Let new candidate section be the section that contains candidate
200
- // section in the outline of current outline target.
201
- const sections = flattenSections(nodeToOutline.get(currentOutlineTarget));
202
- let newCandidateSection = sections.find(section =>
203
- section.subSections.includes(candidateSection));
204
-
205
- // 4. Let candidate section be new candidate section.
206
- candidateSection = newCandidateSection;
207
-
208
- // 5. Return to the step labeled heading loop.
209
- }
210
-
211
- // Push the element being entered onto the stack. (This causes the
212
- // algorithm to skip any descendants of the element.)
213
- stack.push(node);
214
- return;
215
- }
216
- }
217
- }
218
-
219
- // Process node when walk exits it
220
- function exit(node) {
221
- if (node.nodeType !== Node.ELEMENT_NODE) {
222
- return;
223
- }
224
-
225
- function innerExit() {
226
- const topOfStack = (stack.length > 0) ? stack[stack.length - 1] : null;
227
-
228
- // When exiting an element, if that element is the element at the top of
229
- // the stack, pop that element from the stack.
230
- if (topOfStack === node) {
231
- stack.pop();
232
- return;
233
- }
234
-
235
- // If the top of the stack is a heading content element or an element with
236
- // a hidden attribute, do nothing.
237
- if (topOfStack &&
238
- (headingContent.includes(topOfStack.nodeName) ||
239
- topOfStack.hasAttribute('hidden'))) {
240
- return;
241
- }
242
-
243
- // When exiting a sectioning content element, if the stack is not empty
244
- if (sectioningContent.includes(node.nodeName) && (stack.length > 0)) {
245
- // 1. If the current section has no heading, create an implied heading
246
- // and let that be the heading for the current section.
247
- if (!currentSection.heading) {
248
- currentSection.heading = '__implied';
249
- }
250
-
251
- // 2. Pop the top element from the stack, and let the current outline
252
- // target be that element.
253
- currentOutlineTarget = stack.pop();
254
-
255
- // 3. Let current section be the last section in the outline of the
256
- // current outline target element.
257
- let outline = nodeToOutline.get(currentOutlineTarget);
258
- currentSection = outline[outline.length - 1];
259
-
260
- // 4. Append the outline of the sectioning content element being exited
261
- // to the current section. (This does not change which section is the
262
- // last section in the outline)
263
- currentSection.subSections.push(...nodeToOutline.get(node));
264
-
265
- return;
266
- }
267
-
268
- // When exiting a sectioning root element, if the stack is not empty
269
- if (sectioningRoot.includes(node.nodeName) && (stack.length > 0)) {
270
- // 1. If the current section has no heading, create an implied heading
271
- // and let that be the heading for the current section.
272
- if (!currentSection.heading) {
273
- currentSection.heading = '__implied';
274
- }
275
-
276
- // 2. Let current section be current outline target's parent section.
277
- currentSection = nodeToParentSection.get(currentOutlineTarget);
278
-
279
- // A sectioning root generates a separate outline, let's attach it
280
- // to the main outline
281
- currentSection.subRoots.push(...nodeToOutline.get(node));
282
-
283
- // 3. Pop the top element from the stack, and let the current outline
284
- // target be that element.
285
- currentOutlineTarget = stack.pop();
286
-
287
- return;
288
- }
289
-
290
- // When exiting a sectioning content element or a sectioning root element
291
- // (when the stack is empty)
292
- if (sectioningContent.includes(node.nodeName) ||
293
- sectioningRoot.includes(node.nodeName)) {
294
- // If the current section has no heading, create an implied heading and
295
- // let that be the heading for the current section.
296
- if (!currentSection.heading) {
297
- currentSection.heading = '__implied';
298
- }
299
-
300
- // Skip to the next step in the overall set of steps. (The walk is over)
301
- return;
302
- }
303
- }
304
-
305
- innerExit();
306
-
307
- // In addition, whenever the walk exits a node, after doing the steps above,
308
- // if the node is not associated with a section yet, associate the node with
309
- // the section current section.
310
- // (we will only do that for elements that have an ID)
311
- if (node.getAttribute('id') && !nodeToSection.has(node)) {
312
- nodeToSection.set(node, currentSection);
313
- }
314
- }
315
-
316
- // Walk the DOM subtree in depth-first order, entering and exiting nodes.
317
- function walk(root, enter, exit) {
318
- let node = root;
319
- start: while (node) {
320
- enter(node);
321
- // Note: HGROUP is composed of multiple sub-headings but represents a
322
- // single heading, skip its children as that would create ghost
323
- // subsections
324
- if ((node.nodeName !== 'HGROUP') && node.firstChild) {
325
- node = node.firstChild;
326
- continue start;
327
- }
328
- while (node) {
329
- exit(node);
330
- if (node == root) {
331
- node = null;
332
- } else if (node.nextSibling) {
333
- node = node.nextSibling;
334
- continue start;
335
- } else {
336
- node = node.parentNode;
337
- }
338
- }
339
- }
340
- }
341
-
342
- // 4. Walk over the DOM in tree order, starting with the sectioning content
343
- // element or sectioning root element at the root of the subtree for which an
344
- // outline is to be created, and trigger the first relevant step below for
345
- // each element as the walk enters and exits it.
346
- walk(root, enter, exit);
347
-
348
- return {
349
- outline: nodeToOutline.get(root),
350
- nodeToSection
351
- };
352
- }
1
+ /**
2
+ * Creates an outline for the DOM subtree rooted at the given sectioning content
3
+ * or sectioning root element.
4
+ *
5
+ * This function implements the "creating an outline" algorithm in HTML:
6
+ * https://html.spec.whatwg.org/multipage/sections.html#outlines
7
+ *
8
+ * As a by-product of generating the outline, the function also generates a
9
+ * mapping between elements and the (conceptual) section that contains them in
10
+ * the outline. To save memory, this mapping is only done for elements that have
11
+ * an ID.
12
+ *
13
+ * Both the outline and the mapping are returned.
14
+ */
15
+ export default function (root) {
16
+ const headingContent = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'HGROUP'];
17
+ const sectioningContent = ['ARTICLE', 'ASIDE', 'NAV', 'SECTION'];
18
+ const sectioningRoot = ['BLOCKQUOTE', 'BODY', 'DETAILS', 'DIALOG', 'FIELDSET', 'FIGURE', 'TD'];
19
+
20
+ // A conceptual section has:
21
+ // - a heading element, which may be the string "__implied" when there is no
22
+ // real heading element
23
+ // - an explicit sectioning content element that gave birth to the section,
24
+ // unless the section was implicitly created through a heading element
25
+ // - a list of nested sections
26
+ // - a list of nested outlines, generated by sectioning root elements that
27
+ // this section may contain
28
+ function createSection() {
29
+ return {
30
+ heading: null,
31
+ root: null,
32
+ subSections: [],
33
+ subRoots: []
34
+ };
35
+ }
36
+
37
+ function flattenSections(outline) {
38
+ return outline.concat(outline.flatMap(section =>
39
+ flattenSections(section.subSections)));
40
+ }
41
+
42
+ // 1. Let current outline target be null. (It holds the element whose outline
43
+ // is being created.)
44
+ let currentOutlineTarget = null;
45
+
46
+ // 2. Let current section be null. (It holds a pointer to a section, so that
47
+ // elements in the DOM can all be associated with a section.)
48
+ let currentSection = null;
49
+
50
+ // 3. Create a stack to hold elements, which is used to handle nesting.
51
+ // Initialize this stack to empty.
52
+ let stack = [];
53
+
54
+ let nodeToOutline = new Map();
55
+ let nodeToParentSection = new Map();
56
+ let nodeToSection = new Map();
57
+
58
+ // Compute the rank of the given node
59
+ function rank(node) {
60
+ switch (node.nodeName) {
61
+ case 'H1': return -1;
62
+ case 'H2': return -2;
63
+ case 'H3': return -3;
64
+ case 'H4': return -4;
65
+ case 'H5': return -5;
66
+ case 'H6': return -6;
67
+ case 'HGROUP': return Math.max(...[...node.childNodes].map(rank));
68
+ default: return -100;
69
+ }
70
+ }
71
+
72
+ // Process node when walk enters it
73
+ function enter(node) {
74
+ if (node.nodeType !== Node.ELEMENT_NODE) {
75
+ return;
76
+ }
77
+
78
+ // If the top of the stack is a heading content element or an element with a
79
+ // hidden attribute, do nothing.
80
+ const topOfStack = (stack.length > 0) ? stack[stack.length - 1] : null;
81
+ if (topOfStack &&
82
+ (headingContent.includes(topOfStack.nodeName) ||
83
+ topOfStack.hasAttribute('hidden'))) {
84
+ return;
85
+ }
86
+
87
+ // When entering an element with a hidden attribute, push the element being
88
+ // entered onto the stack (This causes the algorithm to skip that element
89
+ // and any descendants of the element).
90
+ if (node.hasAttribute('hidden')) {
91
+ stack.push(node);
92
+ return;
93
+ }
94
+
95
+ // When entering a sectioning content element
96
+ if (sectioningContent.includes(node.nodeName)) {
97
+ // 1. If current outline target is not null, then:
98
+ if (currentOutlineTarget) {
99
+ // 1.1 If the current section has no heading, create an implied heading and
100
+ // let that be the heading for the current section.
101
+ if (!currentSection.heading) {
102
+ currentSection.heading = '__implied';
103
+ }
104
+
105
+ // 1.2 Push current outline target onto the stack.
106
+ stack.push(currentOutlineTarget);
107
+ }
108
+
109
+ // 2. Let current outline target be the element that is being entered.
110
+ currentOutlineTarget = node;
111
+
112
+ // 3. Let current section be a newly created section for the current
113
+ // outline target element.
114
+ currentSection = createSection();
115
+ currentSection.root = currentOutlineTarget;
116
+
117
+ // 4. Associate current outline target with current section.
118
+ nodeToSection.set(currentOutlineTarget, currentSection);
119
+
120
+ // 5. Let there be a new outline for the new current outline target,
121
+ // initialized with just the new current section as the only section in
122
+ // the outline.
123
+ nodeToOutline.set(currentOutlineTarget, [currentSection]);
124
+
125
+ return;
126
+ }
127
+
128
+ // When entering a sectioning root element
129
+ if (sectioningRoot.includes(node.nodeName)) {
130
+ // 1. If current outline target is not null, push current outline target
131
+ // onto the stack.
132
+ if (currentOutlineTarget) {
133
+ stack.push(currentOutlineTarget);
134
+ }
135
+
136
+ // 2. Let current outline target be the element that is being entered.
137
+ currentOutlineTarget = node;
138
+
139
+ // 3. Let current outline target's parent section be current section.
140
+ nodeToParentSection.set(currentOutlineTarget, currentSection);
141
+
142
+ // 4. Let current section be a newly created section for the current
143
+ // outline target element.
144
+ currentSection = createSection();
145
+ currentSection.root = currentOutlineTarget;
146
+
147
+ // 5. Let there be a new outline for the new current outline target,
148
+ // initialized with just the new current section as the only section in
149
+ // the outline.
150
+ nodeToOutline.set(currentOutlineTarget, [currentSection]);
151
+
152
+ return;
153
+ }
154
+
155
+ // When entering a heading content element
156
+ if (headingContent.includes(node.nodeName)) {
157
+ const outline = nodeToOutline.get(currentOutlineTarget);
158
+ const lastSection = outline[outline.length - 1];
159
+
160
+ // If the current section has no heading, let the element being entered be
161
+ // the heading for the current section.
162
+ if (!currentSection.heading) {
163
+ currentSection.heading = node;
164
+ }
165
+
166
+ // Otherwise, if the element being entered has a rank equal to or higher
167
+ // than the heading of the last section of the outline of the current
168
+ // outline target, or if the heading of the last section of the outline of
169
+ // the current outline target is an implied heading, then create a new
170
+ // section and append it to the outline of the current outline target
171
+ // element, so that this new section is the new last section of that
172
+ // outline. Let current section be that new section. Let the element being
173
+ // entered be the new heading for the current section.
174
+ else if ((lastSection.heading === '__implied') ||
175
+ (rank(node) >= rank(lastSection.heading))) {
176
+ currentSection = createSection();
177
+ currentSection.heading = node;
178
+ outline.push(currentSection);
179
+ }
180
+
181
+ // Otherwise, run these substeps:
182
+ else {
183
+ // 1. Let candidate section be current section.
184
+ let candidateSection = currentSection;
185
+ while (candidateSection) {
186
+ // 2. Heading loop: If the element being entered has a rank lower than
187
+ // the rank of the heading of the candidate section, then create a new
188
+ // section, and append it to candidate section. (This does not change
189
+ // which section is the last section in the outline.) Let current
190
+ // section be this new section. Let the element being entered be the
191
+ // new heading for the current section. Abort these substeps.
192
+ if (rank(node) < rank(candidateSection.heading)) {
193
+ currentSection = createSection();
194
+ currentSection.heading = node;
195
+ candidateSection.subSections.push(currentSection);
196
+ break;
197
+ }
198
+
199
+ // 3. Let new candidate section be the section that contains candidate
200
+ // section in the outline of current outline target.
201
+ const sections = flattenSections(nodeToOutline.get(currentOutlineTarget));
202
+ let newCandidateSection = sections.find(section =>
203
+ section.subSections.includes(candidateSection));
204
+
205
+ // 4. Let candidate section be new candidate section.
206
+ candidateSection = newCandidateSection;
207
+
208
+ // 5. Return to the step labeled heading loop.
209
+ }
210
+
211
+ // Push the element being entered onto the stack. (This causes the
212
+ // algorithm to skip any descendants of the element.)
213
+ stack.push(node);
214
+ return;
215
+ }
216
+ }
217
+ }
218
+
219
+ // Process node when walk exits it
220
+ function exit(node) {
221
+ if (node.nodeType !== Node.ELEMENT_NODE) {
222
+ return;
223
+ }
224
+
225
+ function innerExit() {
226
+ const topOfStack = (stack.length > 0) ? stack[stack.length - 1] : null;
227
+
228
+ // When exiting an element, if that element is the element at the top of
229
+ // the stack, pop that element from the stack.
230
+ if (topOfStack === node) {
231
+ stack.pop();
232
+ return;
233
+ }
234
+
235
+ // If the top of the stack is a heading content element or an element with
236
+ // a hidden attribute, do nothing.
237
+ if (topOfStack &&
238
+ (headingContent.includes(topOfStack.nodeName) ||
239
+ topOfStack.hasAttribute('hidden'))) {
240
+ return;
241
+ }
242
+
243
+ // When exiting a sectioning content element, if the stack is not empty
244
+ if (sectioningContent.includes(node.nodeName) && (stack.length > 0)) {
245
+ // 1. If the current section has no heading, create an implied heading
246
+ // and let that be the heading for the current section.
247
+ if (!currentSection.heading) {
248
+ currentSection.heading = '__implied';
249
+ }
250
+
251
+ // 2. Pop the top element from the stack, and let the current outline
252
+ // target be that element.
253
+ currentOutlineTarget = stack.pop();
254
+
255
+ // 3. Let current section be the last section in the outline of the
256
+ // current outline target element.
257
+ let outline = nodeToOutline.get(currentOutlineTarget);
258
+ currentSection = outline[outline.length - 1];
259
+
260
+ // 4. Append the outline of the sectioning content element being exited
261
+ // to the current section. (This does not change which section is the
262
+ // last section in the outline)
263
+ currentSection.subSections.push(...nodeToOutline.get(node));
264
+
265
+ return;
266
+ }
267
+
268
+ // When exiting a sectioning root element, if the stack is not empty
269
+ if (sectioningRoot.includes(node.nodeName) && (stack.length > 0)) {
270
+ // 1. If the current section has no heading, create an implied heading
271
+ // and let that be the heading for the current section.
272
+ if (!currentSection.heading) {
273
+ currentSection.heading = '__implied';
274
+ }
275
+
276
+ // 2. Let current section be current outline target's parent section.
277
+ currentSection = nodeToParentSection.get(currentOutlineTarget);
278
+
279
+ // A sectioning root generates a separate outline, let's attach it
280
+ // to the main outline
281
+ currentSection.subRoots.push(...nodeToOutline.get(node));
282
+
283
+ // 3. Pop the top element from the stack, and let the current outline
284
+ // target be that element.
285
+ currentOutlineTarget = stack.pop();
286
+
287
+ return;
288
+ }
289
+
290
+ // When exiting a sectioning content element or a sectioning root element
291
+ // (when the stack is empty)
292
+ if (sectioningContent.includes(node.nodeName) ||
293
+ sectioningRoot.includes(node.nodeName)) {
294
+ // If the current section has no heading, create an implied heading and
295
+ // let that be the heading for the current section.
296
+ if (!currentSection.heading) {
297
+ currentSection.heading = '__implied';
298
+ }
299
+
300
+ // Skip to the next step in the overall set of steps. (The walk is over)
301
+ return;
302
+ }
303
+ }
304
+
305
+ innerExit();
306
+
307
+ // In addition, whenever the walk exits a node, after doing the steps above,
308
+ // if the node is not associated with a section yet, associate the node with
309
+ // the section current section.
310
+ // (we will only do that for elements that have an ID)
311
+ if (node.getAttribute('id') && !nodeToSection.has(node)) {
312
+ nodeToSection.set(node, currentSection);
313
+ }
314
+ }
315
+
316
+ // Walk the DOM subtree in depth-first order, entering and exiting nodes.
317
+ function walk(root, enter, exit) {
318
+ let node = root;
319
+ start: while (node) {
320
+ enter(node);
321
+ // Note: HGROUP is composed of multiple sub-headings but represents a
322
+ // single heading, skip its children as that would create ghost
323
+ // subsections
324
+ if ((node.nodeName !== 'HGROUP') && node.firstChild) {
325
+ node = node.firstChild;
326
+ continue start;
327
+ }
328
+ while (node) {
329
+ exit(node);
330
+ if (node == root) {
331
+ node = null;
332
+ } else if (node.nextSibling) {
333
+ node = node.nextSibling;
334
+ continue start;
335
+ } else {
336
+ node = node.parentNode;
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ // 4. Walk over the DOM in tree order, starting with the sectioning content
343
+ // element or sectioning root element at the root of the subtree for which an
344
+ // outline is to be created, and trigger the first relevant step below for
345
+ // each element as the walk enters and exits it.
346
+ walk(root, enter, exit);
347
+
348
+ return {
349
+ outline: nodeToOutline.get(root),
350
+ nodeToSection
351
+ };
352
+ }