@wxxb789/anc 0.0.2 → 0.0.3

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/src/lib/schema.js CHANGED
@@ -7,6 +7,7 @@
7
7
  * loud build failure rather than a silent privacy leak.
8
8
  */
9
9
  import { compareSlugs, isNoteSlug, SLUG_MAX_BYTES } from "./route-path.js";
10
+ import { isPositiveDimension, parseAssetUrl } from "./media.js";
10
11
  export const SCHEMA_VERSION = 1;
11
12
  /** Top-level route segments the site owns; a note slug may not collide with them. */
12
13
  export const RESERVED_SLUGS = new Set([
@@ -84,6 +85,23 @@ const KNOWN_FIELDS = new Set([
84
85
  ...REQUIRED_STRINGS,
85
86
  ...OPTIONAL_FIELDS,
86
87
  ]);
88
+ /** Top-level artifact keys, for the one unknown-field check. */
89
+ const ARTIFACT_FIELDS = new Set(['version', 'entries', 'assets']);
90
+ /** One asset declaration's keys. */
91
+ const ASSET_FIELDS = new Set(['url', 'width', 'height']);
92
+ /**
93
+ * Refuse every key outside `allowed`, under the caller's own field label.
94
+ *
95
+ * The artifact, each entry, and each asset declaration each reject unknown
96
+ * fields as a producer defect. One helper keeps the message and the rule from
97
+ * being spelled three slightly different ways.
98
+ */
99
+ function unknownFields(value, allowed, prefix, issues) {
100
+ for (const field of Object.keys(value)) {
101
+ if (!allowed.has(field))
102
+ issues.push(`${prefix}${field}: unknown field is not allowed`);
103
+ }
104
+ }
87
105
  /**
88
106
  * Size ceilings, stated per field.
89
107
  *
@@ -405,10 +423,7 @@ function checkEntry(value, index, issues) {
405
423
  const slug = value['slug'];
406
424
  const label = typeof slug === 'string' ? `entries[${index}] (slug "${slug}")` : `entries[${index}]`;
407
425
  const before = issues.length;
408
- for (const field of Object.keys(value)) {
409
- if (!KNOWN_FIELDS.has(field))
410
- issues.push(`${label}.${field}: unknown field is not allowed`);
411
- }
426
+ unknownFields(value, KNOWN_FIELDS, `${label}.`, issues);
412
427
  for (const field of REQUIRED_STRINGS) {
413
428
  const item = value[field];
414
429
  if (typeof item !== 'string')
@@ -528,6 +543,68 @@ function checkCorpus(entries, issues, edges) {
528
543
  }
529
544
  }
530
545
  }
546
+ /**
547
+ * The image assets this artifact publishes. Every entry names a canonical
548
+ * `/_assets/<sha256>-<name>` URL — one spelling per asset, so the emission step
549
+ * and the inventory cannot disagree about which file a URL names — and the list
550
+ * is sorted and unique, so two builds of the same corpus serialize the same
551
+ * bytes. A URL that is not canonical is a producer defect rather than an
552
+ * author's mistake, and it fails here rather than in a filename lookup.
553
+ *
554
+ * `width`/`height` are the **CSS layout size** of the published picture: the
555
+ * emitted header's pixel size for a raster, and the logical size for an SVG
556
+ * rasterized at 2× (a 600-pixel diagram publishes a 1,200-pixel WebP and lays out
557
+ * at 600). They are a pair or absent: one without the other describes no aspect
558
+ * ratio a browser can reserve, and the producer never writes a guess. Their only
559
+ * consumer is the renderer's layout attributes; they do not participate in
560
+ * identity.
561
+ */
562
+ function checkAssets(rawAssets, issues) {
563
+ if (rawAssets === undefined)
564
+ return;
565
+ if (!Array.isArray(rawAssets)) {
566
+ issues.push('assets: must be an array of published asset declarations');
567
+ return;
568
+ }
569
+ const seen = new Set();
570
+ let previous;
571
+ for (const [index, value] of rawAssets.entries()) {
572
+ if (!isPlainObject(value)) {
573
+ issues.push(`assets[${index}]: must be an object with a url`);
574
+ continue;
575
+ }
576
+ unknownFields(value, ASSET_FIELDS, `assets[${index}].`, issues);
577
+ const url = value['url'];
578
+ if (typeof url !== 'string') {
579
+ issues.push(`assets[${index}].url: must be a string`);
580
+ continue;
581
+ }
582
+ if (parseAssetUrl(url) === undefined) {
583
+ issues.push(`assets[${index}].url: is not a canonical published asset path (/_assets/<sha256>-<name>)`);
584
+ continue;
585
+ }
586
+ const width = value['width'];
587
+ const height = value['height'];
588
+ if ((width === undefined) !== (height === undefined)) {
589
+ issues.push(`assets[${index}]: width and height must be declared together`);
590
+ }
591
+ for (const [field, dimension] of [
592
+ ['width', width],
593
+ ['height', height],
594
+ ]) {
595
+ if (dimension !== undefined && !isPositiveDimension(dimension)) {
596
+ issues.push(`assets[${index}].${field}: must be a positive integer`);
597
+ }
598
+ }
599
+ if (seen.has(url))
600
+ issues.push(`assets[${index}]: appears twice`);
601
+ seen.add(url);
602
+ if (previous !== undefined && !(previous < url)) {
603
+ issues.push(`assets: must be sorted in ascending order, at index ${index}`);
604
+ }
605
+ previous = url;
606
+ }
607
+ }
531
608
  /**
532
609
  * Validate a parsed content artifact.
533
610
  *
@@ -538,13 +615,11 @@ export function validateArtifact(data, source = 'content artifact') {
538
615
  const issues = [];
539
616
  if (!isPlainObject(data))
540
617
  throw new ContentValidationError(source, ['artifact: must be an object']);
541
- for (const field of Object.keys(data)) {
542
- if (field !== 'version' && field !== 'entries')
543
- issues.push(`${field}: unknown field is not allowed`);
544
- }
618
+ unknownFields(data, ARTIFACT_FIELDS, '', issues);
545
619
  if (data['version'] !== SCHEMA_VERSION) {
546
620
  issues.push(`version: must be ${SCHEMA_VERSION}, got ${JSON.stringify(data['version'])}`);
547
621
  }
622
+ checkAssets(data['assets'], issues);
548
623
  const rawEntries = data['entries'];
549
624
  if (!Array.isArray(rawEntries))
550
625
  issues.push('entries: must be an array');