@jxsuite/parser 1.4.1 → 1.5.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.
- package/dist/content-loader.js +267 -11
- package/dist/content.js +105 -6
- package/dist/markdown.js +129 -12
- package/dist/serialize.js +29 -4
- package/package.json +8 -8
- package/src/Markdown.class.json +1 -1
- package/src/content-loader.ts +200 -14
- package/src/content.ts +38 -7
- package/src/dates.ts +184 -0
- package/src/markdown.ts +13 -1
- package/src/md.ts +52 -4
- package/src/serialize.ts +28 -3
- package/src/transpile.ts +25 -6
package/dist/content-loader.js
CHANGED
|
@@ -80,6 +80,184 @@ function assetUrlFor(mounts, absolutePath) {
|
|
|
80
80
|
}
|
|
81
81
|
var URL_TERMINATOR = String.raw`"'\s<>()\\`;
|
|
82
82
|
|
|
83
|
+
// ../../packages/schema/src/locale.ts
|
|
84
|
+
var RTL_SCRIPTS = new Set([
|
|
85
|
+
"Adlm",
|
|
86
|
+
"Arab",
|
|
87
|
+
"Aran",
|
|
88
|
+
"Armi",
|
|
89
|
+
"Avst",
|
|
90
|
+
"Cprt",
|
|
91
|
+
"Egyd",
|
|
92
|
+
"Egyh",
|
|
93
|
+
"Elym",
|
|
94
|
+
"Gara",
|
|
95
|
+
"Hatr",
|
|
96
|
+
"Hebr",
|
|
97
|
+
"Hluw",
|
|
98
|
+
"Hung",
|
|
99
|
+
"Khar",
|
|
100
|
+
"Lydi",
|
|
101
|
+
"Mand",
|
|
102
|
+
"Mani",
|
|
103
|
+
"Mend",
|
|
104
|
+
"Merc",
|
|
105
|
+
"Mero",
|
|
106
|
+
"Narb",
|
|
107
|
+
"Nbat",
|
|
108
|
+
"Nkoo",
|
|
109
|
+
"Orkh",
|
|
110
|
+
"Palm",
|
|
111
|
+
"Phli",
|
|
112
|
+
"Phlp",
|
|
113
|
+
"Phnx",
|
|
114
|
+
"Prti",
|
|
115
|
+
"Rohg",
|
|
116
|
+
"Samr",
|
|
117
|
+
"Sarb",
|
|
118
|
+
"Sogd",
|
|
119
|
+
"Sogo",
|
|
120
|
+
"Syrc",
|
|
121
|
+
"Thaa",
|
|
122
|
+
"Todr",
|
|
123
|
+
"Yezi"
|
|
124
|
+
]);
|
|
125
|
+
function canonicalizeLocale(tag) {
|
|
126
|
+
if (typeof tag !== "string" || tag.trim() === "") {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return new Intl.Locale(tag.trim()).toString();
|
|
131
|
+
} catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
var DEFAULT_ROUTING = "prefix-except-default";
|
|
136
|
+
function resolveI18n(projectConfig) {
|
|
137
|
+
const raw = projectConfig.i18n;
|
|
138
|
+
const errors = [];
|
|
139
|
+
if (!raw || typeof raw !== "object") {
|
|
140
|
+
return { errors, i18n: null };
|
|
141
|
+
}
|
|
142
|
+
const declared = Array.isArray(raw.locales) ? raw.locales : [];
|
|
143
|
+
const locales = [];
|
|
144
|
+
for (const tag of declared) {
|
|
145
|
+
const canonical = canonicalizeLocale(tag);
|
|
146
|
+
if (canonical === null) {
|
|
147
|
+
errors.push(`i18n.locales: "${String(tag)}" is not a well-formed BCP 47 language tag.`);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (!locales.includes(canonical)) {
|
|
151
|
+
locales.push(canonical);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const defaultLocale = canonicalizeLocale(raw.defaultLocale) ?? locales[0] ?? null;
|
|
155
|
+
if (raw.defaultLocale !== undefined && canonicalizeLocale(raw.defaultLocale) === null) {
|
|
156
|
+
errors.push(`i18n.defaultLocale: "${String(raw.defaultLocale)}" is not a well-formed BCP 47 language tag.`);
|
|
157
|
+
}
|
|
158
|
+
if (defaultLocale === null) {
|
|
159
|
+
errors.push("i18n is declared with no usable locale — set `defaultLocale` or `locales`.");
|
|
160
|
+
return { errors, i18n: null };
|
|
161
|
+
}
|
|
162
|
+
if (!locales.includes(defaultLocale)) {
|
|
163
|
+
locales.unshift(defaultLocale);
|
|
164
|
+
}
|
|
165
|
+
const routing = raw.routing === "prefix-always" || raw.routing === "prefix-except-default" ? raw.routing : DEFAULT_ROUTING;
|
|
166
|
+
return { errors, i18n: { defaultLocale, locales, routing } };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/dates.ts
|
|
170
|
+
var DATE_FORMATS = ["date", "date-time"];
|
|
171
|
+
function isDateFormat(format) {
|
|
172
|
+
return typeof format === "string" && DATE_FORMATS.includes(format);
|
|
173
|
+
}
|
|
174
|
+
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
175
|
+
var RFC_3339 = /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?([Zz]|[+-]\d{2}:\d{2})?$/;
|
|
176
|
+
function pad(n, width = 2) {
|
|
177
|
+
return String(n).padStart(width, "0");
|
|
178
|
+
}
|
|
179
|
+
function toDateOnly(d) {
|
|
180
|
+
return `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
181
|
+
}
|
|
182
|
+
function toInstant(d) {
|
|
183
|
+
return `${toDateOnly(d)}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
184
|
+
}
|
|
185
|
+
function parseDateValue(value, format) {
|
|
186
|
+
if (value instanceof Date) {
|
|
187
|
+
return Number.isNaN(value.getTime()) ? null : format === "date" ? toDateOnly(value) : toInstant(value);
|
|
188
|
+
}
|
|
189
|
+
if (typeof value !== "string") {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const text = value.trim();
|
|
193
|
+
if (text === "") {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
if (DATE_ONLY.test(text)) {
|
|
197
|
+
const d = new Date(`${text}T00:00:00Z`);
|
|
198
|
+
if (Number.isNaN(d.getTime())) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return format === "date" ? text : toInstant(d);
|
|
202
|
+
}
|
|
203
|
+
if (RFC_3339.test(text)) {
|
|
204
|
+
const normalized = /[Zz]|[+-]\d{2}:\d{2}$/.test(text) ? text : `${text.replace(" ", "T")}Z`;
|
|
205
|
+
const d = new Date(normalized);
|
|
206
|
+
if (Number.isNaN(d.getTime())) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
return format === "date" ? toDateOnly(d) : toInstant(d);
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
function isCoercedDate(value) {
|
|
214
|
+
return DATE_ONLY.test(value) || RFC_3339.test(value);
|
|
215
|
+
}
|
|
216
|
+
function coerceEntryDates(entries, schema, contentTypeName) {
|
|
217
|
+
const warnings = [];
|
|
218
|
+
const properties = schema?.properties;
|
|
219
|
+
if (!properties) {
|
|
220
|
+
return warnings;
|
|
221
|
+
}
|
|
222
|
+
const dateFields = Object.entries(properties).map(([field, def]) => [field, def.format]).filter((pair) => isDateFormat(pair[1]));
|
|
223
|
+
if (dateFields.length === 0) {
|
|
224
|
+
return warnings;
|
|
225
|
+
}
|
|
226
|
+
for (const entry of entries) {
|
|
227
|
+
for (const [field, format] of dateFields) {
|
|
228
|
+
const raw = entry.data[field];
|
|
229
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
const coerced = parseDateValue(raw, format);
|
|
233
|
+
if (coerced === null) {
|
|
234
|
+
warnings.push({
|
|
235
|
+
entryId: entry.id,
|
|
236
|
+
field,
|
|
237
|
+
message: `Content dates: "${contentTypeName}/${entry.id}" field "${field}" is ` + `${JSON.stringify(raw)}, which is not an unambiguous ${format}. Write it as ` + `${format === "date" ? "YYYY-MM-DD" : "YYYY-MM-DDTHH:MM:SSZ"} — a form like ` + `"03/04/2025" means two different days depending on who reads it, so it is left as ` + `authored rather than guessed.`,
|
|
238
|
+
value: raw
|
|
239
|
+
});
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (coerced !== raw) {
|
|
243
|
+
entry._meta ??= {};
|
|
244
|
+
entry._meta.rawDates ??= {};
|
|
245
|
+
entry._meta.rawDates[field] = raw;
|
|
246
|
+
}
|
|
247
|
+
entry.data[field] = coerced;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return warnings;
|
|
251
|
+
}
|
|
252
|
+
function parseComparable(a, b) {
|
|
253
|
+
const aDate = typeof a === "string" && (DATE_ONLY.test(a) || RFC_3339.test(a));
|
|
254
|
+
const bDate = typeof b === "string" && (DATE_ONLY.test(b) || RFC_3339.test(b));
|
|
255
|
+
if (aDate && bDate) {
|
|
256
|
+
return [a, b];
|
|
257
|
+
}
|
|
258
|
+
return [Number(a), Number(b)];
|
|
259
|
+
}
|
|
260
|
+
|
|
83
261
|
// src/content-loader.ts
|
|
84
262
|
function loadJSONEntries(filePath) {
|
|
85
263
|
const raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
@@ -109,19 +287,53 @@ function discoverJSONFiles(resolvedSource) {
|
|
|
109
287
|
return [];
|
|
110
288
|
}
|
|
111
289
|
}
|
|
290
|
+
var LOCALE_PLACEHOLDER = "{locale}";
|
|
291
|
+
function localesForExpansion(projectConfig) {
|
|
292
|
+
return resolveI18n(projectConfig ?? {}).i18n?.locales ?? [];
|
|
293
|
+
}
|
|
294
|
+
function localeSource(root, source, locale) {
|
|
295
|
+
const candidates = [locale, locale.toLowerCase()];
|
|
296
|
+
for (const spelling of candidates) {
|
|
297
|
+
const path = source.split(LOCALE_PLACEHOLDER).join(spelling);
|
|
298
|
+
if (existsSync(resolve(root, path))) {
|
|
299
|
+
return path;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
const parent = resolve(root, source.slice(0, source.indexOf(LOCALE_PLACEHOLDER)));
|
|
303
|
+
if (existsSync(parent) && statSync(parent).isDirectory()) {
|
|
304
|
+
const match = readdirSync(parent).find((name) => name.toLowerCase() === locale.toLowerCase());
|
|
305
|
+
if (match !== undefined) {
|
|
306
|
+
return source.split(LOCALE_PLACEHOLDER).join(match);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return source.split(LOCALE_PLACEHOLDER).join(locale);
|
|
310
|
+
}
|
|
112
311
|
var SECTION_KEY = "content";
|
|
113
312
|
var SAFE_TYPE_NAME = /^[\w.~-]+$/;
|
|
114
313
|
var ASSET_KEYS = ["src", "poster"];
|
|
115
314
|
function isNonRelativeRef(value) {
|
|
116
315
|
return value === "" || value.startsWith("/") || value.startsWith("#") || value.includes("${") || /^[a-z][\w+.-]*:/i.test(value);
|
|
117
316
|
}
|
|
118
|
-
function contentAssetMounts(section, root) {
|
|
317
|
+
function contentAssetMounts(section, root, projectConfig) {
|
|
119
318
|
const mounts = [];
|
|
319
|
+
const locales = localesForExpansion(projectConfig);
|
|
120
320
|
for (const [name, contentTypeDef] of Object.entries(section ?? {})) {
|
|
121
321
|
const source = contentTypeDef?.source;
|
|
122
322
|
if (!source || source.startsWith("http://") || source.startsWith("https://")) {
|
|
123
323
|
continue;
|
|
124
324
|
}
|
|
325
|
+
if (source.includes(LOCALE_PLACEHOLDER)) {
|
|
326
|
+
for (const locale of locales) {
|
|
327
|
+
if (!SAFE_TYPE_NAME.test(locale)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
const dir = resolve(root, localeSource(root, source, locale)).split("\\").join("/");
|
|
331
|
+
if (SAFE_TYPE_NAME.test(name) && existsSync(dir) && statSync(dir).isDirectory()) {
|
|
332
|
+
mounts.push({ dir, urlPrefix: `/${SECTION_KEY}/${name}/${locale}` });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
125
337
|
if (!SAFE_TYPE_NAME.test(name)) {
|
|
126
338
|
console.warn(`Content type "${name}": name is not URL-safe — its source directory is not published ` + `at /${SECTION_KEY}/${name}, so content-relative asset references stay unresolved.`);
|
|
127
339
|
continue;
|
|
@@ -203,12 +415,35 @@ function loadContentConfig(projectRoot, projectConfig) {
|
|
|
203
415
|
const config = { content: projectConfig?.content ?? {} };
|
|
204
416
|
return { config, contentDir };
|
|
205
417
|
}
|
|
206
|
-
async function loadContentSection(section, root, formats) {
|
|
418
|
+
async function loadContentSection(section, root, formats, projectConfig) {
|
|
207
419
|
const contentTypes = new Map;
|
|
208
|
-
const mounts = new Map(contentAssetMounts(section, root).map((mount) => [
|
|
420
|
+
const mounts = new Map(contentAssetMounts(section, root, projectConfig).map((mount) => [
|
|
421
|
+
mount.urlPrefix.slice(`/${SECTION_KEY}/`.length),
|
|
422
|
+
mount
|
|
423
|
+
]));
|
|
424
|
+
const locales = localesForExpansion(projectConfig);
|
|
209
425
|
for (const [name, contentTypeDef] of Object.entries(section)) {
|
|
210
|
-
const
|
|
211
|
-
|
|
426
|
+
const source = contentTypeDef?.source;
|
|
427
|
+
if (typeof source !== "string" || !source.includes(LOCALE_PLACEHOLDER)) {
|
|
428
|
+
const entries = await loadContentType(name, contentTypeDef, root, formats, mounts.get(name));
|
|
429
|
+
contentTypes.set(name, entries);
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
if (locales.length === 0) {
|
|
433
|
+
console.warn(`Content type "${name}": source "${source}" uses ${LOCALE_PLACEHOLDER}, but the project ` + `declares no i18n locales — nothing to expand it over, so no entries were loaded.`);
|
|
434
|
+
contentTypes.set(name, []);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const all = [];
|
|
438
|
+
for (const locale of locales) {
|
|
439
|
+
const localized = { ...contentTypeDef, source: localeSource(root, source, locale) };
|
|
440
|
+
const entries = await loadContentType(name, localized, root, formats, mounts.get(`${name}/${locale}`));
|
|
441
|
+
for (const entry of entries) {
|
|
442
|
+
entry._meta = { ...entry._meta, locale };
|
|
443
|
+
}
|
|
444
|
+
all.push(...entries);
|
|
445
|
+
}
|
|
446
|
+
contentTypes.set(name, all);
|
|
212
447
|
}
|
|
213
448
|
return contentTypes;
|
|
214
449
|
}
|
|
@@ -295,6 +530,9 @@ async function loadContentType(name, contentTypeDef, projectRoot, registry, moun
|
|
|
295
530
|
entries.push(...fileEntries);
|
|
296
531
|
}
|
|
297
532
|
if (schema) {
|
|
533
|
+
for (const warning of coerceEntryDates(entries, schema, name)) {
|
|
534
|
+
console.warn(warning.message);
|
|
535
|
+
}
|
|
298
536
|
validateEntries(entries, schema, name);
|
|
299
537
|
}
|
|
300
538
|
return entries;
|
|
@@ -319,7 +557,9 @@ function validateEntries(entries, schema, contentTypeName) {
|
|
|
319
557
|
if (value == null) {
|
|
320
558
|
continue;
|
|
321
559
|
}
|
|
322
|
-
if (def.
|
|
560
|
+
if (isDateFormat(def.format) && typeof value === "string" && !isCoercedDate(value)) {
|
|
561
|
+
console.warn(`Content validation: "${contentTypeName}/${entry.id}" field "${field}" is declared ` + `${def.format} but "${value}" was not coerced — it will sort and filter as plain text.`);
|
|
562
|
+
} else if (def.type === "string" && typeof value !== "string") {
|
|
323
563
|
console.warn(`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected string, got ${typeof value}`);
|
|
324
564
|
} else if (def.type === "number" && typeof value !== "number") {
|
|
325
565
|
console.warn(`Content validation: "${contentTypeName}/${entry.id}" field "${field}" expected number, got ${typeof value}`);
|
|
@@ -389,12 +629,12 @@ function resolveContentTypeRefs(contentTypes, section) {
|
|
|
389
629
|
class Content {
|
|
390
630
|
static async projectData(sectionValue, ctx) {
|
|
391
631
|
const section = sectionValue ?? {};
|
|
392
|
-
const data = await loadContentSection(section, ctx.root, ctx.registry.formats);
|
|
632
|
+
const data = await loadContentSection(section, ctx.root, ctx.registry.formats, ctx.projectConfig);
|
|
393
633
|
resolveContentTypeRefs(data, section);
|
|
394
634
|
return data;
|
|
395
635
|
}
|
|
396
636
|
static assets(sectionValue, ctx) {
|
|
397
|
-
return contentAssetMounts(sectionValue ?? {}, ctx.root);
|
|
637
|
+
return contentAssetMounts(sectionValue ?? {}, ctx.root, ctx.projectConfig);
|
|
398
638
|
}
|
|
399
639
|
static async resolvePaths(pathsDef, ctx) {
|
|
400
640
|
const entries = ctx.data.get(pathsDef.contentType);
|
|
@@ -404,17 +644,33 @@ class Content {
|
|
|
404
644
|
}
|
|
405
645
|
const param = pathsDef.param ?? "slug";
|
|
406
646
|
const field = pathsDef.field ?? "id";
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
647
|
+
const localized = entries.some((entry) => entry._meta?.locale !== undefined);
|
|
648
|
+
const wanted = ctx.locale;
|
|
649
|
+
const scoped = localized && typeof wanted === "string" ? entries.filter((entry) => entry._meta?.locale === wanted) : entries;
|
|
650
|
+
const paths = [];
|
|
651
|
+
for (const entry of scoped) {
|
|
652
|
+
const value = field === "id" ? entry.id : entry.data[field] ?? entry.id;
|
|
653
|
+
if (!value) {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
const pathEntry = { [param]: value };
|
|
657
|
+
if (entry._meta !== undefined) {
|
|
658
|
+
pathEntry._meta = entry._meta;
|
|
659
|
+
}
|
|
660
|
+
paths.push(pathEntry);
|
|
661
|
+
}
|
|
662
|
+
return paths;
|
|
410
663
|
}
|
|
411
664
|
}
|
|
412
665
|
export {
|
|
413
666
|
rewriteEntryAssets,
|
|
414
667
|
resolveContentTypeRefs,
|
|
668
|
+
localesForExpansion,
|
|
669
|
+
localeSource,
|
|
415
670
|
loadContentSection,
|
|
416
671
|
loadContentConfig,
|
|
417
672
|
getContentTypeElements,
|
|
418
673
|
contentAssetMounts,
|
|
674
|
+
LOCALE_PLACEHOLDER,
|
|
419
675
|
Content
|
|
420
676
|
};
|
package/dist/content.js
CHANGED
|
@@ -46,6 +46,98 @@ var __export = (target, all) => {
|
|
|
46
46
|
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
47
47
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
48
48
|
|
|
49
|
+
// src/dates.ts
|
|
50
|
+
var DATE_FORMATS = ["date", "date-time"];
|
|
51
|
+
function isDateFormat(format) {
|
|
52
|
+
return typeof format === "string" && DATE_FORMATS.includes(format);
|
|
53
|
+
}
|
|
54
|
+
var DATE_ONLY = /^\d{4}-\d{2}-\d{2}$/;
|
|
55
|
+
var RFC_3339 = /^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}(:\d{2}(\.\d+)?)?([Zz]|[+-]\d{2}:\d{2})?$/;
|
|
56
|
+
function pad(n, width = 2) {
|
|
57
|
+
return String(n).padStart(width, "0");
|
|
58
|
+
}
|
|
59
|
+
function toDateOnly(d) {
|
|
60
|
+
return `${pad(d.getUTCFullYear(), 4)}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}`;
|
|
61
|
+
}
|
|
62
|
+
function toInstant(d) {
|
|
63
|
+
return `${toDateOnly(d)}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
64
|
+
}
|
|
65
|
+
function parseDateValue(value, format) {
|
|
66
|
+
if (value instanceof Date) {
|
|
67
|
+
return Number.isNaN(value.getTime()) ? null : format === "date" ? toDateOnly(value) : toInstant(value);
|
|
68
|
+
}
|
|
69
|
+
if (typeof value !== "string") {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const text = value.trim();
|
|
73
|
+
if (text === "") {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
if (DATE_ONLY.test(text)) {
|
|
77
|
+
const d = new Date(`${text}T00:00:00Z`);
|
|
78
|
+
if (Number.isNaN(d.getTime())) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return format === "date" ? text : toInstant(d);
|
|
82
|
+
}
|
|
83
|
+
if (RFC_3339.test(text)) {
|
|
84
|
+
const normalized = /[Zz]|[+-]\d{2}:\d{2}$/.test(text) ? text : `${text.replace(" ", "T")}Z`;
|
|
85
|
+
const d = new Date(normalized);
|
|
86
|
+
if (Number.isNaN(d.getTime())) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return format === "date" ? toDateOnly(d) : toInstant(d);
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
function isCoercedDate(value) {
|
|
94
|
+
return DATE_ONLY.test(value) || RFC_3339.test(value);
|
|
95
|
+
}
|
|
96
|
+
function coerceEntryDates(entries, schema, contentTypeName) {
|
|
97
|
+
const warnings = [];
|
|
98
|
+
const properties = schema?.properties;
|
|
99
|
+
if (!properties) {
|
|
100
|
+
return warnings;
|
|
101
|
+
}
|
|
102
|
+
const dateFields = Object.entries(properties).map(([field, def]) => [field, def.format]).filter((pair) => isDateFormat(pair[1]));
|
|
103
|
+
if (dateFields.length === 0) {
|
|
104
|
+
return warnings;
|
|
105
|
+
}
|
|
106
|
+
for (const entry of entries) {
|
|
107
|
+
for (const [field, format] of dateFields) {
|
|
108
|
+
const raw = entry.data[field];
|
|
109
|
+
if (raw === undefined || raw === null || raw === "") {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const coerced = parseDateValue(raw, format);
|
|
113
|
+
if (coerced === null) {
|
|
114
|
+
warnings.push({
|
|
115
|
+
entryId: entry.id,
|
|
116
|
+
field,
|
|
117
|
+
message: `Content dates: "${contentTypeName}/${entry.id}" field "${field}" is ` + `${JSON.stringify(raw)}, which is not an unambiguous ${format}. Write it as ` + `${format === "date" ? "YYYY-MM-DD" : "YYYY-MM-DDTHH:MM:SSZ"} — a form like ` + `"03/04/2025" means two different days depending on who reads it, so it is left as ` + `authored rather than guessed.`,
|
|
118
|
+
value: raw
|
|
119
|
+
});
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (coerced !== raw) {
|
|
123
|
+
entry._meta ??= {};
|
|
124
|
+
entry._meta.rawDates ??= {};
|
|
125
|
+
entry._meta.rawDates[field] = raw;
|
|
126
|
+
}
|
|
127
|
+
entry.data[field] = coerced;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return warnings;
|
|
131
|
+
}
|
|
132
|
+
function parseComparable(a, b) {
|
|
133
|
+
const aDate = typeof a === "string" && (DATE_ONLY.test(a) || RFC_3339.test(a));
|
|
134
|
+
const bDate = typeof b === "string" && (DATE_ONLY.test(b) || RFC_3339.test(b));
|
|
135
|
+
if (aDate && bDate) {
|
|
136
|
+
return [a, b];
|
|
137
|
+
}
|
|
138
|
+
return [Number(a), Number(b)];
|
|
139
|
+
}
|
|
140
|
+
|
|
49
141
|
// src/content.ts
|
|
50
142
|
function evaluateFilterRule(rule, entry) {
|
|
51
143
|
const actual = rule.field === "id" ? entry.id : entry.data[rule.field];
|
|
@@ -75,16 +167,20 @@ function evaluateFilterRule(rule, entry) {
|
|
|
75
167
|
return typeof actual === "string" && !actual.includes(String(rule.value ?? ""));
|
|
76
168
|
}
|
|
77
169
|
case ">": {
|
|
78
|
-
|
|
170
|
+
const [a, b] = parseComparable(actual, rule.value);
|
|
171
|
+
return a > b;
|
|
79
172
|
}
|
|
80
173
|
case "<": {
|
|
81
|
-
|
|
174
|
+
const [a, b] = parseComparable(actual, rule.value);
|
|
175
|
+
return a < b;
|
|
82
176
|
}
|
|
83
177
|
case ">=": {
|
|
84
|
-
|
|
178
|
+
const [a, b] = parseComparable(actual, rule.value);
|
|
179
|
+
return a >= b;
|
|
85
180
|
}
|
|
86
181
|
case "<=": {
|
|
87
|
-
|
|
182
|
+
const [a, b] = parseComparable(actual, rule.value);
|
|
183
|
+
return a <= b;
|
|
88
184
|
}
|
|
89
185
|
default: {
|
|
90
186
|
return true;
|
|
@@ -175,10 +271,13 @@ class ContentEntry {
|
|
|
175
271
|
if (!resolvedId) {
|
|
176
272
|
return null;
|
|
177
273
|
}
|
|
274
|
+
const wanted = _document?.route?.locale;
|
|
275
|
+
const localized = entries.some((e) => e._meta?.locale !== undefined);
|
|
276
|
+
const scoped = localized && typeof wanted === "string" ? entries.filter((e) => e._meta?.locale === wanted) : entries;
|
|
178
277
|
if (field && field !== "id") {
|
|
179
|
-
return
|
|
278
|
+
return scoped.find((e) => e.data[field] === resolvedId) ?? null;
|
|
180
279
|
}
|
|
181
|
-
return findEntry(
|
|
280
|
+
return findEntry(scoped, resolvedId);
|
|
182
281
|
}
|
|
183
282
|
}
|
|
184
283
|
export {
|