Rocky_Mountain_Vending/.pnpm-store/v10/files/1c/a03e51bfc79b1b171a20df933182620af632bdc14a612f52e6e27b00eacb7a277867bcfdc2005f0d6e8b5cd2dc252414a426192fe65de02254fd98cd98bf4d
DMleadgen 46d973904b
Initial commit: Rocky Mountain Vending website
Next.js website for Rocky Mountain Vending company featuring:
- Product catalog with Stripe integration
- Service areas and parts pages
- Admin dashboard with Clerk authentication
- SEO optimized pages with JSON-LD structured data

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-12 16:22:15 -07:00

347 lines
12 KiB
Text

/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview Extracts information about illegible text from the page.
*
* In effort to keep this audit's execution time around 1s, maximum number of protocol calls was limited.
* Firstly, number of visited nodes (text nodes for which font size was checked) is capped.
* Secondly, number of failing nodes that are analyzed (for which detailed CSS information is extracted) is also limited.
*
* The applicable CSS rule is also determined by the code here with some simplifications (namely !important is ignored).
* This gatherer collects stylesheet metadata by itself, instead of relying on the styles gatherer which is slow (because it parses the stylesheet content).
*/
import BaseGatherer from '../../base-gatherer.js';
const FONT_SIZE_PROPERTY_NAME = 'font-size';
const MINIMAL_LEGIBLE_FONT_SIZE_PX = 12;
// limit number of protocol calls to make sure that gatherer doesn't take more than 1-2s
const MAX_NODES_SOURCE_RULE_FETCHED = 50; // number of nodes to fetch the source font-size rule
/** @typedef {LH.Artifacts.FontSize['analyzedFailingNodesData'][0]} NodeFontData */
/** @typedef {Map<number, {fontSize: number, textLength: number}>} BackendIdsToFontData */
/**
* @param {LH.Crdp.CSS.CSSStyle} [style]
* @return {boolean}
*/
function hasFontSizeDeclaration(style) {
return !!style && !!style.cssProperties.find(({name}) => name === FONT_SIZE_PROPERTY_NAME);
}
/**
* Finds the most specific directly matched CSS font-size rule from the list.
*
* @param {Array<LH.Crdp.CSS.RuleMatch>} matchedCSSRules
* @param {function(LH.Crdp.CSS.CSSStyle):boolean|string|undefined} isDeclarationOfInterest
* @return {NodeFontData['cssRule']|undefined}
*/
function findMostSpecificMatchedCSSRule(matchedCSSRules = [], isDeclarationOfInterest) {
let mostSpecificRule;
for (let i = matchedCSSRules.length - 1; i >= 0; i--) {
if (isDeclarationOfInterest(matchedCSSRules[i].rule.style)) {
mostSpecificRule = matchedCSSRules[i].rule;
break;
}
}
if (mostSpecificRule) {
return {
type: 'Regular',
...mostSpecificRule.style,
parentRule: {
origin: mostSpecificRule.origin,
selectors: mostSpecificRule.selectorList.selectors,
},
};
}
}
/**
* Finds the most specific directly matched CSS font-size rule from the list.
*
* @param {Array<LH.Crdp.CSS.InheritedStyleEntry>} [inheritedEntries]
* @return {NodeFontData['cssRule']|undefined}
*/
function findInheritedCSSRule(inheritedEntries = []) {
// The inherited array contains the array of matched rules for all parents in ascending tree order.
// i.e. for an element whose path is `html > body > #main > #nav > p`
// `inherited` will be an array of styles like `[#nav, #main, body, html]`
// The closest parent with font-size will win
for (const {inlineStyle, matchedCSSRules} of inheritedEntries) {
if (hasFontSizeDeclaration(inlineStyle)) return {type: 'Inline', ...inlineStyle};
const directRule = findMostSpecificMatchedCSSRule(matchedCSSRules, hasFontSizeDeclaration);
if (directRule) return directRule;
}
}
/**
* Returns the governing/winning CSS font-size rule for the set of styles given.
* This is roughly a stripped down version of the CSSMatchedStyle class in DevTools.
*
* @see https://cs.chromium.org/chromium/src/third_party/blink/renderer/devtools/front_end/sdk/CSSMatchedStyles.js?q=CSSMatchedStyles+f:devtools+-f:out&sq=package:chromium&dr=C&l=59-134
* @param {LH.Crdp.CSS.GetMatchedStylesForNodeResponse} matched CSS rules
* @return {NodeFontData['cssRule']|undefined}
*/
function getEffectiveFontRule({attributesStyle, inlineStyle, matchedCSSRules, inherited}) {
// Inline styles have highest priority
if (hasFontSizeDeclaration(inlineStyle)) return {type: 'Inline', ...inlineStyle};
// Rules directly referencing the node come next
const matchedRule = findMostSpecificMatchedCSSRule(matchedCSSRules, hasFontSizeDeclaration);
if (matchedRule) return matchedRule;
// Then comes attributes styles (<font size="1">)
if (hasFontSizeDeclaration(attributesStyle)) return {type: 'Attributes', ...attributesStyle};
// Finally, find an inherited property if there is one
const inheritedRule = findInheritedCSSRule(inherited);
if (inheritedRule) return inheritedRule;
return undefined;
}
/**
* @param {string} text
* @return {number}
*/
function getTextLength(text) {
// Array.from to count symbols not unicode code points. See: #6973
return !text ? 0 : Array.from(text.trim()).length;
}
/**
* @param {LH.Gatherer.ProtocolSession} session
* @param {number} nodeId text node
* @return {Promise<NodeFontData['cssRule']|undefined>}
*/
async function fetchSourceRule(session, nodeId) {
const matchedRules = await session.sendCommand('CSS.getMatchedStylesForNode', {
nodeId,
});
const sourceRule = getEffectiveFontRule(matchedRules);
if (!sourceRule) return undefined;
return {
type: sourceRule.type,
range: sourceRule.range,
styleSheetId: sourceRule.styleSheetId,
parentRule: sourceRule.parentRule && {
origin: sourceRule.parentRule.origin,
selectors: sourceRule.parentRule.selectors,
},
};
}
class FontSize extends BaseGatherer {
/** @type {LH.Gatherer.GathererMeta} */
meta = {
supportedModes: ['snapshot', 'navigation'],
};
/**
* @param {LH.Gatherer.ProtocolSession} session
* @param {Array<NodeFontData>} failingNodes
*/
static async fetchFailingNodeSourceRules(session, failingNodes) {
const nodesToAnalyze = failingNodes
.sort((a, b) => b.textLength - a.textLength)
.slice(0, MAX_NODES_SOURCE_RULE_FETCHED);
// DOM.getDocument is necessary for pushNodesByBackendIdsToFrontend to properly retrieve nodeIds if the `DOM` domain was enabled before this gatherer, invoke it to be safe.
await session.sendCommand('DOM.getDocument', {depth: -1, pierce: true});
const {nodeIds} = await session.sendCommand('DOM.pushNodesByBackendIdsToFrontend', {
backendNodeIds: nodesToAnalyze.map(node => node.parentNode.backendNodeId),
});
const analysisPromises = nodesToAnalyze
.map(async (failingNode, i) => {
failingNode.nodeId = nodeIds[i];
try {
const cssRule = await fetchSourceRule(session, nodeIds[i]);
failingNode.cssRule = cssRule;
} catch (err) {
// The node was deleted. We don't need to distinguish between lack-of-rule
// due to a deleted node vs due to failed attribution, so just set to undefined.
failingNode.cssRule = undefined;
}
return failingNode;
});
const analyzedFailingNodesData = await Promise.all(analysisPromises);
const analyzedFailingTextLength = analyzedFailingNodesData.reduce(
(sum, {textLength}) => (sum += textLength),
0
);
return {analyzedFailingNodesData, analyzedFailingTextLength};
}
/**
* Returns the TextNodes in a DOM Snapshot.
* Every entry is associated with a TextNode in the layout tree (not display: none).
* @param {LH.Crdp.DOMSnapshot.CaptureSnapshotResponse} snapshot
*/
getTextNodesInLayoutFromSnapshot(snapshot) {
const strings = snapshot.strings;
/** @param {number} index */
const getString = (index) => strings[index];
/** @param {number} index */
const getFloat = (index) => parseFloat(strings[index]);
const textNodesData = [];
for (let j = 0; j < snapshot.documents.length; j++) {
// `doc` is a flattened property list describing all the Nodes in a document, with all string
// values deduped in the `strings` array.
const doc = snapshot.documents[j];
if (!doc.nodes.backendNodeId || !doc.nodes.parentIndex ||
!doc.nodes.attributes || !doc.nodes.nodeName) {
throw new Error('Unexpected response from DOMSnapshot.captureSnapshot.');
}
const nodes = /** @type {Required<typeof doc['nodes']>} */ (doc.nodes);
/** @param {number} parentIndex */
const getParentData = (parentIndex) => ({
backendNodeId: nodes.backendNodeId[parentIndex],
attributes: nodes.attributes[parentIndex].map(getString),
nodeName: getString(nodes.nodeName[parentIndex]),
});
for (const layoutIndex of doc.textBoxes.layoutIndex) {
const text = strings[doc.layout.text[layoutIndex]];
if (!text) continue;
const nodeIndex = doc.layout.nodeIndex[layoutIndex];
const styles = doc.layout.styles[layoutIndex];
const [fontSizeStringId, visibilityStringId] = styles;
const fontSize = getFloat(fontSizeStringId);
const visibility = getString(visibilityStringId);
const parentIndex = nodes.parentIndex[nodeIndex];
const grandParentIndex = nodes.parentIndex[parentIndex];
const parentNode = getParentData(parentIndex);
const grandParentNode =
grandParentIndex !== undefined ? getParentData(grandParentIndex) : undefined;
textNodesData.push({
nodeIndex,
backendNodeId: nodes.backendNodeId[nodeIndex],
fontSize,
visibility,
textLength: getTextLength(text),
parentNode: {
...parentNode,
parentNode: grandParentNode,
},
});
}
}
return textNodesData;
}
/**
* Get all the failing text nodes that don't meet the legible text threshold.
* @param {LH.Crdp.DOMSnapshot.CaptureSnapshotResponse} snapshot
*/
findFailingNodes(snapshot) {
/** @type {NodeFontData[]} */
const failingNodes = [];
let totalTextLength = 0;
let failingTextLength = 0;
for (const textNodeData of this.getTextNodesInLayoutFromSnapshot(snapshot)) {
if (textNodeData.visibility === 'hidden') {
continue;
}
totalTextLength += textNodeData.textLength;
if (textNodeData.fontSize >= MINIMAL_LEGIBLE_FONT_SIZE_PX) {
continue;
}
// Once a bad TextNode is identified, its parent Node is needed.
failingTextLength += textNodeData.textLength;
failingNodes.push({
nodeId: 0, // Set later in fetchFailingNodeSourceRules.
parentNode: textNodeData.parentNode,
textLength: textNodeData.textLength,
fontSize: textNodeData.fontSize,
});
}
return {totalTextLength, failingTextLength, failingNodes};
}
/**
* @param {LH.Gatherer.Context} passContext
* @return {Promise<LH.Artifacts.FontSize>} font-size analysis
*/
async getArtifact(passContext) {
const session = passContext.driver.defaultSession;
/** @type {Map<string, LH.Crdp.CSS.CSSStyleSheetHeader>} */
const stylesheets = new Map();
/** @param {LH.Crdp.CSS.StyleSheetAddedEvent} sheet */
const onStylesheetAdded = sheet => stylesheets.set(sheet.header.styleSheetId, sheet.header);
session.on('CSS.styleSheetAdded', onStylesheetAdded);
await Promise.all([
session.sendCommand('DOMSnapshot.enable'),
session.sendCommand('DOM.enable'),
session.sendCommand('CSS.enable'),
]);
// Get the computed font-size style of every node.
const snapshot = await session.sendCommand('DOMSnapshot.captureSnapshot', {
computedStyles: ['font-size', 'visibility'],
});
const {
totalTextLength,
failingTextLength,
failingNodes,
} = this.findFailingNodes(snapshot);
const {
analyzedFailingNodesData,
analyzedFailingTextLength,
} = await FontSize.fetchFailingNodeSourceRules(session, failingNodes);
session.off('CSS.styleSheetAdded', onStylesheetAdded);
// For the nodes whose computed style we could attribute to a stylesheet, assign
// the stylsheet to the data.
analyzedFailingNodesData
.filter(data => data.cssRule?.styleSheetId)
// @ts-expect-error - guaranteed to exist from the filter immediately above
.forEach(data => (data.cssRule.stylesheet = stylesheets.get(data.cssRule.styleSheetId)));
await Promise.all([
session.sendCommand('DOMSnapshot.disable'),
session.sendCommand('DOM.disable'),
session.sendCommand('CSS.disable'),
]);
return {
analyzedFailingNodesData,
analyzedFailingTextLength,
failingTextLength,
totalTextLength,
};
}
}
export default FontSize;
export {
getEffectiveFontRule,
findMostSpecificMatchedCSSRule,
};