// ************************************************************************* // * (C)opyright 2026 by Ruben Carlo Benante * // * * // * This program is free software; you can redistribute it and/or modify * // * it under the terms of the GNU General Public License as published by * // * the Free Software Foundation, either version 3 of the License, or * // * (at your option) any later version. * // * * // * This program is distributed in the hope that it will be useful, * // * but WITHOUT ANY WARRANTY; without even the implied warranty of * // * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * // * GNU General Public License for more details. * // * * // * You should have received a copy of the GNU General Public License * // * along with this program. If not, see http://www.gnu.org/licenses/. * // * * // * Contact author at: * // * Ruben Carlo Benante * // * rcb@beco.cc * // ************************************************************************* // The portal drives its three inputs with Inputmask, so the values we store // have to already be in the shape the masks expect (the placeholders on the // page: "2000101010", "01/01/2000", "000.000.000-00"). Normalising here means // the options page accepts a raw paste of digits and stores something the // masks will not fight with. /** Strip everything that is not a digit. */ export function digits(value: string): string { return value.replace(/\D+/g, ''); } /** Registration number: digits only, at most 10. */ export function formatRa(value: string): string { return digits(value).slice(0, 10); } /** Birth date as dd/mm/yyyy, formatted progressively while typing. */ export function formatDate(value: string): string { const d = digits(value).slice(0, 8); const parts: string[] = []; if (d.length > 0) parts.push(d.slice(0, 2)); if (d.length > 2) parts.push(d.slice(2, 4)); if (d.length > 4) parts.push(d.slice(4, 8)); return parts.join('/'); } /** CPF as 000.000.000-00, formatted progressively while typing. */ export function formatCpf(value: string): string { const d = digits(value).slice(0, 11); let out = d.slice(0, 3); if (d.length > 3) out += `.${d.slice(3, 6)}`; if (d.length > 6) out += `.${d.slice(6, 9)}`; if (d.length > 9) out += `-${d.slice(9, 11)}`; return out; } export function isCompleteRa(value: string): boolean { return digits(value).length > 0; } /** * A complete, calendar-valid date. Deliberately checks the round trip rather * than just the digit count, so 31/02/2000 is rejected before it ever reaches * the portal. */ export function isCompleteDate(value: string): boolean { const m = /^(\d{2})\/(\d{2})\/(\d{4})$/.exec(value); if (!m) return false; const day = Number(m[1]); const month = Number(m[2]); const year = Number(m[3]); const date = new Date(year, month - 1, day); return ( date.getFullYear() === year && date.getMonth() === month - 1 && date.getDate() === day ); } export function isCompleteCpf(value: string): boolean { return digits(value).length === 11; } /** * Origin of a user-typed site address, or null if it cannot be parsed. * Accepts input without a scheme ("portal.example.br") by assuming * https, which is what someone pasting an address from the URL bar expects. */ export function toOrigin(value: string): string | null { const trimmed = value.trim(); if (!trimmed) return null; const withScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; try { const url = new URL(withScheme); if (url.protocol !== 'http:' && url.protocol !== 'https:') return null; return url.origin; } catch { return null; } }