85 lines
2.4 KiB
JavaScript
85 lines
2.4 KiB
JavaScript
|
|
import { readFileSync } from "node:fs";
|
||
|
|
|
||
|
|
export function slugify(s) {
|
||
|
|
return String(s)
|
||
|
|
.toLowerCase()
|
||
|
|
.normalize("NFKD")
|
||
|
|
.replace(/[̀-ͯ]/g, "")
|
||
|
|
.replace(/[^a-z0-9]+/g, "-")
|
||
|
|
.replace(/^-+|-+$/g, "")
|
||
|
|
.slice(0, 80);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function titleCase(slug) {
|
||
|
|
return String(slug)
|
||
|
|
.split("-")
|
||
|
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||
|
|
.join(" ");
|
||
|
|
}
|
||
|
|
|
||
|
|
export function todayInTz(tz) {
|
||
|
|
const fmt = new Intl.DateTimeFormat("en-CA", {
|
||
|
|
timeZone: tz,
|
||
|
|
year: "numeric",
|
||
|
|
month: "2-digit",
|
||
|
|
day: "2-digit",
|
||
|
|
});
|
||
|
|
return fmt.format(new Date()); // en-CA => YYYY-MM-DD
|
||
|
|
}
|
||
|
|
|
||
|
|
/** ISO timestamp with Medellín's fixed -05:00 offset, e.g. 2026-06-28T14:37:09-05:00. */
|
||
|
|
export function isoInBogota(date = new Date()) {
|
||
|
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
||
|
|
timeZone: "America/Bogota",
|
||
|
|
year: "numeric",
|
||
|
|
month: "2-digit",
|
||
|
|
day: "2-digit",
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
second: "2-digit",
|
||
|
|
hourCycle: "h23",
|
||
|
|
}).formatToParts(date);
|
||
|
|
const g = (t) => parts.find((p) => p.type === t).value;
|
||
|
|
return `${g("year")}-${g("month")}-${g("day")}T${g("hour")}:${g("minute")}:${g("second")}-05:00`;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** A random Bogotá timestamp earlier today (between 00:00 and now) — varied but never future. */
|
||
|
|
export function randomTimestampTodaySoFar(tz = "America/Bogota") {
|
||
|
|
const today = todayInTz(tz);
|
||
|
|
const startMs = Date.parse(`${today}T00:00:00-05:00`);
|
||
|
|
const nowMs = Date.now();
|
||
|
|
const r = startMs + Math.floor(Math.random() * Math.max(1, nowMs - startMs));
|
||
|
|
return isoInBogota(new Date(r));
|
||
|
|
}
|
||
|
|
|
||
|
|
export function addDays(isoDate, n) {
|
||
|
|
const d = new Date(isoDate + "T00:00:00Z");
|
||
|
|
d.setUTCDate(d.getUTCDate() + n);
|
||
|
|
return d.toISOString().slice(0, 10);
|
||
|
|
}
|
||
|
|
|
||
|
|
export function loadJson(path) {
|
||
|
|
return JSON.parse(readFileSync(path, "utf8"));
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Extract the YAML frontmatter block (between the first two --- lines) as raw text. */
|
||
|
|
export function readFrontmatter(mdx) {
|
||
|
|
const m = mdx.match(/^---\n([\s\S]*?)\n---/);
|
||
|
|
return m ? m[1] : "";
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Minimal frontmatter field readers (good enough for our controlled schema). */
|
||
|
|
export function fmString(fm, key) {
|
||
|
|
const m = fm.match(new RegExp(`^${key}:\\s*"?([^"\\n]+?)"?\\s*$`, "m"));
|
||
|
|
return m ? m[1].trim() : null;
|
||
|
|
}
|
||
|
|
|
||
|
|
export function fmArray(fm, key) {
|
||
|
|
const m = fm.match(new RegExp(`^${key}:\\s*\\[([^\\]]*)\\]`, "m"));
|
||
|
|
if (!m) return [];
|
||
|
|
return m[1]
|
||
|
|
.split(",")
|
||
|
|
.map((s) => s.trim().replace(/^["']|["']$/g, ""))
|
||
|
|
.filter(Boolean);
|
||
|
|
}
|