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>
493 lines
11 KiB
Text
493 lines
11 KiB
Text
/**
|
|
* @license
|
|
* Copyright 2016 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
const ALLOWED_DISPLAY_VALUES = [
|
|
'fullscreen',
|
|
'standalone',
|
|
'minimal-ui',
|
|
'browser',
|
|
];
|
|
/**
|
|
* All display-mode fallbacks, including when unset, lead to default display mode 'browser'.
|
|
* @see https://www.w3.org/TR/2016/WD-appmanifest-20160825/#dfn-default-display-mode
|
|
*/
|
|
const DEFAULT_DISPLAY_MODE = 'browser';
|
|
|
|
const ALLOWED_ORIENTATION_VALUES = [
|
|
'any',
|
|
'natural',
|
|
'landscape',
|
|
'portrait',
|
|
'portrait-primary',
|
|
'portrait-secondary',
|
|
'landscape-primary',
|
|
'landscape-secondary',
|
|
];
|
|
|
|
/**
|
|
* @param {*} raw
|
|
* @param {boolean=} trim
|
|
*/
|
|
function parseString(raw, trim) {
|
|
let value;
|
|
let warning;
|
|
|
|
if (typeof raw === 'string') {
|
|
value = trim ? raw.trim() : raw;
|
|
} else {
|
|
if (raw !== undefined) {
|
|
warning = 'ERROR: expected a string.';
|
|
}
|
|
value = undefined;
|
|
}
|
|
|
|
return {
|
|
raw,
|
|
value,
|
|
warning,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} raw
|
|
*/
|
|
function parseColor(raw) {
|
|
const color = parseString(raw);
|
|
|
|
// Finished if color missing or not a string.
|
|
if (color.value === undefined) {
|
|
return color;
|
|
}
|
|
|
|
return color;
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parseName(jsonInput) {
|
|
return parseString(jsonInput.name, true);
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parseShortName(jsonInput) {
|
|
return parseString(jsonInput.short_name, true);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the urls are of the same origin. See https://html.spec.whatwg.org/#same-origin
|
|
* @param {string} url1
|
|
* @param {string} url2
|
|
* @return {boolean}
|
|
*/
|
|
function checkSameOrigin(url1, url2) {
|
|
const parsed1 = new URL(url1);
|
|
const parsed2 = new URL(url2);
|
|
|
|
return parsed1.origin === parsed2.origin;
|
|
}
|
|
|
|
/**
|
|
* https://www.w3.org/TR/2016/WD-appmanifest-20160825/#start_url-member
|
|
* @param {*} jsonInput
|
|
* @param {string} manifestUrl
|
|
* @param {string} documentUrl
|
|
* @return {{raw: any, value: string, warning?: string}}
|
|
*/
|
|
function parseStartUrl(jsonInput, manifestUrl, documentUrl) {
|
|
const raw = jsonInput.start_url;
|
|
|
|
// 8.10(3) - discard the empty string and non-strings.
|
|
if (raw === '') {
|
|
return {
|
|
raw,
|
|
value: documentUrl,
|
|
warning: 'ERROR: start_url string empty',
|
|
};
|
|
}
|
|
if (raw === undefined) {
|
|
return {
|
|
raw,
|
|
value: documentUrl,
|
|
};
|
|
}
|
|
if (typeof raw !== 'string') {
|
|
return {
|
|
raw,
|
|
value: documentUrl,
|
|
warning: 'ERROR: expected a string.',
|
|
};
|
|
}
|
|
|
|
// 8.10(4) - construct URL with raw as input and manifestUrl as the base.
|
|
let startUrl;
|
|
try {
|
|
startUrl = new URL(raw, manifestUrl).href;
|
|
} catch (e) {
|
|
// 8.10(5) - discard invalid URLs.
|
|
return {
|
|
raw,
|
|
value: documentUrl,
|
|
warning: `ERROR: invalid start_url relative to ${manifestUrl}`,
|
|
};
|
|
}
|
|
|
|
// 8.10(6) - discard start_urls that are not same origin as documentUrl.
|
|
if (!checkSameOrigin(startUrl, documentUrl)) {
|
|
return {
|
|
raw,
|
|
value: documentUrl,
|
|
warning: 'ERROR: start_url must be same-origin as document',
|
|
};
|
|
}
|
|
|
|
return {
|
|
raw,
|
|
value: startUrl,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parseDisplay(jsonInput) {
|
|
const parsedString = parseString(jsonInput.display, true);
|
|
const stringValue = parsedString.value;
|
|
|
|
if (!stringValue) {
|
|
return {
|
|
raw: jsonInput,
|
|
value: DEFAULT_DISPLAY_MODE,
|
|
warning: parsedString.warning,
|
|
};
|
|
}
|
|
|
|
const displayValue = stringValue.toLowerCase();
|
|
if (!ALLOWED_DISPLAY_VALUES.includes(displayValue)) {
|
|
return {
|
|
raw: jsonInput,
|
|
value: DEFAULT_DISPLAY_MODE,
|
|
warning: 'ERROR: \'display\' has invalid value ' + displayValue +
|
|
`. will fall back to ${DEFAULT_DISPLAY_MODE}.`,
|
|
};
|
|
}
|
|
|
|
return {
|
|
raw: jsonInput,
|
|
value: displayValue,
|
|
warning: undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parseOrientation(jsonInput) {
|
|
const orientation = parseString(jsonInput.orientation, true);
|
|
|
|
if (orientation.value &&
|
|
!ALLOWED_ORIENTATION_VALUES.includes(orientation.value.toLowerCase())) {
|
|
orientation.value = undefined;
|
|
orientation.warning = 'ERROR: \'orientation\' has an invalid value, will be ignored.';
|
|
}
|
|
|
|
return orientation;
|
|
}
|
|
|
|
/**
|
|
* @see https://www.w3.org/TR/2016/WD-appmanifest-20160825/#src-member
|
|
* @param {*} raw
|
|
* @param {string} manifestUrl
|
|
*/
|
|
function parseIcon(raw, manifestUrl) {
|
|
// 9.4(3)
|
|
const src = parseString(raw.src, true);
|
|
// 9.4(4) - discard if trimmed value is the empty string.
|
|
if (src.value === '') {
|
|
src.value = undefined;
|
|
}
|
|
if (src.value) {
|
|
try {
|
|
// 9.4(4) - construct URL with manifest URL as the base
|
|
src.value = new URL(src.value, manifestUrl).href;
|
|
} catch (_) {
|
|
// 9.4 "This algorithm will return a URL or undefined."
|
|
src.warning = `ERROR: invalid icon url will be ignored: '${raw.src}'`;
|
|
src.value = undefined;
|
|
}
|
|
}
|
|
|
|
const type = parseString(raw.type, true);
|
|
|
|
const parsedPurpose = parseString(raw.purpose);
|
|
const purpose = {
|
|
raw: raw.purpose,
|
|
value: ['any'],
|
|
/** @type {string|undefined} */
|
|
warning: undefined,
|
|
};
|
|
if (parsedPurpose.value !== undefined) {
|
|
purpose.value = parsedPurpose.value.split(/\s+/).map(value => value.toLowerCase());
|
|
}
|
|
|
|
const density = {
|
|
raw: raw.density,
|
|
value: 1,
|
|
/** @type {string|undefined} */
|
|
warning: undefined,
|
|
};
|
|
if (density.raw !== undefined) {
|
|
density.value = parseFloat(density.raw);
|
|
if (isNaN(density.value) || !isFinite(density.value) || density.value <= 0) {
|
|
density.value = 1;
|
|
density.warning = 'ERROR: icon density cannot be NaN, +∞, or less than or equal to +0.';
|
|
}
|
|
}
|
|
|
|
let sizes;
|
|
const parsedSizes = parseString(raw.sizes);
|
|
if (parsedSizes.value !== undefined) {
|
|
/** @type {Set<string>} */
|
|
const set = new Set();
|
|
parsedSizes.value.trim().split(/\s+/).forEach(size => set.add(size.toLowerCase()));
|
|
sizes = {
|
|
raw: raw.sizes,
|
|
value: set.size > 0 ? Array.from(set) : undefined,
|
|
warning: undefined,
|
|
};
|
|
} else {
|
|
sizes = {...parsedSizes, value: undefined};
|
|
}
|
|
|
|
return {
|
|
raw,
|
|
value: {
|
|
src,
|
|
type,
|
|
density,
|
|
sizes,
|
|
purpose,
|
|
},
|
|
warning: undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
* @param {string} manifestUrl
|
|
*/
|
|
function parseIcons(jsonInput, manifestUrl) {
|
|
const raw = jsonInput.icons;
|
|
|
|
if (raw === undefined) {
|
|
return {
|
|
raw,
|
|
/** @type {Array<ReturnType<typeof parseIcon>>} */
|
|
value: [],
|
|
warning: undefined,
|
|
};
|
|
}
|
|
|
|
if (!Array.isArray(raw)) {
|
|
return {
|
|
raw,
|
|
/** @type {Array<ReturnType<typeof parseIcon>>} */
|
|
value: [],
|
|
warning: 'ERROR: \'icons\' expected to be an array but is not.',
|
|
};
|
|
}
|
|
|
|
const parsedIcons = raw
|
|
// 9.6(3)(1)
|
|
.filter(icon => icon.src !== undefined)
|
|
// 9.6(3)(2)(1)
|
|
.map(icon => parseIcon(icon, manifestUrl));
|
|
|
|
// NOTE: we still lose the specific message on these icons, but it's not possible to surface them
|
|
// without a massive change to the structure and paradigms of `manifest-parser`.
|
|
const ignoredIconsWithWarnings = parsedIcons
|
|
.filter(icon => {
|
|
const possibleWarnings = [icon.warning, icon.value.type.warning, icon.value.src.warning,
|
|
icon.value.sizes.warning, icon.value.density.warning].filter(Boolean);
|
|
const hasSrc = !!icon.value.src.value;
|
|
return !!possibleWarnings.length && !hasSrc;
|
|
});
|
|
|
|
const value = parsedIcons
|
|
// 9.6(3)(2)(2)
|
|
.filter(parsedIcon => parsedIcon.value.src.value !== undefined);
|
|
|
|
return {
|
|
raw,
|
|
value,
|
|
warning: ignoredIconsWithWarnings.length ?
|
|
'WARNING: Some icons were ignored due to warnings.' : undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} raw
|
|
*/
|
|
function parseApplication(raw) {
|
|
const platform = parseString(raw.platform, true);
|
|
const id = parseString(raw.id, true);
|
|
|
|
// 10.2.(2) and 10.2.(3)
|
|
const appUrl = parseString(raw.url, true);
|
|
if (appUrl.value) {
|
|
try {
|
|
// 10.2.(4) - attempt to construct URL.
|
|
appUrl.value = new URL(appUrl.value).href;
|
|
} catch (e) {
|
|
appUrl.value = undefined;
|
|
appUrl.warning = `ERROR: invalid application URL ${raw.url}`;
|
|
}
|
|
}
|
|
|
|
return {
|
|
raw,
|
|
value: {
|
|
platform,
|
|
id,
|
|
url: appUrl,
|
|
},
|
|
warning: undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parseRelatedApplications(jsonInput) {
|
|
const raw = jsonInput.related_applications;
|
|
|
|
if (raw === undefined) {
|
|
return {
|
|
raw,
|
|
value: undefined,
|
|
warning: undefined,
|
|
};
|
|
}
|
|
|
|
if (!Array.isArray(raw)) {
|
|
return {
|
|
raw,
|
|
value: undefined,
|
|
warning: 'ERROR: \'related_applications\' expected to be an array but is not.',
|
|
};
|
|
}
|
|
|
|
// TODO(bckenny): spec says to skip apps missing `platform`, so debug messages
|
|
// on individual apps are lost. Warn instead?
|
|
const value = raw
|
|
.filter(application => !!application.platform)
|
|
.map(parseApplication)
|
|
.filter(parsedApp => !!parsedApp.value.id.value || !!parsedApp.value.url.value);
|
|
|
|
return {
|
|
raw,
|
|
value,
|
|
warning: undefined,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parsePreferRelatedApplications(jsonInput) {
|
|
const raw = jsonInput.prefer_related_applications;
|
|
let value;
|
|
let warning;
|
|
|
|
if (typeof raw === 'boolean') {
|
|
value = raw;
|
|
} else {
|
|
if (raw !== undefined) {
|
|
warning = 'ERROR: \'prefer_related_applications\' expected to be a boolean.';
|
|
}
|
|
value = undefined;
|
|
}
|
|
|
|
return {
|
|
raw,
|
|
value,
|
|
warning,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parseThemeColor(jsonInput) {
|
|
return parseColor(jsonInput.theme_color);
|
|
}
|
|
|
|
/**
|
|
* @param {*} jsonInput
|
|
*/
|
|
function parseBackgroundColor(jsonInput) {
|
|
return parseColor(jsonInput.background_color);
|
|
}
|
|
|
|
/**
|
|
* Parse a manifest from the given inputs.
|
|
* @param {string} string Manifest JSON string.
|
|
* @param {string} manifestUrl URL of manifest file.
|
|
* @param {string} documentUrl URL of document containing manifest link element.
|
|
*/
|
|
function parseManifest(string, manifestUrl, documentUrl) {
|
|
if (manifestUrl === undefined || documentUrl === undefined) {
|
|
throw new Error('Manifest and document URLs required for manifest parsing.');
|
|
}
|
|
|
|
let jsonInput;
|
|
|
|
try {
|
|
jsonInput = JSON.parse(string);
|
|
} catch (e) {
|
|
return {
|
|
raw: string,
|
|
value: undefined,
|
|
warning: 'ERROR: file isn\'t valid JSON: ' + e,
|
|
url: manifestUrl,
|
|
};
|
|
}
|
|
|
|
const manifest = {
|
|
name: parseName(jsonInput),
|
|
short_name: parseShortName(jsonInput),
|
|
start_url: parseStartUrl(jsonInput, manifestUrl, documentUrl),
|
|
display: parseDisplay(jsonInput),
|
|
orientation: parseOrientation(jsonInput),
|
|
icons: parseIcons(jsonInput, manifestUrl),
|
|
related_applications: parseRelatedApplications(jsonInput),
|
|
prefer_related_applications: parsePreferRelatedApplications(jsonInput),
|
|
theme_color: parseThemeColor(jsonInput),
|
|
background_color: parseBackgroundColor(jsonInput),
|
|
};
|
|
|
|
/** @type {string|undefined} */
|
|
let manifestUrlWarning;
|
|
try {
|
|
const manifestUrlParsed = new URL(manifestUrl);
|
|
if (!manifestUrlParsed.protocol.startsWith('http')) {
|
|
manifestUrlWarning = `WARNING: manifest URL not available over a valid network protocol`;
|
|
}
|
|
} catch (_) {
|
|
manifestUrlWarning = `ERROR: invalid manifest URL: '${manifestUrl}'`;
|
|
}
|
|
|
|
return {
|
|
raw: string,
|
|
value: manifest,
|
|
warning: manifestUrlWarning,
|
|
url: manifestUrl,
|
|
};
|
|
}
|
|
|
|
export {parseManifest};
|