@usecds/server 0.0.0-stage → 0.1.1
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.
- package/LICENSE +201 -0
- package/dist/core/content-report.d.ts +37 -0
- package/dist/core/content-report.js +306 -0
- package/dist/core/content-report.js.map +1 -0
- package/dist/core/content-types.d.ts +40 -0
- package/dist/core/content-types.js +27 -0
- package/dist/core/content-types.js.map +1 -0
- package/dist/core/jsonld.d.ts +33 -0
- package/dist/core/jsonld.js +94 -0
- package/dist/core/jsonld.js.map +1 -0
- package/dist/core/media-metadata.d.ts +20 -0
- package/dist/core/media-metadata.js +53 -0
- package/dist/core/media-metadata.js.map +1 -0
- package/dist/core/media-names.d.ts +9 -0
- package/dist/core/media-names.js +58 -0
- package/dist/core/media-names.js.map +1 -0
- package/dist/core/menu.d.ts +33 -0
- package/dist/core/menu.js +89 -0
- package/dist/core/menu.js.map +1 -0
- package/dist/core/publisher.d.ts +77 -0
- package/dist/core/publisher.js +263 -0
- package/dist/core/publisher.js.map +1 -0
- package/dist/core/report.d.ts +54 -0
- package/dist/core/report.js +132 -0
- package/dist/core/report.js.map +1 -0
- package/dist/core/settings.d.ts +12 -0
- package/dist/core/settings.js +40 -0
- package/dist/core/settings.js.map +1 -0
- package/dist/core/site.d.ts +37 -0
- package/dist/core/site.js +96 -0
- package/dist/core/site.js.map +1 -0
- package/dist/core/storage-report.d.ts +44 -0
- package/dist/core/storage-report.js +112 -0
- package/dist/core/storage-report.js.map +1 -0
- package/dist/core/targets.d.ts +63 -0
- package/dist/core/targets.js +213 -0
- package/dist/core/targets.js.map +1 -0
- package/dist/core/translations.d.ts +41 -0
- package/dist/core/translations.js +125 -0
- package/dist/core/translations.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/sources/fixture.d.ts +9 -0
- package/dist/sources/fixture.js +79 -0
- package/dist/sources/fixture.js.map +1 -0
- package/dist/storage/filesystem.d.ts +23 -0
- package/dist/storage/filesystem.js +125 -0
- package/dist/storage/filesystem.js.map +1 -0
- package/dist/types.d.ts +223 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +17 -0
- package/dist/utils.js.map +1 -0
- package/dist/validation.d.ts +23 -0
- package/dist/validation.js +102 -0
- package/dist/validation.js.map +1 -0
- package/package.json +28 -3
- package/schemas/v1/block.json +31 -0
- package/schemas/v1/channel-manifest.json +23 -0
- package/schemas/v1/collection.json +89 -0
- package/schemas/v1/jsonld.json +19 -0
- package/schemas/v1/language.json +12 -0
- package/schemas/v1/media-metadata.json +41 -0
- package/schemas/v1/menu.json +21 -0
- package/schemas/v1/page.json +12 -0
- package/schemas/v1/release-manifest.json +82 -0
- package/schemas/v1/route.json +24 -0
- package/schemas/v1/site.json +21 -0
- package/schemas/v1/target.json +98 -0
- package/README.md +0 -3
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CollectionItem } from "../types.js";
|
|
2
|
+
export declare const JSONLD_COLLECTION = "_jsonld";
|
|
3
|
+
/**
|
|
4
|
+
* Field paths in a definition's map:
|
|
5
|
+
* name, address.city, amenities[0] field of the item (translations[locale] first, then the item)
|
|
6
|
+
* media[0] the item's first media file, as an ImageObject
|
|
7
|
+
* ref:hotels the item's reference into "hotels", with that collection's JSON-LD
|
|
8
|
+
*/
|
|
9
|
+
export declare const JSONLD_PATH: RegExp;
|
|
10
|
+
export interface JsonLdDefinition extends CollectionItem {
|
|
11
|
+
type: string;
|
|
12
|
+
appliesTo?: string;
|
|
13
|
+
values?: Record<string, any>;
|
|
14
|
+
map?: Record<string, string>;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Checks the _jsonld collection against the content. Throws on the first problem:
|
|
18
|
+
* invalid definitions or paths, ref: to unknown collections, references to unknown
|
|
19
|
+
* definitions, and more than one default (appliesTo) per collection.
|
|
20
|
+
*/
|
|
21
|
+
export declare function validateJsonLd(collections: Record<string, CollectionItem[]>): void;
|
|
22
|
+
/**
|
|
23
|
+
* The definition that describes an item: its own reference into _jsonld, else its collection's default.
|
|
24
|
+
*/
|
|
25
|
+
export declare function jsonLdDefinitionFor(collections: Record<string, CollectionItem[]>, collection: string, item: CollectionItem): JsonLdDefinition | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Value of a plain field path (not media[] or ref:) for one locale; undefined if absent.
|
|
28
|
+
*/
|
|
29
|
+
export declare function readFieldPath(item: CollectionItem, locale: string, path: string): unknown;
|
|
30
|
+
/**
|
|
31
|
+
* Whether a mapped path yields nothing for an item and locale (used for report recommendations).
|
|
32
|
+
*/
|
|
33
|
+
export declare function isJsonLdPathEmpty(collections: Record<string, CollectionItem[]>, item: CollectionItem, locale: string, path: string): boolean;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { validateJsonLdDefinition } from "../validation.js";
|
|
2
|
+
// Reserved collection holding schema.org JSON-LD definitions
|
|
3
|
+
export const JSONLD_COLLECTION = "_jsonld";
|
|
4
|
+
/**
|
|
5
|
+
* Field paths in a definition's map:
|
|
6
|
+
* name, address.city, amenities[0] field of the item (translations[locale] first, then the item)
|
|
7
|
+
* media[0] the item's first media file, as an ImageObject
|
|
8
|
+
* ref:hotels the item's reference into "hotels", with that collection's JSON-LD
|
|
9
|
+
*/
|
|
10
|
+
export const JSONLD_PATH = /^(?:media\[\d+\]|ref:[\w-]+|[A-Za-z_]\w*(?:\[\d+\])?(?:\.[A-Za-z_]\w*(?:\[\d+\])?)*)$/;
|
|
11
|
+
/**
|
|
12
|
+
* Checks the _jsonld collection against the content. Throws on the first problem:
|
|
13
|
+
* invalid definitions or paths, ref: to unknown collections, references to unknown
|
|
14
|
+
* definitions, and more than one default (appliesTo) per collection.
|
|
15
|
+
*/
|
|
16
|
+
export function validateJsonLd(collections) {
|
|
17
|
+
const definitions = (collections[JSONLD_COLLECTION] ?? []);
|
|
18
|
+
const ids = new Set(definitions.map((d) => d.id));
|
|
19
|
+
const defaults = new Map();
|
|
20
|
+
for (const def of definitions) {
|
|
21
|
+
validateJsonLdDefinition(def);
|
|
22
|
+
if (def.appliesTo) {
|
|
23
|
+
if (!collections[def.appliesTo]) {
|
|
24
|
+
throw new Error(`Invalid _jsonld item (${def.id}): appliesTo unknown collection "${def.appliesTo}"`);
|
|
25
|
+
}
|
|
26
|
+
const existing = defaults.get(def.appliesTo);
|
|
27
|
+
if (existing) {
|
|
28
|
+
throw new Error(`Collection "${def.appliesTo}" has two JSON-LD defaults: ${existing} and ${def.id}`);
|
|
29
|
+
}
|
|
30
|
+
defaults.set(def.appliesTo, def.id);
|
|
31
|
+
}
|
|
32
|
+
for (const [property, path] of Object.entries(def.map ?? {})) {
|
|
33
|
+
if (!JSONLD_PATH.test(path)) {
|
|
34
|
+
throw new Error(`Invalid _jsonld item (${def.id}): "${property}" has an invalid path "${path}"`);
|
|
35
|
+
}
|
|
36
|
+
const ref = path.match(/^ref:(.+)$/);
|
|
37
|
+
if (ref && !collections[ref[1]]) {
|
|
38
|
+
throw new Error(`Invalid _jsonld item (${def.id}): "${property}" refers to unknown collection "${ref[1]}"`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
for (const [collection, items] of Object.entries(collections)) {
|
|
43
|
+
for (const item of items) {
|
|
44
|
+
for (const ref of item.references ?? []) {
|
|
45
|
+
if (ref.collection === JSONLD_COLLECTION && !ids.has(ref.id)) {
|
|
46
|
+
throw new Error(`Item ${collection}/${item.id} references unknown _jsonld definition "${ref.id}"`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The definition that describes an item: its own reference into _jsonld, else its collection's default.
|
|
54
|
+
*/
|
|
55
|
+
export function jsonLdDefinitionFor(collections, collection, item) {
|
|
56
|
+
const definitions = (collections[JSONLD_COLLECTION] ?? []);
|
|
57
|
+
const own = item.references?.find((r) => r.collection === JSONLD_COLLECTION);
|
|
58
|
+
if (own)
|
|
59
|
+
return definitions.find((d) => d.id === own.id);
|
|
60
|
+
return definitions.find((d) => d.appliesTo === collection);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Value of a plain field path (not media[] or ref:) for one locale; undefined if absent.
|
|
64
|
+
*/
|
|
65
|
+
export function readFieldPath(item, locale, path) {
|
|
66
|
+
const segments = path.split(".");
|
|
67
|
+
const step = (value, segment) => {
|
|
68
|
+
const [, key, index] = segment.match(/^(\w+)(?:\[(\d+)\])?$/) ?? [];
|
|
69
|
+
const next = value?.[key];
|
|
70
|
+
return index === undefined ? next : next?.[Number(index)];
|
|
71
|
+
};
|
|
72
|
+
const [first, ...rest] = segments;
|
|
73
|
+
const localized = step(item.translations[locale], first);
|
|
74
|
+
let value = localized !== undefined ? localized : step(item, first);
|
|
75
|
+
for (const segment of rest)
|
|
76
|
+
value = step(value, segment);
|
|
77
|
+
return value;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether a mapped path yields nothing for an item and locale (used for report recommendations).
|
|
81
|
+
*/
|
|
82
|
+
export function isJsonLdPathEmpty(collections, item, locale, path) {
|
|
83
|
+
const media = path.match(/^media\[(\d+)\]$/);
|
|
84
|
+
if (media)
|
|
85
|
+
return !item.media?.[Number(media[1])];
|
|
86
|
+
const ref = path.match(/^ref:(.+)$/);
|
|
87
|
+
if (ref) {
|
|
88
|
+
const ids = new Set((collections[ref[1]] ?? []).map((i) => i.id));
|
|
89
|
+
return !(item.references ?? []).some((r) => r.collection === ref[1] && ids.has(r.id));
|
|
90
|
+
}
|
|
91
|
+
const value = readFieldPath(item, locale, path);
|
|
92
|
+
return value === undefined || value === null || value === "";
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=jsonld.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonld.js","sourceRoot":"","sources":["../../src/core/jsonld.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CollectionItem, MediaMeta } from "../types.js";
|
|
2
|
+
export declare const MEDIA_COLLECTION = "_media";
|
|
3
|
+
/**
|
|
4
|
+
* Checks the _media collection: every item must match the schema and refer to a media
|
|
5
|
+
* file in the release, and no media file may be described twice. Throws on the first problem.
|
|
6
|
+
* (The publisher has already dropped entries for media that isn't published.)
|
|
7
|
+
*/
|
|
8
|
+
export declare function validateMediaMetadata(items: CollectionItem[], media: Record<string, MediaMeta>): void;
|
|
9
|
+
/**
|
|
10
|
+
* Where media files are used: "all" when listed in an item's media array, otherwise the
|
|
11
|
+
* locales whose translations list it (translations[locale].media, for language-specific media).
|
|
12
|
+
* Media that isn't used anywhere is not published.
|
|
13
|
+
*/
|
|
14
|
+
export type MediaUsage = Map<string, "all" | Set<string>>;
|
|
15
|
+
export declare function collectMediaUsage(collections: Record<string, CollectionItem[]>): MediaUsage;
|
|
16
|
+
/**
|
|
17
|
+
* Locales in which a media file is shown, so its _media texts are expected only there.
|
|
18
|
+
* Undefined means every locale.
|
|
19
|
+
*/
|
|
20
|
+
export declare function mediaLocales(usage: MediaUsage, path: string): string[] | undefined;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { validateMediaMetadataItem } from "../validation.js";
|
|
2
|
+
// Reserved collection holding metadata (alt text, description, focal point, size) per media file
|
|
3
|
+
export const MEDIA_COLLECTION = "_media";
|
|
4
|
+
/**
|
|
5
|
+
* Checks the _media collection: every item must match the schema and refer to a media
|
|
6
|
+
* file in the release, and no media file may be described twice. Throws on the first problem.
|
|
7
|
+
* (The publisher has already dropped entries for media that isn't published.)
|
|
8
|
+
*/
|
|
9
|
+
export function validateMediaMetadata(items, media) {
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for (const item of items) {
|
|
12
|
+
validateMediaMetadataItem(item);
|
|
13
|
+
if (!media[item.id]) {
|
|
14
|
+
throw new Error(`Invalid _media item (${item.id}): no media file with this virtual path in the release`);
|
|
15
|
+
}
|
|
16
|
+
if (seen.has(item.id)) {
|
|
17
|
+
throw new Error(`Invalid _media item (${item.id}): described more than once`);
|
|
18
|
+
}
|
|
19
|
+
seen.add(item.id);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function collectMediaUsage(collections) {
|
|
23
|
+
const usage = new Map();
|
|
24
|
+
for (const [collection, items] of Object.entries(collections)) {
|
|
25
|
+
if (collection === MEDIA_COLLECTION)
|
|
26
|
+
continue;
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
for (const path of item.media ?? [])
|
|
29
|
+
usage.set(path, "all");
|
|
30
|
+
for (const [locale, fields] of Object.entries(item.translations)) {
|
|
31
|
+
const localized = fields?.media;
|
|
32
|
+
if (!Array.isArray(localized))
|
|
33
|
+
continue;
|
|
34
|
+
for (const path of localized) {
|
|
35
|
+
const current = usage.get(path);
|
|
36
|
+
if (current === "all")
|
|
37
|
+
continue;
|
|
38
|
+
usage.set(path, (current ?? new Set()).add(locale));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return usage;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Locales in which a media file is shown, so its _media texts are expected only there.
|
|
47
|
+
* Undefined means every locale.
|
|
48
|
+
*/
|
|
49
|
+
export function mediaLocales(usage, path) {
|
|
50
|
+
const used = usage.get(path);
|
|
51
|
+
return used === undefined || used === "all" ? undefined : [...used].sort();
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=media-metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-metadata.js","sourceRoot":"","sources":["../../src/core/media-metadata.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,yBAAyB,EAAE,MAAM,kBAAkB,CAAC;AAE7D,iGAAiG;AACjG,MAAM,CAAC,MAAM,gBAAgB,GAAG,QAAQ,CAAC;AAEzC;;;;GAIG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAuB,EAAE,KAAgC;IAC7F,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,yBAAyB,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,CAAC,EAAE,wDAAwD,CAAC,CAAC;QAC3G,CAAC;QACD,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,wBAAwB,IAAI,CAAC,EAAE,6BAA6B,CAAC,CAAC;QAChF,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACpB,CAAC;AACH,CAAC;AASD,MAAM,UAAU,iBAAiB,CAAC,WAA6C;IAC7E,MAAM,KAAK,GAAe,IAAI,GAAG,EAAE,CAAC;IACpC,KAAK,MAAM,CAAC,UAAU,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QAC9D,IAAI,UAAU,KAAK,gBAAgB;YAAE,SAAS;QAC9C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,KAAK,IAAI,EAAE;gBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;YAC5D,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,YAAY,CAAC,EAAE,CAAC;gBACjE,MAAM,SAAS,GAAG,MAAM,EAAE,KAAK,CAAC;gBAChC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;oBAAE,SAAS;gBACxC,KAAK,MAAM,IAAI,IAAI,SAAS,EAAE,CAAC;oBAC7B,MAAM,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;oBAChC,IAAI,OAAO,KAAK,KAAK;wBAAE,SAAS;oBAChC,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,OAAO,IAAI,IAAI,GAAG,EAAU,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;gBAC9D,CAAC;YACH,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAiB,EAAE,IAAY;IAC1D,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IAC7B,OAAO,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,EAAE,CAAC;AAC7E,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Explains why a media file name doesn't describe the image (camera/app/AI defaults, ids,
|
|
3
|
+
* dates and numbers only), or returns undefined if it contains at least one descriptive word.
|
|
4
|
+
*/
|
|
5
|
+
export declare function meaninglessNameReason(fileName: string): string | undefined;
|
|
6
|
+
/**
|
|
7
|
+
* A name suggestion from a text such as the alt text: its first meaningful words as a slug.
|
|
8
|
+
*/
|
|
9
|
+
export declare function suggestMediaName(text: string, maxWords?: number): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Words that don't describe what an image shows: camera and app prefixes, generic nouns,
|
|
2
|
+
// AI tool names, versioning words and month names (English and German)
|
|
3
|
+
const FILLER = new Set([
|
|
4
|
+
"img", "image", "images", "photo", "photos", "foto", "fotos", "bild", "bilder", "pic", "pics", "picture", "pictures",
|
|
5
|
+
"screenshot", "screen", "shot", "bildschirmfoto", "scan", "scans", "capture",
|
|
6
|
+
"dsc", "dscn", "dscf", "pxl", "mvimg", "gopr", "dji", "vid", "video", "camera", "cam",
|
|
7
|
+
"whatsapp", "telegram", "signal",
|
|
8
|
+
"chatgpt", "dall", "dalle", "gemini", "generated", "midjourney", "firefly", "copilot",
|
|
9
|
+
"copy", "kopie", "final", "edited", "edit", "new", "neu", "old", "alt", "untitled", "unnamed", "unbenannt",
|
|
10
|
+
"download", "downloads", "file", "datei", "unknown", "default", "temp", "tmp", "test", "version", "export", "uhr",
|
|
11
|
+
"jan", "january", "januar", "feb", "february", "februar", "mar", "march", "maerz", "apr", "april", "may", "mai",
|
|
12
|
+
"jun", "june", "juni", "jul", "july", "juli", "aug", "august", "sep", "sept", "september", "oct", "october",
|
|
13
|
+
"okt", "oktober", "nov", "november", "dec", "december", "dez", "dezember"
|
|
14
|
+
]);
|
|
15
|
+
// Recognizable defaults, used to explain why a name says nothing
|
|
16
|
+
const DEFAULTS = [
|
|
17
|
+
{ reason: "UUID", pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i },
|
|
18
|
+
{ reason: "AI tool default name", pattern: /chatgpt|dall[·.\-_ ]?e|gemini[_ -]?generated|midjourney|firefly|copilot/i },
|
|
19
|
+
{ reason: "screenshot default name", pattern: /screen ?shot|bildschirmfoto|capture/i },
|
|
20
|
+
{ reason: "messenger default name", pattern: /whatsapp|telegram|signal|^photo_\d{4}-\d{2}-\d{2}/i },
|
|
21
|
+
{ reason: "camera default name", pattern: /^(img|dsc[nf]?|pxl|mvimg|gopr|dji|vid)[_-]?\d|^p\d{7}/i },
|
|
22
|
+
{ reason: "hash or random id", pattern: /^[0-9a-f_-]{12,}$/i }
|
|
23
|
+
];
|
|
24
|
+
const STOP_WORDS = new Set(["a", "an", "the", "with", "and", "of", "on", "in", "at", "to", "ein", "eine", "einem", "einer", "der", "die", "das", "mit", "und", "von", "im", "am", "zu", "auf"]);
|
|
25
|
+
const baseName = (path) => path.split("/").pop().replace(/\.[^.]+$/, "");
|
|
26
|
+
/**
|
|
27
|
+
* Explains why a media file name doesn't describe the image (camera/app/AI defaults, ids,
|
|
28
|
+
* dates and numbers only), or returns undefined if it contains at least one descriptive word.
|
|
29
|
+
*/
|
|
30
|
+
export function meaninglessNameReason(fileName) {
|
|
31
|
+
const name = baseName(fileName);
|
|
32
|
+
const words = name
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss")
|
|
35
|
+
.split(/[^a-z0-9]+/)
|
|
36
|
+
// ids and hashes: long hex runs with digits
|
|
37
|
+
.filter((token) => !(/^[0-9a-f]{6,}$/.test(token) && /\d/.test(token)))
|
|
38
|
+
// "img2034" -> "img", "2034"
|
|
39
|
+
.flatMap((token) => token.match(/[a-z]+|\d+/g) ?? [])
|
|
40
|
+
.filter((token) => /^[a-z]{3,}$/.test(token) && !FILLER.has(token));
|
|
41
|
+
if (words.length > 0)
|
|
42
|
+
return undefined;
|
|
43
|
+
return DEFAULTS.find((d) => d.pattern.test(name))?.reason ?? "no descriptive words";
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A name suggestion from a text such as the alt text: its first meaningful words as a slug.
|
|
47
|
+
*/
|
|
48
|
+
export function suggestMediaName(text, maxWords = 5) {
|
|
49
|
+
return text
|
|
50
|
+
.toLowerCase()
|
|
51
|
+
.replace(/ä/g, "ae").replace(/ö/g, "oe").replace(/ü/g, "ue").replace(/ß/g, "ss")
|
|
52
|
+
.normalize("NFKD").replace(/[\u0300-\u036f]/g, "")
|
|
53
|
+
.split(/[^a-z0-9]+/)
|
|
54
|
+
.filter((word) => word && !STOP_WORDS.has(word))
|
|
55
|
+
.slice(0, maxWords)
|
|
56
|
+
.join("-");
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=media-names.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media-names.js","sourceRoot":"","sources":["../../src/core/media-names.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CollectionItem } from "../types.js";
|
|
2
|
+
export declare const MENU_COLLECTION = "_menu";
|
|
3
|
+
export declare const MAX_MENU_DEPTH = 3;
|
|
4
|
+
export interface MenuLink {
|
|
5
|
+
route?: string;
|
|
6
|
+
block?: string;
|
|
7
|
+
url?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface MenuItem extends CollectionItem {
|
|
10
|
+
children?: string[];
|
|
11
|
+
link?: MenuLink;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Checks _menu and the links blocks use. Throws on the first problem: schema errors, unknown
|
|
15
|
+
* children, cycles, menus nested deeper than MAX_MENU_DEPTH, links with neither or both of
|
|
16
|
+
* route/url, unknown routes or blocks, a block that isn't on the route's page, relative URLs,
|
|
17
|
+
* and block links to unknown items.
|
|
18
|
+
*/
|
|
19
|
+
export declare function validateMenu(collections: Record<string, CollectionItem[]>): void;
|
|
20
|
+
/**
|
|
21
|
+
* Menus: _menu items that no other item lists as a child.
|
|
22
|
+
*/
|
|
23
|
+
export declare function menuRoots(collections: Record<string, CollectionItem[]>): MenuItem[];
|
|
24
|
+
/**
|
|
25
|
+
* Entries (items with a link) that have no label in any locale.
|
|
26
|
+
*/
|
|
27
|
+
export declare function menuRecommendations(collections: Record<string, CollectionItem[]>): {
|
|
28
|
+
collection: string;
|
|
29
|
+
id: string;
|
|
30
|
+
issue: string;
|
|
31
|
+
field: string;
|
|
32
|
+
message: string;
|
|
33
|
+
}[];
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { validateMenuItem } from "../validation.js";
|
|
2
|
+
import { BLOCKS_COLLECTION, PAGES_COLLECTION, ROUTES_COLLECTION } from "./site.js";
|
|
3
|
+
// Optional, typed reserved collection of nestable link items
|
|
4
|
+
export const MENU_COLLECTION = "_menu";
|
|
5
|
+
export const MAX_MENU_DEPTH = 3; // entry levels below a menu
|
|
6
|
+
// Protocol-relative //host links are external too
|
|
7
|
+
const EXTERNAL_URL = /^(https?:\/\/|\/\/|mailto:|tel:)/;
|
|
8
|
+
/**
|
|
9
|
+
* Checks _menu and the links blocks use. Throws on the first problem: schema errors, unknown
|
|
10
|
+
* children, cycles, menus nested deeper than MAX_MENU_DEPTH, links with neither or both of
|
|
11
|
+
* route/url, unknown routes or blocks, a block that isn't on the route's page, relative URLs,
|
|
12
|
+
* and block links to unknown items.
|
|
13
|
+
*/
|
|
14
|
+
export function validateMenu(collections) {
|
|
15
|
+
const items = new Map((collections[MENU_COLLECTION] ?? []).map((i) => [i.id, i]));
|
|
16
|
+
const routes = new Map((collections[ROUTES_COLLECTION] ?? []).map((r) => [r.id, r]));
|
|
17
|
+
const pages = new Map((collections[PAGES_COLLECTION] ?? []).map((p) => [p.id, p]));
|
|
18
|
+
const blocks = new Map((collections[BLOCKS_COLLECTION] ?? []).map((b) => [b.id, b]));
|
|
19
|
+
for (const item of items.values()) {
|
|
20
|
+
validateMenuItem(item);
|
|
21
|
+
for (const child of item.children ?? []) {
|
|
22
|
+
if (!items.has(child))
|
|
23
|
+
throw new Error(`Invalid _menu item (${item.id}): unknown child "${child}"`);
|
|
24
|
+
}
|
|
25
|
+
const link = item.link;
|
|
26
|
+
if (!link)
|
|
27
|
+
continue;
|
|
28
|
+
if (!!link.route === !!link.url) {
|
|
29
|
+
throw new Error(`Invalid _menu item (${item.id}): a link needs either a route or a url`);
|
|
30
|
+
}
|
|
31
|
+
if (link.url && !EXTERNAL_URL.test(link.url)) {
|
|
32
|
+
throw new Error(`Invalid _menu item (${item.id}): url must be absolute (https://, //host, mailto:, tel:), got "${link.url}"`);
|
|
33
|
+
}
|
|
34
|
+
if (link.route) {
|
|
35
|
+
const route = routes.get(link.route);
|
|
36
|
+
if (!route)
|
|
37
|
+
throw new Error(`Invalid _menu item (${item.id}): unknown route "${link.route}"`);
|
|
38
|
+
if (link.block) {
|
|
39
|
+
if (!blocks.has(link.block))
|
|
40
|
+
throw new Error(`Invalid _menu item (${item.id}): unknown block "${link.block}"`);
|
|
41
|
+
const onPage = route.page ? pages.get(route.page)?.blocks?.includes(link.block) : false;
|
|
42
|
+
if (!onPage) {
|
|
43
|
+
throw new Error(`Invalid _menu item (${item.id}): block "${link.block}" isn't on the page of route "${link.route}"`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else if (link.block) {
|
|
48
|
+
throw new Error(`Invalid _menu item (${item.id}): a block anchor needs a route`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Cycles and depth, walking down from every item
|
|
52
|
+
const walk = (id, path) => {
|
|
53
|
+
if (path.includes(id))
|
|
54
|
+
throw new Error(`Menu cycle: ${[...path, id].join(" -> ")}`);
|
|
55
|
+
if (path.length > MAX_MENU_DEPTH) {
|
|
56
|
+
throw new Error(`Menu ${path[0]} is nested deeper than ${MAX_MENU_DEPTH} levels (${[...path, id].join(" -> ")})`);
|
|
57
|
+
}
|
|
58
|
+
for (const child of items.get(id)?.children ?? [])
|
|
59
|
+
walk(child, [...path, id]);
|
|
60
|
+
};
|
|
61
|
+
for (const root of menuRoots(collections))
|
|
62
|
+
walk(root.id, []);
|
|
63
|
+
// Items in a cycle aren't reachable from any root, so check them as well
|
|
64
|
+
for (const item of items.values())
|
|
65
|
+
walk(item.id, []);
|
|
66
|
+
for (const block of blocks.values()) {
|
|
67
|
+
for (const linkId of block.links ?? []) {
|
|
68
|
+
if (!items.has(linkId))
|
|
69
|
+
throw new Error(`Invalid _blocks item (${block.id}): unknown _menu link "${linkId}"`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Menus: _menu items that no other item lists as a child.
|
|
75
|
+
*/
|
|
76
|
+
export function menuRoots(collections) {
|
|
77
|
+
const items = (collections[MENU_COLLECTION] ?? []);
|
|
78
|
+
const children = new Set(items.flatMap((i) => i.children ?? []));
|
|
79
|
+
return items.filter((i) => !children.has(i.id));
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Entries (items with a link) that have no label in any locale.
|
|
83
|
+
*/
|
|
84
|
+
export function menuRecommendations(collections) {
|
|
85
|
+
return (collections[MENU_COLLECTION] ?? [])
|
|
86
|
+
.filter((i) => i.link && !Object.values(i.translations).some((t) => typeof t?.label === "string" && t.label))
|
|
87
|
+
.map((i) => ({ collection: MENU_COLLECTION, id: i.id, issue: "missing-label", field: "label", message: `Menu entry ${i.id} has no label in any locale, so it's never shown` }));
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=menu.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"menu.js","sourceRoot":"","sources":["../../src/core/menu.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { ContentSource, ObjectStore, ReleaseManifest, SourceMap } from "../types.js";
|
|
2
|
+
import { StorageReport } from "./storage-report.js";
|
|
3
|
+
import { TranslationReport } from "./translations.js";
|
|
4
|
+
import { TargetDefinition } from "./targets.js";
|
|
5
|
+
import { ContentReport } from "./content-report.js";
|
|
6
|
+
export interface PublisherConfig {
|
|
7
|
+
retentionCount?: number;
|
|
8
|
+
}
|
|
9
|
+
export interface PublishOptions {
|
|
10
|
+
sourceLocale?: string;
|
|
11
|
+
targets?: TargetDefinition[];
|
|
12
|
+
}
|
|
13
|
+
export interface PublishArtifacts {
|
|
14
|
+
sourceMap?: SourceMap & {
|
|
15
|
+
releaseId: string;
|
|
16
|
+
};
|
|
17
|
+
translations: TranslationReport;
|
|
18
|
+
content: ContentReport;
|
|
19
|
+
targets: Record<string, TargetDefinition>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when content fails a target requirement. Nothing has been written to the store.
|
|
23
|
+
* The artifacts (including the content report) are attached for the pipeline.
|
|
24
|
+
*/
|
|
25
|
+
export declare class PublishRequirementsError extends Error {
|
|
26
|
+
artifacts: PublishArtifacts;
|
|
27
|
+
constructor(artifacts: PublishArtifacts);
|
|
28
|
+
}
|
|
29
|
+
export interface PublishResult {
|
|
30
|
+
manifest: ReleaseManifest;
|
|
31
|
+
artifacts: PublishArtifacts;
|
|
32
|
+
}
|
|
33
|
+
export interface GarbageCollectOptions {
|
|
34
|
+
dryRun?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface GarbageCollectResult {
|
|
37
|
+
dryRun: boolean;
|
|
38
|
+
deletedObjects: number;
|
|
39
|
+
deletedMedia: number;
|
|
40
|
+
freedBytes: number;
|
|
41
|
+
report: StorageReport;
|
|
42
|
+
}
|
|
43
|
+
export declare class Publisher {
|
|
44
|
+
private source;
|
|
45
|
+
private store;
|
|
46
|
+
private config;
|
|
47
|
+
constructor(source: ContentSource, store: ObjectStore, config?: PublisherConfig);
|
|
48
|
+
/**
|
|
49
|
+
* Compiles the source content and publishes a new release to the object store.
|
|
50
|
+
* Updates the channel manifest as the final atomic action.
|
|
51
|
+
*/
|
|
52
|
+
publish(channel: string, releaseId: string, options?: PublishOptions): Promise<PublishResult>;
|
|
53
|
+
/**
|
|
54
|
+
* Source locale precedence: publish argument → ContentSource → inferred from markers → "en".
|
|
55
|
+
*/
|
|
56
|
+
private resolveSourceLocale;
|
|
57
|
+
/**
|
|
58
|
+
* Reads the collections of the release a channel currently points to, or undefined
|
|
59
|
+
* if the channel, its release or any of its objects are missing.
|
|
60
|
+
*/
|
|
61
|
+
private readChannelCollections;
|
|
62
|
+
/**
|
|
63
|
+
* Cleans up old releases that exceed the configured retention limit.
|
|
64
|
+
* Releases that a channel currently points to are never deleted.
|
|
65
|
+
*/
|
|
66
|
+
private manageReleaseRetention;
|
|
67
|
+
/**
|
|
68
|
+
* Reports per-release storage usage, diffs between releases, channel pointers
|
|
69
|
+
* and orphaned files. Read-only.
|
|
70
|
+
*/
|
|
71
|
+
analyzeStorage(): Promise<StorageReport>;
|
|
72
|
+
/**
|
|
73
|
+
* Deletes all objects/media not referenced by any retained release.
|
|
74
|
+
* Deletes exactly the orphans listed in the returned report.
|
|
75
|
+
*/
|
|
76
|
+
garbageCollect(options?: GarbageCollectOptions): Promise<GarbageCollectResult>;
|
|
77
|
+
}
|