@webyy/kontrakt 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Bilder ligger i R2 under nøkler som utledes av innholdet (arkitektur.md 2.6). Repoet lagrer bare
3
+ * nøkkelen, alt-teksten og målene. Samme fil gir alltid samme nøkkel, og et objekt overskrives aldri.
4
+ */
5
+ export declare const BILDEFORMATER: {
6
+ readonly "image/jpeg": "jpg";
7
+ readonly "image/png": "png";
8
+ readonly "image/webp": "webp";
9
+ readonly "image/avif": "avif";
10
+ };
11
+ export type Bildeformat = keyof typeof BILDEFORMATER;
12
+ export declare const BILDENOKKEL: RegExp;
13
+ /** Verdien i et bildefelt, slik den står i innholdsfila. */
14
+ export interface WebyyBildeVerdi {
15
+ nokkel: string;
16
+ alt: string;
17
+ bredde: number;
18
+ hoyde: number;
19
+ }
20
+ export declare function bildenokkel(sha256: string, format: Bildeformat): string;
21
+ /** Alle bildenøkler i en oppføring, uansett hvor i strukturen de står. */
22
+ export declare function finnBildenokler(data: unknown): string[];
package/dist/bilde.js ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Bilder ligger i R2 under nøkler som utledes av innholdet (arkitektur.md 2.6). Repoet lagrer bare
3
+ * nøkkelen, alt-teksten og målene. Samme fil gir alltid samme nøkkel, og et objekt overskrives aldri.
4
+ */
5
+ export const BILDEFORMATER = { "image/jpeg": "jpg", "image/png": "png", "image/webp": "webp", "image/avif": "avif" };
6
+ export const BILDENOKKEL = /^bilder\/[0-9a-f]{64}\.(jpg|png|webp|avif)$/;
7
+ export function bildenokkel(sha256, format) {
8
+ return `bilder/${sha256}.${BILDEFORMATER[format]}`;
9
+ }
10
+ /** Alle bildenøkler i en oppføring, uansett hvor i strukturen de står. */
11
+ export function finnBildenokler(data) {
12
+ if (Array.isArray(data))
13
+ return data.flatMap(finnBildenokler);
14
+ if (typeof data !== "object" || data === null)
15
+ return [];
16
+ const egne = typeof data.nokkel === "string" && BILDENOKKEL.test(data.nokkel) ? [data.nokkel] : [];
17
+ return [...new Set([...egne, ...Object.values(data).flatMap(finnBildenokler)])];
18
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ import { kjorManifest, MANIFESTFIL } from "./node.js";
3
+ const BRUK = `Bruk: webyy manifest [--sjekk] [--uten-sync]
4
+
5
+ manifest Lager ${MANIFESTFIL} fra webyy.config.ts og Astros innholdsskjema.
6
+ --sjekk Skriver ingenting. Feiler hvis ${MANIFESTFIL} er utdatert eller innholdet er ugyldig.
7
+ --uten-sync Hopper over «astro sync» (bruk når den nettopp er kjørt).`;
8
+ const [kommando, ...flagg] = process.argv.slice(2);
9
+ const ukjente = flagg.filter((f) => f !== "--sjekk" && f !== "--uten-sync");
10
+ if (kommando !== "manifest" || ukjente.length > 0) {
11
+ console.error(BRUK);
12
+ process.exit(2);
13
+ }
14
+ try {
15
+ const sjekk = flagg.includes("--sjekk");
16
+ const { manifest, feil } = await kjorManifest(process.cwd(), { sjekk, sync: !flagg.includes("--uten-sync") });
17
+ const samlinger = Object.keys(manifest.samlinger).join(", ");
18
+ if (feil.length > 0) {
19
+ console.error(`webyy: ${feil.length} feil\n${feil.map((f) => ` ${f}`).join("\n")}`);
20
+ process.exit(1);
21
+ }
22
+ console.log(sjekk ? `webyy: ${MANIFESTFIL} er oppdatert og innholdet er gyldig (${samlinger}).` : `webyy: skrev ${MANIFESTFIL} (${samlinger}).`);
23
+ }
24
+ catch (e) {
25
+ console.error(`webyy: ${e instanceof Error ? e.message : e}`);
26
+ process.exit(1);
27
+ }
@@ -0,0 +1,4 @@
1
+ export type { Manifest, ManifestSamling, SamlingKonfig, Skjema, Valideringsfeil, WebyyKonfig, WebyyMerke } from "./typer.js";
2
+ export { defineWebyy, lagManifest, merkSeksjoner, oppforingId, oppforingSti, oppforingUrl, serialiserManifest } from "./manifest.js";
3
+ export { validerInnhold } from "./valider.js";
4
+ export { BILDEFORMATER, BILDENOKKEL, bildenokkel, finnBildenokler, type Bildeformat, type WebyyBildeVerdi } from "./bilde.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { defineWebyy, lagManifest, merkSeksjoner, oppforingId, oppforingSti, oppforingUrl, serialiserManifest } from "./manifest.js";
2
+ export { validerInnhold } from "./valider.js";
3
+ export { BILDEFORMATER, BILDENOKKEL, bildenokkel, finnBildenokler } from "./bilde.js";
@@ -0,0 +1,18 @@
1
+ import type { Manifest, ManifestSamling, Skjema, WebyyKonfig } from "./typer.js";
2
+ export declare function defineWebyy(konfig: WebyyKonfig): WebyyKonfig;
3
+ /**
4
+ * Slår sammen konfigurasjonen med Astros JSON Schema per samling.
5
+ * `skjemaer` er innholdet i `.astro/collections/<navn>.schema.json`.
6
+ */
7
+ export declare function lagManifest(konfig: WebyyKonfig, skjemaer: Record<string, Skjema>): Manifest;
8
+ export declare function serialiserManifest(manifest: Manifest): string;
9
+ /**
10
+ * Merker hver liste der elementene er en discriminated union (oneOf av objekter med en felles
11
+ * `const`-nøkkel) med `webyy.seksjoner`. Editoren og byggeren kjenner seksjonslister på dette.
12
+ */
13
+ export declare function merkSeksjoner(node: unknown): unknown;
14
+ /** Repo-stien til en oppføring. Id-en kan ikke inneholde skråstrek eller punktum. */
15
+ export declare function oppforingSti(samling: ManifestSamling, id: string): string;
16
+ /** Id-en til en fil i samlingen, eller undefined hvis stien ikke hører til samlingen. */
17
+ export declare function oppforingId(samling: ManifestSamling, sti: string): string | undefined;
18
+ export declare function oppforingUrl(samling: ManifestSamling, id: string): string | undefined;
@@ -0,0 +1,89 @@
1
+ export function defineWebyy(konfig) {
2
+ return konfig;
3
+ }
4
+ /**
5
+ * Slår sammen konfigurasjonen med Astros JSON Schema per samling.
6
+ * `skjemaer` er innholdet i `.astro/collections/<navn>.schema.json`.
7
+ */
8
+ export function lagManifest(konfig, skjemaer) {
9
+ kontrollerKonfig(konfig);
10
+ const samlinger = {};
11
+ for (const [navn, samling] of Object.entries(konfig.samlinger)) {
12
+ const skjema = skjemaer[navn];
13
+ if (!skjema) {
14
+ throw new Error(`Samlingen «${navn}» finnes ikke i src/content.config.ts (fant: ${Object.keys(skjemaer).join(", ") || "ingen"}).`);
15
+ }
16
+ const { $schema: _, ...uten } = skjema;
17
+ samlinger[navn] = {
18
+ tittel: samling.tittel,
19
+ mappe: samling.mappe.replace(/\/+$/, ""),
20
+ format: "json",
21
+ ...(samling.url ? { url: samling.url } : {}),
22
+ skjema: merkSeksjoner(uten),
23
+ };
24
+ }
25
+ return { versjon: 1, samlinger, etiketter: konfig.etiketter ?? {} };
26
+ }
27
+ export function serialiserManifest(manifest) {
28
+ return `${JSON.stringify(manifest, null, 2)}\n`;
29
+ }
30
+ /**
31
+ * Merker hver liste der elementene er en discriminated union (oneOf av objekter med en felles
32
+ * `const`-nøkkel) med `webyy.seksjoner`. Editoren og byggeren kjenner seksjonslister på dette.
33
+ */
34
+ export function merkSeksjoner(node) {
35
+ if (Array.isArray(node))
36
+ return node.map(merkSeksjoner);
37
+ if (!erObjekt(node))
38
+ return node;
39
+ const ut = Object.fromEntries(Object.entries(node).map(([k, v]) => [k, merkSeksjoner(v)]));
40
+ const items = ut.items;
41
+ if (ut.type === "array" && erObjekt(items) && Array.isArray(items.oneOf)) {
42
+ const diskriminator = finnDiskriminator(items.oneOf);
43
+ if (diskriminator) {
44
+ ut.webyy = { ...(erObjekt(ut.webyy) ? ut.webyy : {}), seksjoner: { diskriminator } };
45
+ }
46
+ }
47
+ return ut;
48
+ }
49
+ function finnDiskriminator(varianter) {
50
+ const egenskaper = varianter.map((v) => (erObjekt(v) && erObjekt(v.properties) ? v.properties : undefined));
51
+ if (egenskaper.some((e) => !e))
52
+ return undefined;
53
+ const kandidater = Object.keys(egenskaper[0]).filter((k) => egenskaper.every((e) => erObjekt(e[k]) && typeof e[k].const === "string"));
54
+ return kandidater.includes("type") ? "type" : kandidater[0];
55
+ }
56
+ /** Repo-stien til en oppføring. Id-en kan ikke inneholde skråstrek eller punktum. */
57
+ export function oppforingSti(samling, id) {
58
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(id))
59
+ throw new Error(`Ugyldig id «${id}».`);
60
+ return `${samling.mappe}/${id}.json`;
61
+ }
62
+ /** Id-en til en fil i samlingen, eller undefined hvis stien ikke hører til samlingen. */
63
+ export function oppforingId(samling, sti) {
64
+ const treff = sti.startsWith(`${samling.mappe}/`) ? /^([a-z0-9][a-z0-9-]*)\.json$/.exec(sti.slice(samling.mappe.length + 1)) : null;
65
+ return treff?.[1];
66
+ }
67
+ export function oppforingUrl(samling, id) {
68
+ if (!samling.url)
69
+ return undefined;
70
+ return id === "index" ? "/" : samling.url.replace("[id]", id);
71
+ }
72
+ function kontrollerKonfig(konfig) {
73
+ if (!erObjekt(konfig) || !erObjekt(konfig.samlinger) || Object.keys(konfig.samlinger).length === 0) {
74
+ throw new Error("webyy.config.ts må ha minst én samling under «samlinger».");
75
+ }
76
+ for (const [navn, s] of Object.entries(konfig.samlinger)) {
77
+ if (!s.tittel)
78
+ throw new Error(`Samlingen «${navn}» mangler tittel.`);
79
+ if (!s.mappe || s.mappe.startsWith("/") || s.mappe.split("/").includes("..")) {
80
+ throw new Error(`Samlingen «${navn}» må ha «mappe» som relativ sti i repoet.`);
81
+ }
82
+ if (s.url !== undefined && (!s.url.startsWith("/") || !s.url.includes("[id]"))) {
83
+ throw new Error(`«url» i samlingen «${navn}» må starte med / og inneholde [id].`);
84
+ }
85
+ }
86
+ }
87
+ function erObjekt(v) {
88
+ return typeof v === "object" && v !== null && !Array.isArray(v);
89
+ }
package/dist/node.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import type { Manifest } from "./typer.js";
2
+ export declare const MANIFESTFIL = "webyy.json";
3
+ export interface Resultat {
4
+ manifest: Manifest;
5
+ /** Feil som gjør at `--sjekk` feiler. Tom liste betyr at alt er i orden. */
6
+ feil: string[];
7
+ }
8
+ /**
9
+ * Lager manifestet for nettstedet i `rot`. Med `sjekk` skrives ingenting; i stedet sammenlignes
10
+ * manifestet med det som ligger der, og alt eksisterende innhold valideres slik editoren gjør.
11
+ */
12
+ export declare function kjorManifest(rot: string, valg?: {
13
+ sjekk?: boolean;
14
+ sync?: boolean;
15
+ }): Promise<Resultat>;
package/dist/node.js ADDED
@@ -0,0 +1,72 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { readdir, readFile, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { lagManifest, oppforingId, serialiserManifest } from "./manifest.js";
7
+ import { validerInnhold } from "./valider.js";
8
+ export const MANIFESTFIL = "webyy.json";
9
+ const KONFIGFILER = ["webyy.config.ts", "webyy.config.mjs", "webyy.config.js"];
10
+ /**
11
+ * Lager manifestet for nettstedet i `rot`. Med `sjekk` skrives ingenting; i stedet sammenlignes
12
+ * manifestet med det som ligger der, og alt eksisterende innhold valideres slik editoren gjør.
13
+ */
14
+ export async function kjorManifest(rot, valg = {}) {
15
+ const konfig = await lesKonfig(rot);
16
+ if (valg.sync !== false)
17
+ astroSync(rot);
18
+ const manifest = lagManifest(konfig, await lesSkjemaer(rot));
19
+ const tekst = serialiserManifest(manifest);
20
+ const fil = join(rot, MANIFESTFIL);
21
+ if (!valg.sjekk) {
22
+ await writeFile(fil, tekst);
23
+ return { manifest, feil: [] };
24
+ }
25
+ const feil = [];
26
+ const gammel = existsSync(fil) ? await readFile(fil, "utf8") : undefined;
27
+ if (gammel !== tekst) {
28
+ feil.push(`${MANIFESTFIL} er ${gammel === undefined ? "ikke laget" : "utdatert"}. Kjør «npx webyy manifest» og commit fila.`);
29
+ }
30
+ for (const [navn, samling] of Object.entries(manifest.samlinger)) {
31
+ const mappe = join(rot, samling.mappe);
32
+ if (!existsSync(mappe)) {
33
+ feil.push(`Mappa ${samling.mappe} for samlingen «${navn}» finnes ikke.`);
34
+ continue;
35
+ }
36
+ for (const filnavn of (await readdir(mappe)).filter((f) => f.endsWith(".json")).sort()) {
37
+ const sti = `${samling.mappe}/${filnavn}`;
38
+ if (!oppforingId(samling, sti)) {
39
+ feil.push(`${sti}: filnavnet må bestå av små bokstaver, tall og bindestrek.`);
40
+ continue;
41
+ }
42
+ const data = JSON.parse(await readFile(join(rot, sti), "utf8"));
43
+ for (const f of validerInnhold(samling.skjema, data))
44
+ feil.push(`${sti}${f.sti ? ` ${f.sti}` : ""}: ${f.melding}`);
45
+ }
46
+ }
47
+ return { manifest, feil };
48
+ }
49
+ async function lesKonfig(rot) {
50
+ const fil = KONFIGFILER.map((f) => join(rot, f)).find(existsSync);
51
+ if (!fil)
52
+ throw new Error(`Fant ikke ${KONFIGFILER[0]} i ${rot}.`);
53
+ const modul = await import(pathToFileURL(fil).href);
54
+ return modul.default;
55
+ }
56
+ async function lesSkjemaer(rot) {
57
+ const mappe = join(rot, ".astro", "collections");
58
+ if (!existsSync(mappe))
59
+ throw new Error("Fant ikke .astro/collections. Kjør «astro sync» eller dropp --uten-sync.");
60
+ const skjemaer = {};
61
+ for (const fil of await readdir(mappe)) {
62
+ const navn = /^(.+)\.schema\.json$/.exec(fil)?.[1];
63
+ if (navn)
64
+ skjemaer[navn] = JSON.parse(await readFile(join(mappe, fil), "utf8"));
65
+ }
66
+ return skjemaer;
67
+ }
68
+ function astroSync(rot) {
69
+ const kjoring = spawnSync("npx", ["--no-install", "astro", "sync"], { cwd: rot, stdio: ["ignore", "ignore", "inherit"] });
70
+ if (kjoring.status !== 0)
71
+ throw new Error("«astro sync» feilet. Se feilmeldingen over.");
72
+ }
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Bildefelt for content collections. Verdien er en referanse til R2, ikke en fil i repoet.
4
+ * Gjengis med `<WebyyBilde>` og optimaliseres av Astro ved bygg.
5
+ */
6
+ export declare function webyyBilde(): z.ZodObject<{
7
+ nokkel: z.ZodString;
8
+ alt: z.ZodString;
9
+ bredde: z.ZodNumber;
10
+ hoyde: z.ZodNumber;
11
+ }, z.core.$strip>;
package/dist/skjema.js ADDED
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { BILDENOKKEL } from "./bilde.js";
3
+ /**
4
+ * Bildefelt for content collections. Verdien er en referanse til R2, ikke en fil i repoet.
5
+ * Gjengis med `<WebyyBilde>` og optimaliseres av Astro ved bygg.
6
+ */
7
+ export function webyyBilde() {
8
+ return z
9
+ .object({
10
+ nokkel: z.string().regex(BILDENOKKEL),
11
+ alt: z.string().min(1).meta({ title: "Bildetekst for skjermlesere" }),
12
+ bredde: z.number().int().positive(),
13
+ hoyde: z.number().int().positive(),
14
+ })
15
+ .meta({ webyy: { felt: "bilde" } });
16
+ }
@@ -0,0 +1,39 @@
1
+ /** JSON Schema (draft 2020-12) slik Astro skriver det, pluss Webyys merker under `webyy`. */
2
+ export type Skjema = {
3
+ [nokkel: string]: unknown;
4
+ };
5
+ /** En samling er en mappe med én JSON-fil per oppføring; filnavnet uten `.json` er id-en. */
6
+ export interface SamlingKonfig {
7
+ tittel: string;
8
+ mappe: string;
9
+ /** Adressen en oppføring får, med `[id]` som plassholder. Id-en `index` gir `/`. */
10
+ url?: string;
11
+ }
12
+ export interface WebyyKonfig {
13
+ /** Nøklene er navnene på samlingene i `src/content.config.ts`. */
14
+ samlinger: Record<string, SamlingKonfig>;
15
+ /** Norske visningsnavn for feltnøkler og seksjonstyper, f.eks. `{ lead: "Ingress" }`. */
16
+ etiketter?: Record<string, string>;
17
+ }
18
+ export interface ManifestSamling extends SamlingKonfig {
19
+ format: "json";
20
+ skjema: Skjema;
21
+ }
22
+ export interface Manifest {
23
+ versjon: 1;
24
+ samlinger: Record<string, ManifestSamling>;
25
+ etiketter: Record<string, string>;
26
+ }
27
+ /** Merker utvikleren setter med `.meta({ webyy: … })`, og som genereringen legger til. */
28
+ export interface WebyyMerke {
29
+ felt?: "flerlinje" | "bilde" | "lenke";
30
+ /** Settes av genereringen på lister med seksjoner (discriminated union). */
31
+ seksjoner?: {
32
+ diskriminator: string;
33
+ };
34
+ }
35
+ export interface Valideringsfeil {
36
+ /** JSON Pointer til feltet, f.eks. `/sections/0/title`. Tom streng er hele oppføringen. */
37
+ sti: string;
38
+ melding: string;
39
+ }
package/dist/typer.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ import type { Skjema, Valideringsfeil } from "./typer.js";
2
+ /**
3
+ * Validerer én oppføring mot samlingens skjema fra manifestet. Brukes likt i nettleseren,
4
+ * i API-et før commit og i `webyy manifest --sjekk`. Tom liste betyr gyldig.
5
+ */
6
+ export declare function validerInnhold(skjema: Skjema, data: unknown): Valideringsfeil[];
@@ -0,0 +1,115 @@
1
+ import { Validator } from "@cfworker/json-schema";
2
+ const validatorer = new WeakMap();
3
+ /** Ord som bare sier at et barn feilet. Barnets egen feil rapporteres for seg. */
4
+ const OMSLUTTENDE = new Set(["properties", "items", "prefixItems", "allOf", "anyOf", "oneOf", "if", "then", "else", "$ref", "dependentSchemas"]);
5
+ /**
6
+ * Validerer én oppføring mot samlingens skjema fra manifestet. Brukes likt i nettleseren,
7
+ * i API-et før commit og i `webyy manifest --sjekk`. Tom liste betyr gyldig.
8
+ */
9
+ export function validerInnhold(skjema, data) {
10
+ let kompilert = validatorer.get(skjema);
11
+ if (!kompilert) {
12
+ const omskrevet = velgSeksjonEtterType(skjema);
13
+ kompilert = { validator: new Validator(omskrevet, "2020-12", false), skjema: omskrevet };
14
+ validatorer.set(skjema, kompilert);
15
+ }
16
+ const feil = [...kompilert.validator.validate(data).errors]
17
+ .filter((e) => !OMSLUTTENDE.has(e.keyword))
18
+ .map((e) => oversett(e, kompilert.skjema));
19
+ feil.push(...finnHtml(data, ""));
20
+ const sett = new Set();
21
+ return feil.filter((f) => !sett.has(f.sti) && sett.add(f.sti));
22
+ }
23
+ /**
24
+ * En seksjonsliste er `oneOf`, som gir én feil per variant når én seksjon er ugyldig. Her velges
25
+ * varianten etter diskriminatoren, slik at feilen gjelder feltet som faktisk er galt.
26
+ */
27
+ function velgSeksjonEtterType(node) {
28
+ if (Array.isArray(node))
29
+ return node.map(velgSeksjonEtterType);
30
+ if (!erObjekt(node))
31
+ return node;
32
+ const ut = Object.fromEntries(Object.entries(node).map(([k, v]) => [k, velgSeksjonEtterType(v)]));
33
+ const merke = erObjekt(ut.webyy) && erObjekt(ut.webyy.seksjoner) ? ut.webyy.seksjoner : undefined;
34
+ if (merke && erObjekt(ut.items) && Array.isArray(ut.items.oneOf)) {
35
+ const d = merke.diskriminator;
36
+ const varianter = ut.items.oneOf;
37
+ const typer = varianter.map((v) => v.properties[d].const);
38
+ ut.items = {
39
+ type: "object",
40
+ required: [d],
41
+ properties: { [d]: { enum: typer } },
42
+ allOf: varianter.map((v, i) => ({ if: { properties: { [d]: { const: typer[i] } } }, then: v })),
43
+ };
44
+ }
45
+ return ut;
46
+ }
47
+ function oversett(e, skjema) {
48
+ let sti = e.instanceLocation.replace(/^#/, "");
49
+ const verdi = slaOpp(skjema, e.keywordLocation);
50
+ switch (e.keyword) {
51
+ case "required": {
52
+ const felt = /"([^"]+)"/.exec(e.error)?.[1];
53
+ if (felt)
54
+ sti = `${sti}/${felt.replace(/~/g, "~0").replace(/\//g, "~1")}`;
55
+ return { sti, melding: "Må fylles ut." };
56
+ }
57
+ case "type":
58
+ return { sti, melding: `Feil type, forventet ${TYPER[String(verdi)] ?? verdi}.` };
59
+ case "enum":
60
+ return { sti, melding: `Må være en av: ${verdi.join(", ")}.` };
61
+ case "const":
62
+ return { sti, melding: `Må være «${verdi}».` };
63
+ case "minLength":
64
+ return { sti, melding: verdi === 1 ? "Må fylles ut." : `Må ha minst ${verdi} tegn.` };
65
+ case "maxLength":
66
+ return { sti, melding: `Kan ha høyst ${verdi} tegn.` };
67
+ case "minItems":
68
+ return { sti, melding: `Må ha minst ${verdi} element.` };
69
+ case "maxItems":
70
+ return { sti, melding: `Kan ha høyst ${verdi} element.` };
71
+ case "minimum":
72
+ case "exclusiveMinimum":
73
+ return { sti, melding: `Må være minst ${verdi}.` };
74
+ case "maximum":
75
+ case "exclusiveMaximum":
76
+ return { sti, melding: `Kan være høyst ${verdi}.` };
77
+ case "format":
78
+ return { sti, melding: FORMATER[String(verdi)] ?? "Har feil format." };
79
+ case "pattern":
80
+ return { sti, melding: "Har feil format." };
81
+ default:
82
+ return { sti, melding: e.error };
83
+ }
84
+ }
85
+ const TYPER = { string: "tekst", number: "tall", integer: "heltall", boolean: "ja/nei", array: "liste", object: "gruppe" };
86
+ const FORMATER = { email: "Må være en gyldig e-postadresse.", uri: "Må være en gyldig adresse (https://…)." };
87
+ /**
88
+ * Tekst er ren tekst: ingen HTML, og ingen lenker som kjører kode. Dette er et ekstra lag;
89
+ * Astro escaper fortsatt alt ved gjengivelse.
90
+ */
91
+ function finnHtml(data, sti) {
92
+ if (typeof data === "string") {
93
+ if (/<[a-z/!?]/i.test(data))
94
+ return [{ sti, melding: "HTML er ikke tillatt i tekst." }];
95
+ if (/^\s*(javascript|data|vbscript):/i.test(data.replace(/[\u0000-\u001f]/g, "")))
96
+ return [{ sti, melding: "Lenken er ikke tillatt." }];
97
+ return [];
98
+ }
99
+ if (Array.isArray(data))
100
+ return data.flatMap((v, i) => finnHtml(v, `${sti}/${i}`));
101
+ if (erObjekt(data))
102
+ return Object.entries(data).flatMap(([k, v]) => finnHtml(v, `${sti}/${k.replace(/~/g, "~0").replace(/\//g, "~1")}`));
103
+ return [];
104
+ }
105
+ function slaOpp(skjema, peker) {
106
+ return peker
107
+ .replace(/^#\/?/, "")
108
+ .split("/")
109
+ .filter(Boolean)
110
+ .map((del) => decodeURIComponent(del).replace(/~1/g, "/").replace(/~0/g, "~"))
111
+ .reduce((node, del) => node?.[del], skjema);
112
+ }
113
+ function erObjekt(v) {
114
+ return typeof v === "object" && v !== null && !Array.isArray(v);
115
+ }
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@webyy/kontrakt",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Kontrakten mellom et Astro-nettsted og Webyy-editoren: webyy.config.ts → webyy.json, og validering av innhold.",
5
+ "license": "UNLICENSED",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "default": "./dist/index.js"
11
+ },
12
+ "./node": {
13
+ "types": "./dist/node.d.ts",
14
+ "default": "./dist/node.js"
15
+ },
16
+ "./skjema": {
17
+ "types": "./dist/skjema.d.ts",
18
+ "default": "./dist/skjema.js"
19
+ }
20
+ },
21
+ "bin": {
22
+ "webyy": "./dist/cli.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "prepare": "npm run build",
30
+ "test": "vitest run",
31
+ "typecheck": "tsc --noEmit"
32
+ },
33
+ "dependencies": {
34
+ "@cfworker/json-schema": "^4.1.1"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^24.0.0",
38
+ "typescript": "^5.9.3",
39
+ "vitest": "^5.0.5",
40
+ "zod": "^4.6.7"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "peerDependencies": {
46
+ "zod": "^4.0.0"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://github.com/nettskaperne/webyy.git",
51
+ "directory": "packages/kontrakt"
52
+ },
53
+ "homepage": "https://webyy.dev"
54
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.