-
Notifications
You must be signed in to change notification settings - Fork 780
/
heading-order-after.js
115 lines (103 loc) · 3.45 KB
/
heading-order-after.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { matchAncestry } from '../../core/utils';
export default function headingOrderAfter(results) {
// Construct a map of all headings on the page
const headingOrder = getHeadingOrder(results);
results.forEach(result => {
result.result = getHeadingOrderOutcome(result, headingOrder);
});
return results;
}
/**
* Determine check outcome, based on the position of the result in the headingOrder
*/
function getHeadingOrderOutcome(result, headingOrder) {
const index = findHeadingOrderIndex(headingOrder, result.node.ancestry);
const currLevel = headingOrder[index]?.level ?? -1;
const prevLevel = headingOrder[index - 1]?.level ?? -1;
// First heading always passes
if (index === 0) {
return true;
}
// Heading not in the map
if (currLevel === -1) {
return undefined;
}
// Check if a heading is skipped
return currLevel - prevLevel <= 1;
}
/**
* Generate a flattened heading order map, from the data property
* of heading-order results
*/
function getHeadingOrder(results) {
// Ensure parent frames are handled first
results = [...results];
results.sort(({ node: nodeA }, { node: nodeB }) => {
return nodeA.ancestry.length - nodeB.ancestry.length;
});
// push or splice result.data into headingOrder
const headingOrder = results.reduce(mergeHeadingOrder, []);
// Remove all frame placeholders
return headingOrder.filter(({ level }) => level !== -1);
}
/**
* Add the data of a heading-order result to the headingOrder map
*/
function mergeHeadingOrder(mergedHeadingOrder, result) {
const frameHeadingOrder = result.data?.headingOrder;
const frameAncestry = shortenArray(result.node.ancestry, 1);
// Only the first result in each frame has a headingOrder. Ignore the rest
if (!frameHeadingOrder) {
return mergedHeadingOrder;
}
// Prepend node ancestry to each heading.ancestry
const normalizedHeadingOrder = frameHeadingOrder.map(heading => {
return addFrameToHeadingAncestry(heading, frameAncestry);
});
// Find if the result is from a frame previously processed
const index = getFrameIndex(mergedHeadingOrder, frameAncestry);
// heading is not in a frame, stick 'm in at the end.
if (index === -1) {
mergedHeadingOrder.push(...normalizedHeadingOrder);
} else {
mergedHeadingOrder.splice(index, 0, ...normalizedHeadingOrder);
}
return mergedHeadingOrder;
}
/**
* Determine where the iframe results fit into the top-level heading order
*
* If a frame has no headings, but it does have iframes we might not have a result.
* We can account for this by finding the closest ancestor we do know about.
*/
function getFrameIndex(headingOrder, frameAncestry) {
while (frameAncestry.length) {
const index = findHeadingOrderIndex(headingOrder, frameAncestry);
if (index !== -1) {
return index;
}
frameAncestry = shortenArray(frameAncestry, 1);
}
return -1;
}
/**
* Find the index of a heading in the headingOrder by matching ancestries
*/
function findHeadingOrderIndex(headingOrder, ancestry) {
return headingOrder.findIndex(heading => {
return matchAncestry(heading.ancestry, ancestry);
});
}
/**
* Prepend the frame ancestry of a node to heading.ancestry
*/
function addFrameToHeadingAncestry(heading, frameAncestry) {
const ancestry = frameAncestry.concat(heading.ancestry);
return { ...heading, ancestry };
}
/**
* Shorten an array by some number of items
*/
function shortenArray(arr, spliceLength) {
return arr.slice(0, arr.length - spliceLength);
}