Initial release of Barcode Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
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 1–17 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 };
|
||||
}
|
||||
Reference in New Issue
Block a user