@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.
- package/README.md +63 -34
- package/bin/anc.mjs +120 -11
- package/package.json +8 -1
- package/scripts/copy-assets.js +141 -0
- package/scripts/init-repository.js +18 -0
- package/scripts/load-config.js +258 -5
- package/scripts/markdown-to-artifact.js +310 -6
- package/scripts/publish-media.js +468 -0
- package/scripts/resolve-links.js +318 -42
- package/scripts/scan-residue.js +17 -6
- package/scripts/verify-output-inventory.js +53 -0
- package/src/lib/content.js +22 -0
- package/src/lib/font-coverage.js +845 -0
- package/src/lib/markdown.js +257 -11
- package/src/lib/media-codec.js +355 -0
- package/src/lib/media-delivery.js +76 -0
- package/src/lib/media.js +1426 -0
- package/src/lib/mermaid-render.js +36 -0
- package/src/lib/schema.js +83 -8
- package/src/lib/svg-css.js +685 -0
- package/src/lib/svg-policy.js +1699 -0
- package/src/lib/svg-refusal.js +1 -0
- package/src/lib/svg-render.js +343 -0
- package/src/pages/notes/[slug].astro +10 -1
- package/src/pages/privacy.astro +9 -5
- package/src/styles/global.css +29 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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');
|