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>
355 lines
12 KiB
Text
355 lines
12 KiB
Text
/**
|
|
* @license
|
|
* Copyright 2019 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
/** @typedef {import('./details-renderer').DetailsRenderer} DetailsRenderer */
|
|
/** @typedef {import('./dom').DOM} DOM */
|
|
|
|
import {Util} from '../../shared/util.js';
|
|
import {Globals} from './report-globals.js';
|
|
|
|
/** @enum {number} */
|
|
const LineVisibility = {
|
|
/** Show regardless of whether the snippet is collapsed or expanded */
|
|
ALWAYS: 0,
|
|
WHEN_COLLAPSED: 1,
|
|
WHEN_EXPANDED: 2,
|
|
};
|
|
|
|
/** @enum {number} */
|
|
const LineContentType = {
|
|
/** A line of content */
|
|
CONTENT_NORMAL: 0,
|
|
/** A line of content that's emphasized by setting the CSS background color */
|
|
CONTENT_HIGHLIGHTED: 1,
|
|
/** Use when some lines are hidden, shows the "..." placeholder */
|
|
PLACEHOLDER: 2,
|
|
/** A message about a line of content or the snippet in general */
|
|
MESSAGE: 3,
|
|
};
|
|
|
|
/** @typedef {{
|
|
content: string;
|
|
lineNumber: string | number;
|
|
contentType: LineContentType;
|
|
truncated?: boolean;
|
|
visibility?: LineVisibility;
|
|
}} LineDetails */
|
|
|
|
const classNamesByContentType = {
|
|
[LineContentType.CONTENT_NORMAL]: ['lh-snippet__line--content'],
|
|
[LineContentType.CONTENT_HIGHLIGHTED]: [
|
|
'lh-snippet__line--content',
|
|
'lh-snippet__line--content-highlighted',
|
|
],
|
|
[LineContentType.PLACEHOLDER]: ['lh-snippet__line--placeholder'],
|
|
[LineContentType.MESSAGE]: ['lh-snippet__line--message'],
|
|
};
|
|
|
|
/**
|
|
* @param {LH.Audit.Details.SnippetValue['lines']} lines
|
|
* @param {number} lineNumber
|
|
* @return {{line?: LH.Audit.Details.SnippetValue['lines'][0], previousLine?: LH.Audit.Details.SnippetValue['lines'][0]}}
|
|
*/
|
|
function getLineAndPreviousLine(lines, lineNumber) {
|
|
return {
|
|
line: lines.find(l => l.lineNumber === lineNumber),
|
|
previousLine: lines.find(l => l.lineNumber === lineNumber - 1),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {LH.Audit.Details.SnippetValue["lineMessages"]} messages
|
|
* @param {number} lineNumber
|
|
*/
|
|
function getMessagesForLineNumber(messages, lineNumber) {
|
|
return messages.filter(h => h.lineNumber === lineNumber);
|
|
}
|
|
|
|
/**
|
|
* @param {LH.Audit.Details.SnippetValue} details
|
|
* @return {LH.Audit.Details.SnippetValue['lines']}
|
|
*/
|
|
function getLinesWhenCollapsed(details) {
|
|
const SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED = 2;
|
|
return Util.filterRelevantLines(
|
|
details.lines,
|
|
details.lineMessages,
|
|
SURROUNDING_LINES_TO_SHOW_WHEN_COLLAPSED
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Render snippet of text with line numbers and annotations.
|
|
* By default we only show a few lines around each annotation and the user
|
|
* can click "Expand snippet" to show more.
|
|
* Content lines with annotations are highlighted.
|
|
*/
|
|
export class SnippetRenderer {
|
|
/**
|
|
* @param {DOM} dom
|
|
* @param {LH.Audit.Details.SnippetValue} details
|
|
* @param {DetailsRenderer} detailsRenderer
|
|
* @param {function} toggleExpandedFn
|
|
* @return {DocumentFragment}
|
|
*/
|
|
static renderHeader(dom, details, detailsRenderer, toggleExpandedFn) {
|
|
const linesWhenCollapsed = getLinesWhenCollapsed(details);
|
|
const canExpand = linesWhenCollapsed.length < details.lines.length;
|
|
|
|
const header = dom.createComponent('snippetHeader');
|
|
dom.find('.lh-snippet__title', header).textContent = details.title;
|
|
|
|
const {
|
|
snippetCollapseButtonLabel,
|
|
snippetExpandButtonLabel,
|
|
} = Globals.strings;
|
|
dom.find(
|
|
'.lh-snippet__btn-label-collapse',
|
|
header
|
|
).textContent = snippetCollapseButtonLabel;
|
|
dom.find(
|
|
'.lh-snippet__btn-label-expand',
|
|
header
|
|
).textContent = snippetExpandButtonLabel;
|
|
|
|
const toggleExpandButton = dom.find('.lh-snippet__toggle-expand', header);
|
|
// If we're already showing all the available lines of the snippet, we don't need an
|
|
// expand/collapse button and can remove it from the DOM.
|
|
// If we leave the button in though, wire up the click listener to toggle visibility!
|
|
if (!canExpand) {
|
|
toggleExpandButton.remove();
|
|
} else {
|
|
toggleExpandButton.addEventListener('click', () => toggleExpandedFn());
|
|
}
|
|
|
|
// We only show the source node of the snippet in DevTools because then the user can
|
|
// access the full element detail. Just being able to see the outer HTML isn't very useful.
|
|
if (details.node && dom.isDevTools()) {
|
|
const nodeContainer = dom.find('.lh-snippet__node', header);
|
|
nodeContainer.append(detailsRenderer.renderNode(details.node));
|
|
}
|
|
|
|
return header;
|
|
}
|
|
|
|
/**
|
|
* Renders a line (text content, message, or placeholder) as a DOM element.
|
|
* @param {DOM} dom
|
|
* @param {DocumentFragment} tmpl
|
|
* @param {LineDetails} lineDetails
|
|
* @return {Element}
|
|
*/
|
|
static renderSnippetLine(
|
|
dom,
|
|
tmpl,
|
|
{content, lineNumber, truncated, contentType, visibility}
|
|
) {
|
|
const clonedTemplate = dom.createComponent('snippetLine');
|
|
const contentLine = dom.find('.lh-snippet__line', clonedTemplate);
|
|
const {classList} = contentLine;
|
|
|
|
classNamesByContentType[contentType].forEach(typeClass =>
|
|
classList.add(typeClass)
|
|
);
|
|
|
|
if (visibility === LineVisibility.WHEN_COLLAPSED) {
|
|
classList.add('lh-snippet__show-if-collapsed');
|
|
} else if (visibility === LineVisibility.WHEN_EXPANDED) {
|
|
classList.add('lh-snippet__show-if-expanded');
|
|
}
|
|
|
|
const lineContent = content + (truncated ? '…' : '');
|
|
const lineContentEl = dom.find('.lh-snippet__line code', contentLine);
|
|
if (contentType === LineContentType.MESSAGE) {
|
|
lineContentEl.append(dom.convertMarkdownLinkSnippets(lineContent));
|
|
} else {
|
|
lineContentEl.textContent = lineContent;
|
|
}
|
|
|
|
dom.find(
|
|
'.lh-snippet__line-number',
|
|
contentLine
|
|
).textContent = lineNumber.toString();
|
|
|
|
return contentLine;
|
|
}
|
|
|
|
/**
|
|
* @param {DOM} dom
|
|
* @param {DocumentFragment} tmpl
|
|
* @param {{message: string}} message
|
|
* @return {Element}
|
|
*/
|
|
static renderMessage(dom, tmpl, message) {
|
|
return SnippetRenderer.renderSnippetLine(dom, tmpl, {
|
|
lineNumber: ' ',
|
|
content: message.message,
|
|
contentType: LineContentType.MESSAGE,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {DOM} dom
|
|
* @param {DocumentFragment} tmpl
|
|
* @param {LineVisibility} visibility
|
|
* @return {Element}
|
|
*/
|
|
static renderOmittedLinesPlaceholder(dom, tmpl, visibility) {
|
|
return SnippetRenderer.renderSnippetLine(dom, tmpl, {
|
|
lineNumber: '…',
|
|
content: '',
|
|
visibility,
|
|
contentType: LineContentType.PLACEHOLDER,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @param {DOM} dom
|
|
* @param {DocumentFragment} tmpl
|
|
* @param {LH.Audit.Details.SnippetValue} details
|
|
* @return {DocumentFragment}
|
|
*/
|
|
static renderSnippetContent(dom, tmpl, details) {
|
|
const template = dom.createComponent('snippetContent');
|
|
const snippetEl = dom.find('.lh-snippet__snippet-inner', template);
|
|
|
|
// First render messages that don't belong to specific lines
|
|
details.generalMessages.forEach(m =>
|
|
snippetEl.append(SnippetRenderer.renderMessage(dom, tmpl, m))
|
|
);
|
|
// Then render the lines and their messages, as well as placeholders where lines are omitted
|
|
snippetEl.append(SnippetRenderer.renderSnippetLines(dom, tmpl, details));
|
|
|
|
return template;
|
|
}
|
|
|
|
/**
|
|
* @param {DOM} dom
|
|
* @param {DocumentFragment} tmpl
|
|
* @param {LH.Audit.Details.SnippetValue} details
|
|
* @return {DocumentFragment}
|
|
*/
|
|
static renderSnippetLines(dom, tmpl, details) {
|
|
const {lineMessages, generalMessages, lineCount, lines} = details;
|
|
const linesWhenCollapsed = getLinesWhenCollapsed(details);
|
|
const hasOnlyGeneralMessages =
|
|
generalMessages.length > 0 && lineMessages.length === 0;
|
|
|
|
const lineContainer = dom.createFragment();
|
|
|
|
// When a line is not shown in the collapsed state we try to see if we also need an
|
|
// omitted lines placeholder for the expanded state, rather than rendering two separate
|
|
// placeholders.
|
|
let hasPendingOmittedLinesPlaceholderForCollapsedState = false;
|
|
|
|
for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {
|
|
const {line, previousLine} = getLineAndPreviousLine(lines, lineNumber);
|
|
const {
|
|
line: lineWhenCollapsed,
|
|
previousLine: previousLineWhenCollapsed,
|
|
} = getLineAndPreviousLine(linesWhenCollapsed, lineNumber);
|
|
|
|
const showLineWhenCollapsed = !!lineWhenCollapsed;
|
|
const showPreviousLineWhenCollapsed = !!previousLineWhenCollapsed;
|
|
|
|
// If we went from showing lines in the collapsed state to not showing them
|
|
// we need to render a placeholder
|
|
if (showPreviousLineWhenCollapsed && !showLineWhenCollapsed) {
|
|
hasPendingOmittedLinesPlaceholderForCollapsedState = true;
|
|
}
|
|
// If we are back to lines being visible in the collapsed and the placeholder
|
|
// hasn't been rendered yet then render it now
|
|
if (
|
|
showLineWhenCollapsed &&
|
|
hasPendingOmittedLinesPlaceholderForCollapsedState
|
|
) {
|
|
lineContainer.append(
|
|
SnippetRenderer.renderOmittedLinesPlaceholder(
|
|
dom,
|
|
tmpl,
|
|
LineVisibility.WHEN_COLLAPSED
|
|
)
|
|
);
|
|
hasPendingOmittedLinesPlaceholderForCollapsedState = false;
|
|
}
|
|
|
|
// Render omitted lines placeholder if we have not already rendered one for this gap
|
|
const isFirstOmittedLineWhenExpanded = !line && !!previousLine;
|
|
const isFirstLineOverallAndIsOmittedWhenExpanded =
|
|
!line && lineNumber === 1;
|
|
if (
|
|
isFirstOmittedLineWhenExpanded ||
|
|
isFirstLineOverallAndIsOmittedWhenExpanded
|
|
) {
|
|
// In the collapsed state we don't show omitted lines placeholders around
|
|
// the edges of the snippet
|
|
const hasRenderedAllLinesVisibleWhenCollapsed = !linesWhenCollapsed.some(
|
|
l => l.lineNumber > lineNumber
|
|
);
|
|
const onlyShowWhenExpanded =
|
|
hasRenderedAllLinesVisibleWhenCollapsed || lineNumber === 1;
|
|
lineContainer.append(
|
|
SnippetRenderer.renderOmittedLinesPlaceholder(
|
|
dom,
|
|
tmpl,
|
|
onlyShowWhenExpanded
|
|
? LineVisibility.WHEN_EXPANDED
|
|
: LineVisibility.ALWAYS
|
|
)
|
|
);
|
|
hasPendingOmittedLinesPlaceholderForCollapsedState = false;
|
|
}
|
|
|
|
if (!line) {
|
|
// Can't render the line if we don't know its content (instead we've rendered a placeholder)
|
|
continue;
|
|
}
|
|
|
|
// Now render the line and any messages
|
|
const messages = getMessagesForLineNumber(lineMessages, lineNumber);
|
|
const highlightLine = messages.length > 0 || hasOnlyGeneralMessages;
|
|
const contentLineDetails = Object.assign({}, line, {
|
|
contentType: highlightLine
|
|
? LineContentType.CONTENT_HIGHLIGHTED
|
|
: LineContentType.CONTENT_NORMAL,
|
|
visibility: lineWhenCollapsed
|
|
? LineVisibility.ALWAYS
|
|
: LineVisibility.WHEN_EXPANDED,
|
|
});
|
|
lineContainer.append(
|
|
SnippetRenderer.renderSnippetLine(dom, tmpl, contentLineDetails)
|
|
);
|
|
|
|
messages.forEach(message => {
|
|
lineContainer.append(SnippetRenderer.renderMessage(dom, tmpl, message));
|
|
});
|
|
}
|
|
|
|
return lineContainer;
|
|
}
|
|
|
|
/**
|
|
* @param {DOM} dom
|
|
* @param {LH.Audit.Details.SnippetValue} details
|
|
* @param {DetailsRenderer} detailsRenderer
|
|
* @return {!Element}
|
|
*/
|
|
static render(dom, details, detailsRenderer) {
|
|
const tmpl = dom.createComponent('snippet');
|
|
const snippetEl = dom.find('.lh-snippet', tmpl);
|
|
|
|
const header = SnippetRenderer.renderHeader(
|
|
dom,
|
|
details,
|
|
detailsRenderer,
|
|
() => snippetEl.classList.toggle('lh-snippet--expanded')
|
|
);
|
|
const content = SnippetRenderer.renderSnippetContent(dom, tmpl, details);
|
|
snippetEl.append(header, content);
|
|
|
|
return snippetEl;
|
|
}
|
|
}
|