-
Notifications
You must be signed in to change notification settings - Fork 791
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(heading-order): Prevent crash on page with iframes but no headings (
#2965) * fix(heading-order): Crash on page with iframes but no headings * chore: Fix linter issues * chore: fewer things undefined * chore: attempt to fix IE stuff * chore: refactor * chore: tweak heading-order test * fix: heading-order properly filter placeholders
- Loading branch information
1 parent
042e5f0
commit 4b7db37
Showing
13 changed files
with
1,364 additions
and
1,073 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,144 +1,132 @@ | ||
const joinStr = ' > '; | ||
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; | ||
} | ||
|
||
/** | ||
* Flatten an ancestry path of an iframe result into a string. | ||
* Determine check outcome, based on the position of the result in the headingOrder | ||
*/ | ||
function getFramePath(ancestry, nodePath) { | ||
// remove the last path so we're only left with iframe paths | ||
ancestry = ancestry.slice(0, ancestry.length - 1); | ||
|
||
if (nodePath) { | ||
ancestry = ancestry.concat(nodePath); | ||
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; | ||
} | ||
|
||
return ancestry.join(joinStr); | ||
} | ||
|
||
function headingOrderAfter(results) { | ||
if (results.length < 2) { | ||
return results; | ||
// Heading not in the map | ||
if (currLevel === -1) { | ||
return undefined; | ||
} | ||
// Check if a heading is skipped | ||
return currLevel - prevLevel <= 1; | ||
} | ||
|
||
/** | ||
* In order to correctly return heading order results (even for | ||
* headings that may be out of the current context) we need to | ||
* construct an in-order list of all headings on the page, | ||
* including headings from iframes. | ||
* | ||
* To do this we will find all nested headingOrders (i.e. those | ||
* from iframes) and then determine where those results fit into | ||
* the top-level heading order. once we've put all the heading | ||
* orders into their proper place, we can then determine which | ||
* headings are not in the correct order. | ||
**/ | ||
|
||
// start by replacing all array ancestry paths with a flat string | ||
// path | ||
const pageResult = results.find(result => !result.node._fromFrame); | ||
let headingOrder = pageResult.data.headingOrder.map(heading => { | ||
return { | ||
...heading, | ||
ancestry: getFramePath(pageResult.node.ancestry, heading.ancestry) | ||
}; | ||
}); | ||
|
||
// find all nested headindOrders | ||
const nestedResults = results.filter(result => { | ||
return result.data && result.data.headingOrder && result.node._fromFrame; | ||
}); | ||
|
||
// update the path of nodes to include the iframe path | ||
nestedResults.forEach(result => { | ||
result.data.headingOrder = result.data.headingOrder.map(heading => { | ||
return { | ||
...heading, | ||
ancestry: getFramePath(result.node.ancestry, heading.ancestry) | ||
}; | ||
}); | ||
/** | ||
* 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); | ||
} | ||
|
||
/** | ||
* Determine where the iframe results fit into the top-level | ||
* heading order | ||
*/ | ||
function getFrameIndex(result) { | ||
const path = getFramePath(result.node.ancestry); | ||
const heading = headingOrder.find(heading => { | ||
return heading.ancestry === path; | ||
}); | ||
return headingOrder.indexOf(heading); | ||
} | ||
/** | ||
* 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); | ||
|
||
/** | ||
* Replace an iframe placeholder with its results | ||
*/ | ||
function replaceFrameWithResults(index, result) { | ||
headingOrder.splice(index, 1, ...result.data.headingOrder); | ||
// Only the first result in each frame has a headingOrder. Ignore the rest | ||
if (!frameHeadingOrder) { | ||
return mergedHeadingOrder; | ||
} | ||
|
||
// replace each iframe in the top-level heading order with its | ||
// results. | ||
// since nested iframe results can appear before their parent | ||
// iframe, we will just loop over the nested results and | ||
// piece-meal replace each iframe in the top-level heading order | ||
// with their results until we no longer have results to replace | ||
let replaced = false; | ||
while (nestedResults.length) { | ||
for (let i = 0; i < nestedResults.length; ) { | ||
const nestedResult = nestedResults[i]; | ||
const index = getFrameIndex(nestedResult); | ||
|
||
if (index !== -1) { | ||
replaceFrameWithResults(index, nestedResult); | ||
replaced = true; | ||
// Prepend node ancestry to each heading.ancestry | ||
const normalizedHeadingOrder = frameHeadingOrder.map(heading => { | ||
return addFrameToHeadingAncestry(heading, frameAncestry); | ||
}); | ||
|
||
// remove the nested result from the list | ||
nestedResults.splice(i, 1); | ||
} else { | ||
i++; | ||
} | ||
} | ||
// 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; | ||
} | ||
|
||
// something went wrong if we can't replace an iframe in | ||
// the top-level results | ||
if (!replaced) { | ||
throw new Error('Unable to find parent iframe of heading-order results'); | ||
/** | ||
* 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; | ||
} | ||
|
||
// replace the ancestry path with information about the result | ||
results.forEach(result => { | ||
const path = result.node.ancestry.join(joinStr); | ||
const heading = headingOrder.find(heading => { | ||
return heading.ancestry === path; | ||
}); | ||
const index = headingOrder.indexOf(heading); | ||
if (index > -1) { | ||
headingOrder.splice(index, 1, { | ||
level: headingOrder[index].level, | ||
result | ||
}); | ||
} | ||
/** | ||
* 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); | ||
}); | ||
} | ||
|
||
// remove any iframes that aren't in context (level == -1) | ||
headingOrder = headingOrder.filter(heading => heading.level > 0); | ||
/** | ||
* Prepend the frame ancestry of a node to heading.ancestry | ||
*/ | ||
function addFrameToHeadingAncestry(heading, frameAncestry) { | ||
const ancestry = frameAncestry.concat(heading.ancestry); | ||
return { ...heading, ancestry }; | ||
} | ||
|
||
// now make sure all headings are in the correct order | ||
for (let i = 1; i < results.length; i++) { | ||
const result = results[i]; | ||
const heading = headingOrder.find(heading => { | ||
return heading.result === result; | ||
}); | ||
const index = headingOrder.indexOf(heading); | ||
const currLevel = headingOrder[index].level; | ||
const prevLevel = headingOrder[index - 1].level; | ||
if (currLevel - prevLevel > 1) { | ||
result.result = false; | ||
} | ||
/** | ||
* Check if two ancestries are identical | ||
*/ | ||
function matchAncestry(ancestryA, ancestryB) { | ||
if (ancestryA.length !== ancestryB.length) { | ||
return false; | ||
} | ||
|
||
return results; | ||
return ancestryA.every((selectorA, index) => { | ||
const selectorB = ancestryB[index]; | ||
if (!Array.isArray(selectorA)) { | ||
return selectorA === selectorB; | ||
} | ||
if (selectorA.length !== selectorB.length) { | ||
return false; | ||
} | ||
return selectorA.every((str, index) => selectorB[index] === str); | ||
}); | ||
} | ||
|
||
export default headingOrderAfter; | ||
/** | ||
* Shorten an array by some number of items | ||
*/ | ||
function shortenArray(arr, spliceLength) { | ||
return arr.slice(0, arr.length - spliceLength); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.