@wxxb789/anc 0.0.1 → 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.
@@ -405,6 +405,41 @@ function convertInlineStyles(root) {
405
405
  * handlers across the corpus — which is exactly why a *failure* is the right
406
406
  * response to finding one: it would mean the renderer changed under us.
407
407
  */
408
+ /**
409
+ * Remove Mermaid's gantt "today" marker: the one part of a render that reads the
410
+ * wall clock.
411
+ *
412
+ * The marker is positioned from `new Date()`, so two renders of the same note
413
+ * seconds apart differ by a pixel — measured on a `main` build, where the
414
+ * determinism gate went red on `x1="35387"` against `x1="35388"` — and the same
415
+ * note would hash differently on every rebuild, which
416
+ * `src/lib/markdown.ts` says cannot happen. A build also freezes the build date
417
+ * into content that outlives it, which is nobody's today.
418
+ *
419
+ * There is no configuration for this. Mermaid's `drawToday` reads
420
+ * `db.getTodayMarker()`, which only the in-source `todayMarker` directive sets;
421
+ * `gantt.todayMarker` in `initialize()` is never consulted, verified by setting
422
+ * it and watching the line survive. Removing the group after the render is
423
+ * therefore what makes the output a function of its source alone, and it holds
424
+ * whether or not a diagram spelled the directive itself.
425
+ *
426
+ * The client renderer keeps the marker: it draws at read time, where "today" is
427
+ * the reader's today rather than the build's.
428
+ *
429
+ * Scoped to gantt charts, and that scope is load-bearing rather than tidy: a
430
+ * flowchart node can carry an author's `:::today` class, which Mermaid renders
431
+ * as `<g class="node default today">`, so an unscoped `g.today` selector
432
+ * deletes that node's shape and label and leaves its edges dangling. Gantt has
433
+ * no author-supplied classes, and `aria-roledescription` is the renderer's own
434
+ * statement of the diagram type rather than a guess at its markup.
435
+ */
436
+ function dropWallClockOutput(root) {
437
+ if (root.getAttribute('aria-roledescription') !== 'gantt')
438
+ return;
439
+ for (const marker of [...root.querySelectorAll('g.today')]) {
440
+ marker.remove();
441
+ }
442
+ }
408
443
  function sanitizeDiagram(root, source) {
409
444
  if (root.querySelector('script') !== null) {
410
445
  throw new DiagramError(source, 'the renderer emitted a <script> element');
@@ -702,6 +737,7 @@ async function renderOne(source, diagramId, accessibleName) {
702
737
  const light = parse(rendered.light);
703
738
  const dark = parse(rendered.dark);
704
739
  for (const root of [light, dark]) {
740
+ dropWallClockOutput(root);
705
741
  const style = root.querySelector('style');
706
742
  flattenStylesheet(root, style?.textContent ?? '');
707
743
  convertInlineStyles(root);
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');