Files
barcode-tools/src/barcode/payloads.ts
T

103 lines
3.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export type PayloadKind =
"text" | "url" | "email" | "sms" | "wifi" | "vcard" | "geo";
export interface PayloadFields {
primary: string;
secondary?: string;
tertiary?: string;
hidden?: boolean;
}
function escapeWifi(value: string): string {
return value.replace(/[\\;,:"]/gu, (character) => `\\${character}`);
}
function escapeVcard(value: string): string {
return value
.replaceAll("\\", "\\\\")
.replaceAll("\n", "\\n")
.replaceAll(";", "\\;")
.replaceAll(",", "\\,");
}
export function buildStructuredPayload(
kind: PayloadKind,
fields: PayloadFields,
): string {
const primary = fields.primary.trim();
if (!primary) throw new Error("The primary value is required.");
switch (kind) {
case "text":
return fields.primary;
case "url": {
const url = new URL(primary);
if (!["http:", "https:"].includes(url.protocol))
throw new Error("The URL builder accepts HTTP and HTTPS URLs only.");
return url.href;
}
case "email":
return `mailto:${encodeURIComponent(primary)}${fields.secondary ? `?subject=${encodeURIComponent(fields.secondary)}` : ""}`;
case "sms":
return `SMSTO:${primary}:${fields.secondary ?? ""}`;
case "wifi": {
const supplied = (fields.tertiary || "WPA").trim();
if (!/^(?:WPA|WEP|nopass)$/iu.test(supplied))
throw new Error("Wi-Fi security must be WPA, WEP, or nopass.");
const security =
supplied.toLowerCase() === "nopass" ? "nopass" : supplied.toUpperCase();
return `WIFI:T:${security};S:${escapeWifi(primary)};P:${escapeWifi(fields.secondary ?? "")};H:${fields.hidden ? "true" : "false"};;`;
}
case "vcard":
return [
"BEGIN:VCARD",
"VERSION:3.0",
`FN:${escapeVcard(primary)}`,
fields.secondary ? `TEL:${escapeVcard(fields.secondary)}` : "",
fields.tertiary ? `EMAIL:${escapeVcard(fields.tertiary)}` : "",
"END:VCARD",
]
.filter(Boolean)
.join("\r\n");
case "geo": {
const latitude = Number(primary);
const longitude = Number(fields.secondary);
if (
!Number.isFinite(latitude) ||
latitude < -90 ||
latitude > 90 ||
!Number.isFinite(longitude) ||
longitude < -180 ||
longitude > 180
)
throw new Error("Latitude must be 90…90 and longitude 180…180.");
return `geo:${latitude},${longitude}`;
}
}
}
export function gtinCheckDigit(body: string): number {
if (!/^\d{1,17}$/u.test(body))
throw new Error("A GTIN body must contain 117 decimal digits.");
let sum = 0;
for (
let index = body.length - 1, position = 0;
index >= 0;
index -= 1, position += 1
)
sum += Number(body[index]) * (position % 2 === 0 ? 3 : 1);
return (10 - (sum % 10)) % 10;
}
export function validateGtin(value: string): {
valid: boolean;
expected: string;
} {
if (![8, 12, 13, 14].includes(value.length) || !/^\d+$/u.test(value))
return {
valid: false,
expected: "GTIN length must be 8, 12, 13, or 14 digits.",
};
const expected = String(gtinCheckDigit(value.slice(0, -1)));
return { valid: value.endsWith(expected), expected };
}