pandorga 1.7.3 → 1.7.5
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +49 -0
- data/_includes/content-runtime/50-detail.html +64 -1
- data/_includes/content-runtime/60-fragments.html +1 -1
- data/_includes/content-runtime/90-bootstrap.html +1 -0
- data/_layouts/articles.html +1 -0
- data/_layouts/blog.html +1 -0
- data/_layouts/cv.html +1 -0
- data/_layouts/projects.html +1 -0
- data/assets/css/article.css +66 -19
- data/assets/css/ledger/detail.css +21 -28
- data/assets/css/ledger/footer.css +5 -2
- data/assets/css/ledger/home.css +1 -2
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/version.rb +1 -1
- data/scripts/content/band_art.rb +1 -1
- data/scripts/content/export-content-json.rb +1 -1
- data/scripts/test/test-css-structure.rb +110 -0
- data/scripts/test/test-home-band-art.rb +1 -1
- data/scripts/test/test-new.rb +40 -0
- data/scripts/test/test-r2-config-consistency.rb +1 -1
- data/scripts/test/test-shell-a11y.rb +16 -0
- data/scripts/test/test-theme-contrast.rb +21 -1
- metadata +1 -3
- data/scripts/content/backfill-content-keys.rb +0 -99
- data/scripts/test/test-cloudflare-watch-paths-doc.rb +0 -31
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 50cefbd5818ff3cacd667823f637d8dd3aca3a0dd06fb78a01f44d68954b8a28
|
|
4
|
+
data.tar.gz: a602af73319ba4acc679ffdc30d75b555663dfb0ce54febf32823ce8e213cc03
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97f05ec16f99648f22c74146bb9f8ce497e3afa759727b8cc5d09f50dcdceee955e3c993e1ada003cd1492a5cd0c9975dbfa9d05bd56668aaf97eec945236425
|
|
7
|
+
data.tar.gz: a6a9494a944b8807f594e1f3c1dd6bc8337aade5ba8c53a7b29e3a905575d39f6b5fd310ad99ac00d7d017d44b182e1d78db85e52c218057d430848455d0135f
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.5] — 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- A display equation scrolls inside the article column. On a 390px
|
|
8
|
+
viewport a MathJax display box about 455px wide was stretching the
|
|
9
|
+
document to a scrollWidth of 667, so the page moved sideways and the
|
|
10
|
+
sticky header scrolled with it. `mjx-container[display="true"]` and
|
|
11
|
+
`mjx-container[jax][display="true"]` are `max-width: 100%` with
|
|
12
|
+
`overflow-x: auto` and `overflow-y: hidden`. Tables, figures, and
|
|
13
|
+
article SVG are bounded to the column. Mermaid still scrolls inside
|
|
14
|
+
`.mermaid`. `html` and `body` do not set overflow, so the sticky
|
|
15
|
+
header still sticks.
|
|
16
|
+
- A detail URL served by the listing shell had two `h1` elements: the
|
|
17
|
+
listing title (Articles, Rascunhos, Projects, Curriculum) and the
|
|
18
|
+
entry title. While the entry is showing, the listing heading is not
|
|
19
|
+
an `h1`. The document title is the entry title, and `html` `lang`
|
|
20
|
+
follows the entry (`en`, `pt-BR`, `fr`, `es`). Articles, posts,
|
|
21
|
+
projects, and products.
|
|
22
|
+
|
|
23
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
24
|
+
ledger fallbacks are unchanged.
|
|
25
|
+
|
|
26
|
+
## [1.7.4] — 2026-10-12
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
|
|
30
|
+
- The footer imprint separator uses `outline` on the panel step.
|
|
31
|
+
`outline-variant` on that panel is 1.80:1 in light (`#b8b8b8` on
|
|
32
|
+
`#f4f4f4`) and 1.93:1 in dark (`#45454c` on `#141418`). `outline`
|
|
33
|
+
on the same panels is 6.08:1 and 5.95:1.
|
|
34
|
+
|
|
35
|
+
### Changed
|
|
36
|
+
|
|
37
|
+
- Unused selectors are gone. `assets/css/ledger/home.css` drops
|
|
38
|
+
`.home-hero-pandorga-svg` (25 bytes). The hero SVG uses
|
|
39
|
+
`home-hero-art-svg`. `assets/css/ledger/detail.css` drops 916 bytes:
|
|
40
|
+
the author byline suppressor and the `.table-csv` companions. No
|
|
41
|
+
template or runtime emits either class. 941 bytes in all.
|
|
42
|
+
- The one-shot key backfill is gone (`scripts/content/backfill-content-keys.rb`,
|
|
43
|
+
3,432 bytes). Nothing called it.
|
|
44
|
+
- The watch-path test that read `docs/ops/cloudflare.md` is gone
|
|
45
|
+
(1,230 bytes). That path is not in the tree.
|
|
46
|
+
- Deploy notes pin `~> 1.3` and tag `v1.7.4`. The Pages watch paths
|
|
47
|
+
include `*` and exclude `content/*` and `docs/*`.
|
|
48
|
+
|
|
49
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
50
|
+
ledger fallbacks are unchanged.
|
|
51
|
+
|
|
3
52
|
## [1.7.3] — 2026-10-12
|
|
4
53
|
|
|
5
54
|
### Fixed
|
|
@@ -526,11 +526,71 @@
|
|
|
526
526
|
el.setAttribute('href', url);
|
|
527
527
|
}
|
|
528
528
|
|
|
529
|
+
/* The listing shell is one document for the index and every detail URL.
|
|
530
|
+
The static h1 is the index title ("Articles"). The runtime then paints
|
|
531
|
+
the entry title as a second h1. Demote the index heading while a detail
|
|
532
|
+
is showing so the document has one h1, and put it back for the index. */
|
|
533
|
+
var listingDocumentSnapshot = null;
|
|
534
|
+
|
|
535
|
+
function rememberListingDocument() {
|
|
536
|
+
if (listingDocumentSnapshot) return;
|
|
537
|
+
listingDocumentSnapshot = {
|
|
538
|
+
title: document.title,
|
|
539
|
+
lang: document.documentElement.getAttribute('lang') || ''
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function concealListingHeader() {
|
|
544
|
+
var header = document.querySelector('[data-content-page-header]');
|
|
545
|
+
if (!header) return;
|
|
546
|
+
header.setAttribute('hidden', '');
|
|
547
|
+
var heading = header.querySelector('h1');
|
|
548
|
+
if (!heading) return;
|
|
549
|
+
var replacement = document.createElement('p');
|
|
550
|
+
replacement.className = heading.className;
|
|
551
|
+
replacement.setAttribute('data-listing-heading', 'demoted');
|
|
552
|
+
while (heading.firstChild) replacement.appendChild(heading.firstChild);
|
|
553
|
+
heading.replaceWith(replacement);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function restoreListingDocument() {
|
|
557
|
+
var header = document.querySelector('[data-content-page-header]');
|
|
558
|
+
if (header) {
|
|
559
|
+
header.removeAttribute('hidden');
|
|
560
|
+
var demoted = header.querySelector('[data-listing-heading="demoted"]');
|
|
561
|
+
if (demoted) {
|
|
562
|
+
var heading = document.createElement('h1');
|
|
563
|
+
heading.className = demoted.className;
|
|
564
|
+
while (demoted.firstChild) heading.appendChild(demoted.firstChild);
|
|
565
|
+
demoted.replaceWith(heading);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!listingDocumentSnapshot) return;
|
|
569
|
+
document.title = listingDocumentSnapshot.title;
|
|
570
|
+
document.documentElement.lang = listingDocumentSnapshot.lang;
|
|
571
|
+
listingDocumentSnapshot = null;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* Site codes (enus, ptbr, en_us, pt_br) are not BCP-47. The html lang
|
|
575
|
+
attribute has to be. */
|
|
576
|
+
function htmlLangFromContent(language) {
|
|
577
|
+
var compact = String(language || '').trim().toLowerCase().replace(/[_-]/g, '');
|
|
578
|
+
if (!compact) return '';
|
|
579
|
+
var primary = compact.slice(0, 2);
|
|
580
|
+
if (primary === 'en') return 'en';
|
|
581
|
+
if (primary === 'pt') return 'pt-BR';
|
|
582
|
+
if (primary === 'fr') return 'fr';
|
|
583
|
+
if (primary === 'es') return 'es';
|
|
584
|
+
return '';
|
|
585
|
+
}
|
|
586
|
+
|
|
529
587
|
function applyDocumentMeta(meta) {
|
|
530
588
|
meta = meta || {};
|
|
531
589
|
var siteSuffix = (window.ContentRuntimeConfig && window.ContentRuntimeConfig.siteSuffix) || '';
|
|
532
590
|
var title = meta.title ? String(meta.title) + siteSuffix : document.title;
|
|
533
591
|
document.title = title;
|
|
592
|
+
var htmlLang = htmlLangFromContent(meta.lang);
|
|
593
|
+
if (htmlLang) document.documentElement.lang = htmlLang;
|
|
534
594
|
if (meta.description) setMetaTag('description', meta.description);
|
|
535
595
|
setMetaTag('og:title', title, 'property');
|
|
536
596
|
if (meta.description) setMetaTag('og:description', meta.description, 'property');
|
|
@@ -736,12 +796,15 @@
|
|
|
736
796
|
|
|
737
797
|
async function renderRuntimeDetailSection(options) {
|
|
738
798
|
var fm = options.frontMatter || {};
|
|
799
|
+
rememberListingDocument();
|
|
800
|
+
concealListingHeader();
|
|
739
801
|
if (options.documentMeta !== false) {
|
|
740
802
|
applyDocumentMeta({
|
|
741
803
|
title: fm.title,
|
|
742
804
|
description: fm.description,
|
|
743
805
|
url: window.location.pathname,
|
|
744
|
-
image: fm.og_image || fm.thumbnail
|
|
806
|
+
image: fm.og_image || fm.thumbnail,
|
|
807
|
+
lang: fm.language || ((fm.layout === 'post' || options.detailAttribute === 'data-dynamic-post-detail') ? 'ptbr' : '')
|
|
745
808
|
});
|
|
746
809
|
}
|
|
747
810
|
var section = document.createElement('section');
|
|
@@ -385,7 +385,7 @@
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
/* The home page's reference band opens each panel with a plate drawn from
|
|
388
|
-
* that section's own content by the export (docs/
|
|
388
|
+
* that section's own content by the export (docs/deploy/cloudflare.md).
|
|
389
389
|
* The markup arrives as an SVG document and is injected inline, never as an
|
|
390
390
|
* <img>: every stroke and fill in it is a custom property, and an external
|
|
391
391
|
* document cannot see the palette the page is currently wearing.
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
insertRuntimeBackLink: insertRuntimeBackLink,
|
|
40
40
|
removeRuntimeBackLink: removeRuntimeBackLink,
|
|
41
41
|
renderRuntimeDetailSection: renderRuntimeDetailSection,
|
|
42
|
+
restoreListingDocument: restoreListingDocument,
|
|
42
43
|
reportDetailLoadError: reportDetailLoadError,
|
|
43
44
|
buildDetailActions: buildDetailActions,
|
|
44
45
|
buildTextSidebar: buildTextSidebar,
|
data/_layouts/articles.html
CHANGED
data/_layouts/blog.html
CHANGED
data/_layouts/cv.html
CHANGED
data/_layouts/projects.html
CHANGED
data/assets/css/article.css
CHANGED
|
@@ -112,44 +112,58 @@
|
|
|
112
112
|
which outranks `.article-content mjx-container[display="true"]` on
|
|
113
113
|
specificity. The 0.5rem this file asked for was never what the page drew.
|
|
114
114
|
|
|
115
|
-
|
|
116
|
-
inside it owns none of them. */
|
|
115
|
+
The wrapper owns the paragraph gap. The container owns the scroll. */
|
|
117
116
|
.article-content .math-display {
|
|
118
117
|
display: block;
|
|
118
|
+
max-width: 100%;
|
|
119
119
|
/* The bottom gap is the paragraph gap `.article-content p` sets in
|
|
120
120
|
ledger/detail.css; the top gap is left to the previous block, exactly as
|
|
121
121
|
it is between two paragraphs. The negative top margin hands back the
|
|
122
|
-
padding below, which a margin collapse cannot absorb
|
|
123
|
-
number, so change them together or the rhythm moves. */
|
|
122
|
+
container padding below, which a margin collapse cannot absorb. The two
|
|
123
|
+
are one number, so change them together or the rhythm moves. */
|
|
124
124
|
margin: -1.25rem 0 calc(1.15em - 1.25rem);
|
|
125
|
-
|
|
126
|
-
overflow-y to auto alongside it, so a bracket, radical or matrix rule that
|
|
127
|
-
MathJax draws past its own line box gets clipped, or grows a vertical
|
|
128
|
-
scrollbar it has no use for. The padding is the allowance for that
|
|
129
|
-
overshoot: 1.25rem clears the worst case on the object-centric article
|
|
130
|
-
(a 3-row bmatrix, measured at 12px past a 0.5rem allowance). It costs
|
|
131
|
-
nothing visually, because the margin above gives it straight back. */
|
|
132
|
-
padding: 1.25rem 0;
|
|
125
|
+
padding: 0;
|
|
133
126
|
overflow-x: auto;
|
|
127
|
+
overflow-y: hidden;
|
|
134
128
|
text-align: center;
|
|
135
129
|
}
|
|
136
130
|
|
|
137
|
-
/* `[jax]`
|
|
138
|
-
|
|
139
|
-
|
|
131
|
+
/* `[jax]` lifts this rule past MathJax's injected margin
|
|
132
|
+
(`mjx-container[jax="CHTML"][display="true"]`). The container is the clip
|
|
133
|
+
edge: on a 390px phone a display `mjx-math` about 455px wide was stretching
|
|
134
|
+
the document to a scrollWidth of 667, because this rule was
|
|
135
|
+
`overflow: visible`. The page then scrolled sideways and the sticky header
|
|
136
|
+
moved with it. `max-width: 100%` plus `overflow-x: auto` and
|
|
137
|
+
`overflow-y: hidden` scroll the equation inside the column, the way `pre`
|
|
138
|
+
already scrolls code. `overflow-y: hidden` is stated so a radical does not
|
|
139
|
+
grow a vertical scrollbar, and so a specified `visible` cannot let the
|
|
140
|
+
equation's min-content widen the column. The 1.25rem padding is the
|
|
141
|
+
allowance for a bracket, radical, or matrix rule MathJax draws past its
|
|
142
|
+
line box (a 3-row bmatrix, measured at 12px past a 0.5rem allowance).
|
|
143
|
+
Do not move this overflow onto `html` or `body`. That scrollport breaks
|
|
144
|
+
`position: sticky` on the header. */
|
|
140
145
|
.article-content .math-display mjx-container[jax][display="true"] {
|
|
146
|
+
display: block;
|
|
141
147
|
margin: 0;
|
|
142
|
-
padding: 0;
|
|
143
|
-
|
|
148
|
+
padding: 1.25rem 0;
|
|
149
|
+
max-width: 100%;
|
|
150
|
+
overflow-x: auto;
|
|
151
|
+
overflow-y: hidden;
|
|
152
|
+
text-align: center;
|
|
144
153
|
}
|
|
145
154
|
|
|
146
155
|
/* Display math the wrapper regex cannot reach, because the `$$` sits inside a
|
|
147
156
|
list item or a table cell rather than alone in a paragraph. It still has to
|
|
148
157
|
break the line; its vertical margin stays MathJax's own 1em, which lands
|
|
149
|
-
within a pixel of the paragraph gap.
|
|
150
|
-
|
|
158
|
+
within a pixel of the paragraph gap. `[jax]` names the same box. Every
|
|
159
|
+
MathJax container carries it, and the attribute keeps this rule ahead of
|
|
160
|
+
the injected stylesheet. */
|
|
161
|
+
.article-content mjx-container[display="true"],
|
|
162
|
+
.article-content mjx-container[jax][display="true"] {
|
|
151
163
|
display: block;
|
|
164
|
+
max-width: 100%;
|
|
152
165
|
overflow-x: auto;
|
|
166
|
+
overflow-y: hidden;
|
|
153
167
|
text-align: center;
|
|
154
168
|
}
|
|
155
169
|
|
|
@@ -248,7 +262,9 @@
|
|
|
248
262
|
:is(.article-content, .article-cite-block) pre {
|
|
249
263
|
background: var(--color-surface-container);
|
|
250
264
|
padding: 1rem;
|
|
265
|
+
max-width: 100%;
|
|
251
266
|
overflow-x: auto;
|
|
267
|
+
overflow-y: hidden;
|
|
252
268
|
margin-bottom: 1rem;
|
|
253
269
|
font-family: var(--font-code);
|
|
254
270
|
}
|
|
@@ -359,11 +375,30 @@
|
|
|
359
375
|
margin: 1.5rem auto;
|
|
360
376
|
text-align: center;
|
|
361
377
|
width: 100%;
|
|
378
|
+
max-width: 100%;
|
|
362
379
|
display: block;
|
|
363
380
|
background: var(--color-surface-container-low);
|
|
364
381
|
padding: var(--space-md);
|
|
365
382
|
}
|
|
366
383
|
|
|
384
|
+
/* A wide SVG, video, or iframe must stay inside the plate. Cut-in plates
|
|
385
|
+
and YouTube shorts set a narrower max-width on a more specific rule.
|
|
386
|
+
Mermaid and display-math SVG opt back out below so those boxes scroll
|
|
387
|
+
instead of shrinking the drawing. Iframes keep their own height: `height:
|
|
388
|
+
auto` collapses a YouTube or Plotly frame that has no intrinsic ratio. */
|
|
389
|
+
.article-content figure svg,
|
|
390
|
+
.article-content svg {
|
|
391
|
+
max-width: 100%;
|
|
392
|
+
height: auto;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.article-content figure video,
|
|
396
|
+
.article-content figure iframe,
|
|
397
|
+
.article-content figure canvas,
|
|
398
|
+
.article-content iframe {
|
|
399
|
+
max-width: 100%;
|
|
400
|
+
}
|
|
401
|
+
|
|
367
402
|
.article-content figure img {
|
|
368
403
|
margin-inline: auto;
|
|
369
404
|
}
|
|
@@ -554,7 +589,9 @@ body.figure-lightbox-open {
|
|
|
554
589
|
padding: var(--space-xl) var(--space-lg);
|
|
555
590
|
background: var(--color-surface-container);
|
|
556
591
|
text-align: center;
|
|
592
|
+
max-width: 100%;
|
|
557
593
|
overflow-x: auto;
|
|
594
|
+
overflow-y: hidden;
|
|
558
595
|
-webkit-overflow-scrolling: touch;
|
|
559
596
|
}
|
|
560
597
|
|
|
@@ -567,6 +604,15 @@ body.figure-lightbox-open {
|
|
|
567
604
|
height: auto;
|
|
568
605
|
}
|
|
569
606
|
|
|
607
|
+
/* SVG output of a display equation keeps its own width and scrolls inside
|
|
608
|
+
`mjx-container`. Inline math is not included: `display: block` here would
|
|
609
|
+
pull an inline equation out of the line. */
|
|
610
|
+
.article-content mjx-container[display="true"] svg {
|
|
611
|
+
max-width: none;
|
|
612
|
+
width: auto;
|
|
613
|
+
height: auto;
|
|
614
|
+
}
|
|
615
|
+
|
|
570
616
|
/* Captions are sentences. The include centres the whole figure, and a centred
|
|
571
617
|
paragraph of provenance is harder to read than a left-set one; the rule on
|
|
572
618
|
the element itself beats the `text-center` utility the runtime emits. The
|
|
@@ -628,6 +674,7 @@ body.figure-lightbox-open {
|
|
|
628
674
|
/* ── Tables ── */
|
|
629
675
|
.article-content table {
|
|
630
676
|
width: 100%;
|
|
677
|
+
max-width: 100%;
|
|
631
678
|
border-collapse: collapse;
|
|
632
679
|
margin-bottom: 1.5rem;
|
|
633
680
|
font-size: var(--type-dek);
|
|
@@ -323,6 +323,17 @@ a.back-link .material-symbols-outlined {
|
|
|
323
323
|
|
|
324
324
|
Every detail page uses this one rule, including the projects page, which
|
|
325
325
|
only differs in holding the split back to 1024px (ledger/projects.css). */
|
|
326
|
+
/* A grid item's automatic minimum is its widest child. A display equation
|
|
327
|
+
used to be that child, so the column grew past the viewport, the document
|
|
328
|
+
scrolled sideways, and the sticky header scrolled with it. The track is
|
|
329
|
+
already allowed to shrink (`minmax(0, 1fr)` from 768px, and `grid-cols-1`
|
|
330
|
+
below that). The item has to be allowed to shrink with it. Wide children
|
|
331
|
+
scroll inside their own boxes; this does not put overflow on `html` or
|
|
332
|
+
`body`. */
|
|
333
|
+
.grid.text-detail-grid > * {
|
|
334
|
+
min-width: 0;
|
|
335
|
+
}
|
|
336
|
+
|
|
326
337
|
@media (min-width: 768px) {
|
|
327
338
|
.grid.text-detail-grid {
|
|
328
339
|
--detail-column-gap: 2rem;
|
|
@@ -485,11 +496,9 @@ aside:has(> .text-detail-sidebar) {
|
|
|
485
496
|
font-variant-numeric: tabular-nums;
|
|
486
497
|
}
|
|
487
498
|
|
|
488
|
-
/* Projects and products can carry no meta at all
|
|
489
|
-
the byline below is the block's only child. Either way an all-empty meta
|
|
499
|
+
/* Projects and products can carry no meta at all. An all-empty meta
|
|
490
500
|
block must not hang a stub of padding off the bottom of the panel. */
|
|
491
|
-
.text-detail-sidebar .article-meta:has(> div:empty)
|
|
492
|
-
.text-detail-sidebar .article-meta:has(> div > span.text-secondary:not(.font-bold):only-child) {
|
|
501
|
+
.text-detail-sidebar .article-meta:has(> div:empty) {
|
|
493
502
|
display: none;
|
|
494
503
|
}
|
|
495
504
|
|
|
@@ -497,14 +506,6 @@ aside:has(> .text-detail-sidebar) {
|
|
|
497
506
|
gap: var(--space-sm);
|
|
498
507
|
}
|
|
499
508
|
|
|
500
|
-
/* Author byline. Dropped: single-author site. The runtime still emits it, so
|
|
501
|
-
it is suppressed here until `buildTextSidebar` stops rendering it — see the
|
|
502
|
-
runtime-change note in this branch's commit message. The product/company
|
|
503
|
-
line is the same colour but carries `font-bold`, hence the :not(). */
|
|
504
|
-
.text-detail-sidebar .article-meta>div>span.text-secondary:not(.font-bold) {
|
|
505
|
-
display: none;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
509
|
.text-detail-sidebar .article-meta .text-tertiary,
|
|
509
510
|
.text-detail-sidebar .article-meta span.font-bold {
|
|
510
511
|
font-family: var(--font-label);
|
|
@@ -861,8 +862,7 @@ aside:has(> .text-detail-sidebar) {
|
|
|
861
862
|
header, and figures that line up. */
|
|
862
863
|
/* No table surface: the header band and the hover row are the only fills, so
|
|
863
864
|
both read against the page canvas in either palette. */
|
|
864
|
-
.article-content table
|
|
865
|
-
.article-content .table-csv {
|
|
865
|
+
.article-content table {
|
|
866
866
|
margin: var(--space-xl) 0;
|
|
867
867
|
font-size: var(--type-dek);
|
|
868
868
|
font-variant-numeric: tabular-nums;
|
|
@@ -870,9 +870,7 @@ aside:has(> .text-detail-sidebar) {
|
|
|
870
870
|
}
|
|
871
871
|
|
|
872
872
|
.article-content th,
|
|
873
|
-
.article-content td
|
|
874
|
-
.article-content .table-csv th,
|
|
875
|
-
.article-content .table-csv td {
|
|
873
|
+
.article-content td {
|
|
876
874
|
border: 0;
|
|
877
875
|
border-bottom: 1px solid var(--border-gentle-subtle);
|
|
878
876
|
padding: var(--space-sm) var(--space-md);
|
|
@@ -899,8 +897,7 @@ aside:has(> .text-detail-sidebar) {
|
|
|
899
897
|
font-variant-numeric: tabular-nums;
|
|
900
898
|
}
|
|
901
899
|
|
|
902
|
-
.article-content thead th
|
|
903
|
-
.article-content .table-csv thead th {
|
|
900
|
+
.article-content thead th {
|
|
904
901
|
background: var(--color-surface-container);
|
|
905
902
|
font-family: var(--font-body);
|
|
906
903
|
font-size: var(--type-body);
|
|
@@ -910,13 +907,11 @@ aside:has(> .text-detail-sidebar) {
|
|
|
910
907
|
}
|
|
911
908
|
|
|
912
909
|
/* Zebra striping fights the row rules; the hover row is the reading aid. */
|
|
913
|
-
.article-content tbody tr:nth-child(even)
|
|
914
|
-
.article-content .table-csv tbody tr:nth-child(even) td {
|
|
910
|
+
.article-content tbody tr:nth-child(even) {
|
|
915
911
|
background: transparent;
|
|
916
912
|
}
|
|
917
913
|
|
|
918
|
-
.article-content tbody tr:hover
|
|
919
|
-
.article-content .table-csv tbody tr:hover {
|
|
914
|
+
.article-content tbody tr:hover {
|
|
920
915
|
background: var(--color-surface-container-low);
|
|
921
916
|
}
|
|
922
917
|
|
|
@@ -1131,18 +1126,16 @@ nav.text-page-nav:not(:has(a)) {
|
|
|
1131
1126
|
too wide then scrolls inside the table rather than moving the page. The
|
|
1132
1127
|
runtime emits the table with no wrapper, so the table itself has to be the
|
|
1133
1128
|
scroll container. */
|
|
1134
|
-
.article-content table
|
|
1135
|
-
.article-content .table-csv {
|
|
1129
|
+
.article-content table {
|
|
1136
1130
|
display: block;
|
|
1137
1131
|
width: 100%;
|
|
1138
1132
|
max-width: 100%;
|
|
1139
1133
|
overflow-x: auto;
|
|
1134
|
+
overflow-y: hidden;
|
|
1140
1135
|
}
|
|
1141
1136
|
|
|
1142
1137
|
.article-content th,
|
|
1143
|
-
.article-content td
|
|
1144
|
-
.article-content .table-csv th,
|
|
1145
|
-
.article-content .table-csv td {
|
|
1138
|
+
.article-content td {
|
|
1146
1139
|
padding: var(--space-xs) var(--space-sm);
|
|
1147
1140
|
}
|
|
1148
1141
|
}
|
|
@@ -230,11 +230,14 @@
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
/* A middot between items rather than a separator element, so an absent
|
|
233
|
-
license leaves no orphaned punctuation behind.
|
|
233
|
+
license leaves no orphaned punctuation behind. outline-variant is the
|
|
234
|
+
hairline token: #b8b8b8 on #f4f4f4 is 1.80:1, and #45454c on #141418
|
|
235
|
+
is 1.93:1. outline clears 4.5:1 on the same panel (6.08:1 light,
|
|
236
|
+
5.95:1 dark). */
|
|
234
237
|
.site-footer-imprint-item+.site-footer-imprint-item::before {
|
|
235
238
|
content: "·";
|
|
236
239
|
margin-right: var(--space-md);
|
|
237
|
-
color: var(--color-outline
|
|
240
|
+
color: var(--color-outline);
|
|
238
241
|
}
|
|
239
242
|
|
|
240
243
|
.site-footer-imprint-link {
|
data/assets/css/ledger/home.css
CHANGED
|
@@ -24,7 +24,7 @@ module Pandorga
|
|
|
24
24
|
|
|
25
25
|
gem "pandorga", "~> 1.3"
|
|
26
26
|
# Local checkout: gem "pandorga", path: "../pandorga"
|
|
27
|
-
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.5
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.7.5"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
data/lib/pandorga/version.rb
CHANGED
data/scripts/content/band_art.rb
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
# because the plates went stale otherwise. Cloudflare Pages excludes `content/*`
|
|
28
28
|
# from its build watch paths, so a Studio save never rebuilds the shell, and art
|
|
29
29
|
# committed into a layout is art that stops counting the day it is written. See
|
|
30
|
-
# docs/
|
|
30
|
+
# docs/deploy/cloudflare.md.
|
|
31
31
|
#
|
|
32
32
|
# The output is SVG whose every stroke and fill is a custom property, because the
|
|
33
33
|
# site has two complete palettes and switches between them on a `data-theme`
|
|
@@ -653,7 +653,7 @@ def export_derived_data(content_root, output_root)
|
|
|
653
653
|
end
|
|
654
654
|
|
|
655
655
|
# The home page's reference band, drawn from the corpus this run just exported
|
|
656
|
-
# rather than from committed markup. See docs/
|
|
656
|
+
# rather than from committed markup. See docs/deploy/cloudflare.md for why
|
|
657
657
|
# it lives here: a Studio save never rebuilds the shell, so art baked into a
|
|
658
658
|
# layout stops counting the day it is written.
|
|
659
659
|
def export_band_art(output_root, writing_index)
|
|
@@ -86,7 +86,12 @@ sheets.each do |path|
|
|
|
86
86
|
home-index-shard--gamma
|
|
87
87
|
home-index-note
|
|
88
88
|
post-content
|
|
89
|
+
home-hero-pandorga-svg
|
|
90
|
+
table-csv
|
|
89
91
|
]
|
|
92
|
+
if cleaned.include?("span.text-secondary:not(.font-bold)")
|
|
93
|
+
errors << "#{relative}: author byline suppressor matches no markup"
|
|
94
|
+
end
|
|
90
95
|
retired.each do |name|
|
|
91
96
|
next unless cleaned.match?(/[.#]#{Regexp.escape(name)}\b/)
|
|
92
97
|
|
|
@@ -94,6 +99,111 @@ sheets.each do |path|
|
|
|
94
99
|
end
|
|
95
100
|
end
|
|
96
101
|
|
|
102
|
+
# PLT-AC-36. A display equation scrolls inside the article column. Overflow
|
|
103
|
+
# on html or body would scroll the sticky header off the viewport.
|
|
104
|
+
def declaration_bodies(css, selector)
|
|
105
|
+
wanted = selector.gsub(/\s+/, " ").strip
|
|
106
|
+
bodies = []
|
|
107
|
+
css.scan(/([^{}]+)\{([^{}]*)\}/m) do |sel, body|
|
|
108
|
+
parts = sel.split(",").map { |part| part.gsub(/\s+/, " ").strip }
|
|
109
|
+
bodies << body if parts.include?(wanted)
|
|
110
|
+
end
|
|
111
|
+
bodies
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def declarations(body)
|
|
115
|
+
body.downcase.gsub(/\s+/, "")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def require_declarations(errors, label, css, selector, required)
|
|
119
|
+
bodies = declaration_bodies(css, selector)
|
|
120
|
+
if bodies.empty?
|
|
121
|
+
errors << "#{label}: missing #{selector}"
|
|
122
|
+
return
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
blob = bodies.map { |body| declarations(body) }.join(";")
|
|
126
|
+
required.each do |decl|
|
|
127
|
+
needle = decl.downcase.gsub(/\s+/, "")
|
|
128
|
+
next if blob.include?(needle)
|
|
129
|
+
|
|
130
|
+
errors << "#{label}: #{selector} missing #{decl}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def strip_comments(source)
|
|
135
|
+
source.gsub(%r{/\*.*?\*/}m, "")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
article_sheet = strip_comments(File.read(CSS_ROOT.join("article.css")))
|
|
139
|
+
detail_sheet = strip_comments(File.read(CSS_ROOT.join("ledger/detail.css")))
|
|
140
|
+
math_box = ["max-width: 100%", "overflow-x: auto", "overflow-y: hidden"]
|
|
141
|
+
|
|
142
|
+
require_declarations(
|
|
143
|
+
errors, "assets/css/article.css", article_sheet,
|
|
144
|
+
".article-content .math-display", math_box
|
|
145
|
+
)
|
|
146
|
+
require_declarations(
|
|
147
|
+
errors, "assets/css/article.css", article_sheet,
|
|
148
|
+
'.article-content .math-display mjx-container[jax][display="true"]', math_box
|
|
149
|
+
)
|
|
150
|
+
require_declarations(
|
|
151
|
+
errors, "assets/css/article.css", article_sheet,
|
|
152
|
+
'.article-content mjx-container[display="true"]', math_box
|
|
153
|
+
)
|
|
154
|
+
require_declarations(
|
|
155
|
+
errors, "assets/css/article.css", article_sheet,
|
|
156
|
+
'.article-content mjx-container[jax][display="true"]', math_box
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
if article_sheet.match?(/mjx-container[^{]*\{[^}]*overflow\s*:\s*visible/m)
|
|
160
|
+
errors << "assets/css/article.css: display math sets overflow: visible"
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
require_declarations(
|
|
164
|
+
errors, "assets/css/article.css", article_sheet,
|
|
165
|
+
".article-content figure", ["max-width: 100%"]
|
|
166
|
+
)
|
|
167
|
+
require_declarations(
|
|
168
|
+
errors, "assets/css/article.css", article_sheet,
|
|
169
|
+
".article-content svg", ["max-width: 100%"]
|
|
170
|
+
)
|
|
171
|
+
require_declarations(
|
|
172
|
+
errors, "assets/css/article.css", article_sheet,
|
|
173
|
+
".article-content .mermaid", math_box
|
|
174
|
+
)
|
|
175
|
+
require_declarations(
|
|
176
|
+
errors, "assets/css/article.css", article_sheet,
|
|
177
|
+
".article-content .mermaid svg", ["max-width: none"]
|
|
178
|
+
)
|
|
179
|
+
require_declarations(
|
|
180
|
+
errors, "assets/css/article.css", article_sheet,
|
|
181
|
+
".article-content table", ["max-width: 100%"]
|
|
182
|
+
)
|
|
183
|
+
require_declarations(
|
|
184
|
+
errors, "assets/css/ledger/detail.css", detail_sheet,
|
|
185
|
+
".article-content table", ["max-width: 100%", "overflow-x: auto", "overflow-y: hidden"]
|
|
186
|
+
)
|
|
187
|
+
require_declarations(
|
|
188
|
+
errors, "assets/css/ledger/detail.css", detail_sheet,
|
|
189
|
+
".grid.text-detail-grid > *", ["min-width: 0"]
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
sheets.each do |path|
|
|
193
|
+
relative = Pathname.new(path).relative_path_from(REPO_ROOT)
|
|
194
|
+
cleaned = strip_noise(File.read(path))
|
|
195
|
+
cleaned.scan(/([^{}]+)\{([^{}]*)\}/m) do |sel, body|
|
|
196
|
+
next unless body.match?(/overflow/i)
|
|
197
|
+
|
|
198
|
+
sel.split(",").each do |part|
|
|
199
|
+
name = part.gsub(/\s+/, " ").strip
|
|
200
|
+
next unless name == "html" || name == "body" || name == "html body"
|
|
201
|
+
|
|
202
|
+
errors << "#{relative}: #{name} sets overflow and would break the sticky header"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
97
207
|
fail!("CSS structure problems:\n - #{errors.join("\n - ")}") unless errors.empty?
|
|
98
208
|
|
|
99
209
|
puts " #{sheets.size} stylesheet(s) balanced, no conflict markers."
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
-
# Home band art
|
|
4
|
+
# Home band art. Watch paths: docs/deploy/cloudflare.md.
|
|
5
5
|
#
|
|
6
6
|
# AC-BAND-01 the export writes both plates under data/
|
|
7
7
|
# AC-BAND-03 a second export with no content change reports them unchanged
|
data/scripts/test/test-new.rb
CHANGED
|
@@ -32,6 +32,46 @@ if readme.include?("once 1.1 is on RubyGems")
|
|
|
32
32
|
exit 1
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
version = ROOT.join("lib/pandorga/version.rb").read[/VERSION = "([^"]+)"/, 1]
|
|
36
|
+
unless version
|
|
37
|
+
warn "FAIL test-new: Pandorga::VERSION missing"
|
|
38
|
+
exit 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
deploy = ROOT.join("docs/deploy/cloudflare.md").read
|
|
42
|
+
upgrading = ROOT.join("docs/upgrading.md").read
|
|
43
|
+
scaffold = ROOT.join("lib/pandorga/commands/new.rb").read
|
|
44
|
+
[
|
|
45
|
+
[deploy, "docs/deploy/cloudflare.md"],
|
|
46
|
+
[upgrading, "docs/upgrading.md"],
|
|
47
|
+
[scaffold, "lib/pandorga/commands/new.rb"]
|
|
48
|
+
].each do |text, label|
|
|
49
|
+
if text.include?("v1.0.0") || text.include?("v1.3.11") || text.include?("v1.5.0")
|
|
50
|
+
warn "FAIL test-new: #{label} still names a stale release pin"
|
|
51
|
+
exit 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
unless deploy.include?('~> 1.3') && deploy.include?(%(tag: "v#{version}"))
|
|
55
|
+
warn "FAIL test-new: deploy doc must pin ~> 1.3 and tag v#{version}"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
unless deploy.include?("content/*") && deploy.include?("docs/*") && deploy.include?("Gemfile.lock")
|
|
59
|
+
warn "FAIL test-new: deploy doc lost the watch-path include and excludes"
|
|
60
|
+
exit 1
|
|
61
|
+
end
|
|
62
|
+
unless deploy.include?("3.4.4") && deploy.include?("SKIP_DEPENDENCY_INSTALL")
|
|
63
|
+
warn "FAIL test-new: deploy doc lost the Pages Ruby pin or SKIP_DEPENDENCY_INSTALL"
|
|
64
|
+
exit 1
|
|
65
|
+
end
|
|
66
|
+
unless upgrading.include?(%(tag: "v#{version}")) && upgrading.include?("pandorga-#{version}.gem")
|
|
67
|
+
warn "FAIL test-new: upgrading examples are not v#{version}"
|
|
68
|
+
exit 1
|
|
69
|
+
end
|
|
70
|
+
unless scaffold.include?(%(tag: "v#{version}"))
|
|
71
|
+
warn "FAIL test-new: scaffold comment is not tag v#{version}"
|
|
72
|
+
exit 1
|
|
73
|
+
end
|
|
74
|
+
|
|
35
75
|
FileUtils.rm_rf(DEST)
|
|
36
76
|
|
|
37
77
|
stdout, stderr, status = run_new(DEST.to_s)
|
|
@@ -36,7 +36,7 @@ end
|
|
|
36
36
|
|
|
37
37
|
if ContentStorageConfig.placeholder?(base)
|
|
38
38
|
puts "SKIP: R2 URL alignment checks (content_api_base_url is still placeholder: #{base})"
|
|
39
|
-
puts " Fill the URL per docs/
|
|
39
|
+
puts " Fill the URL per docs/deploy/cloudflare.md before production cutover."
|
|
40
40
|
exit 0
|
|
41
41
|
end
|
|
42
42
|
|
|
@@ -54,4 +54,20 @@ ICON_FILES.each do |path|
|
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# PLT-AC-37. The listing shell keeps its index h1 in the document. A detail
|
|
58
|
+
# URL then adds the entry title, so the page has two h1 elements. The runtime
|
|
59
|
+
# demotes the index heading and sets the document title and lang from the entry.
|
|
60
|
+
DETAIL = ROOT.join("_includes/content-runtime/50-detail.html").read
|
|
61
|
+
BOOT = ROOT.join("_includes/content-runtime/90-bootstrap.html").read
|
|
62
|
+
fail!("detail runtime does not demote the listing h1") unless DETAIL.include?("data-listing-heading") && DETAIL.include?("concealListingHeader")
|
|
63
|
+
fail!("detail runtime does not restore the listing document") unless DETAIL.include?("function restoreListingDocument")
|
|
64
|
+
fail!("detail runtime does not set html lang") unless DETAIL.include?("document.documentElement.lang")
|
|
65
|
+
fail!("detail runtime dropped the entry title h1") unless DETAIL.include?('<h1 class="text-title">')
|
|
66
|
+
fail!("restoreListingDocument is not on the runtime") unless BOOT.include?("restoreListingDocument: restoreListingDocument")
|
|
67
|
+
|
|
68
|
+
%w[articles blog projects cv].each do |layout|
|
|
69
|
+
source = ROOT.join("_layouts/#{layout}.html").read
|
|
70
|
+
fail!("#{layout} listing does not restore the document") unless source.include?("restoreListingDocument()")
|
|
71
|
+
end
|
|
72
|
+
|
|
57
73
|
puts "PASS test-shell-a11y"
|
|
@@ -40,4 +40,24 @@ fail!("dark tertiary container moved") unless background.casecmp("#6a684f").zero
|
|
|
40
40
|
ratio = contrast(foreground, background)
|
|
41
41
|
fail!("#{foreground} on #{background} is #{ratio.round(2)}:1") unless ratio >= 4.5
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
# Footer imprint middot. outline-variant is a hairline and fails as ink
|
|
44
|
+
# on the panel step. The separator uses outline, which clears 4.5:1.
|
|
45
|
+
footer = ROOT.join("assets/css/ledger/footer.css").read
|
|
46
|
+
fail!("footer imprint separator still uses outline-variant") if footer.include?("color: var(--color-outline-variant)")
|
|
47
|
+
fail!("footer imprint separator lost outline") unless footer.include?("color: var(--color-outline)")
|
|
48
|
+
|
|
49
|
+
light = YAML.safe_load(ROOT.join("_data/themes/light.yml").read) || {}
|
|
50
|
+
pairs = {
|
|
51
|
+
"light" => [light.dig("neutrals", "outline"), light.dig("neutrals", "panel_contrast")],
|
|
52
|
+
"dark" => [dark.dig("neutrals", "outline"), dark.dig("neutrals", "panel_contrast")]
|
|
53
|
+
}
|
|
54
|
+
reports = []
|
|
55
|
+
pairs.each do |name, (ink, panel)|
|
|
56
|
+
fail!("#{name} outline or panel missing") if ink.to_s.empty? || panel.to_s.empty?
|
|
57
|
+
|
|
58
|
+
pair_ratio = contrast(ink, panel)
|
|
59
|
+
fail!("#{name} outline #{ink} on panel #{panel} is #{pair_ratio.round(2)}:1") unless pair_ratio >= 4.5
|
|
60
|
+
reports << "#{name} #{ink} on #{panel} = #{pair_ratio.round(2)}:1"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
puts "PASS test-theme-contrast (#{foreground} on #{background} = #{ratio.round(2)}:1; #{reports.join('; ')})"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pandorga
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.7.
|
|
4
|
+
version: 1.7.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Pandorga contributors
|
|
@@ -328,7 +328,6 @@ files:
|
|
|
328
328
|
- lib/pandorga/version.rb
|
|
329
329
|
- scripts/cdn-allowlist.json
|
|
330
330
|
- scripts/check_home_lists.mjs
|
|
331
|
-
- scripts/content/backfill-content-keys.rb
|
|
332
331
|
- scripts/content/band_art.rb
|
|
333
332
|
- scripts/content/export-content-json.rb
|
|
334
333
|
- scripts/content/flatten-ansi-capture.mjs
|
|
@@ -405,7 +404,6 @@ files:
|
|
|
405
404
|
- scripts/test/render-parity/youtube.json
|
|
406
405
|
- scripts/test/test-absent-field-absent-element.rb
|
|
407
406
|
- scripts/test/test-cdn-pins.rb
|
|
408
|
-
- scripts/test/test-cloudflare-watch-paths-doc.rb
|
|
409
407
|
- scripts/test/test-configuration-doc.rb
|
|
410
408
|
- scripts/test/test-content-backend.rb
|
|
411
409
|
- scripts/test/test-content-detail-urls.rb
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "pathname"
|
|
5
|
-
require "yaml"
|
|
6
|
-
|
|
7
|
-
require_relative "../../_plugins/content_keys"
|
|
8
|
-
require_relative "../../_plugins/content_detail_paths"
|
|
9
|
-
require_relative "../../_plugins/git_chronology"
|
|
10
|
-
|
|
11
|
-
REPO_ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
12
|
-
CONTENT_ROOT = REPO_ROOT.join("content")
|
|
13
|
-
|
|
14
|
-
KEY_OVERRIDES = {
|
|
15
|
-
"content/collections/posts/2026-03-06-horse-auto-follow.md" => "horse-auto-follow-pt",
|
|
16
|
-
"content/collections/posts/2026-03-02-mcmc.md" => "mcmc-pt"
|
|
17
|
-
}.freeze
|
|
18
|
-
|
|
19
|
-
def split_front_matter(text)
|
|
20
|
-
ContentKeys.split_front_matter(text)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def write_front_matter(path, front_matter, body)
|
|
24
|
-
yaml = front_matter.to_yaml.sub(/\A---\n/, "").strip
|
|
25
|
-
path.write("---\n#{yaml}\n---\n\n#{body}")
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def infer_key(entry)
|
|
29
|
-
override = KEY_OVERRIDES[entry["repo_relative_path"]]
|
|
30
|
-
return override if override
|
|
31
|
-
|
|
32
|
-
key = ContentKeys.normalize_key(entry["key"])
|
|
33
|
-
return key unless key.empty?
|
|
34
|
-
|
|
35
|
-
ContentKeys.infer_key_from_filename(entry["path"], entry["type"])
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def legacy_detail_path(type, legacy_segment)
|
|
39
|
-
case type
|
|
40
|
-
when "article" then ContentDetailPaths.article_detail_path(legacy_segment)
|
|
41
|
-
when "post" then ContentDetailPaths.post_detail_path(legacy_segment)
|
|
42
|
-
when "project" then ContentDetailPaths.project_detail_path(legacy_segment)
|
|
43
|
-
when "product" then ContentDetailPaths.product_detail_path(legacy_segment)
|
|
44
|
-
else
|
|
45
|
-
raise "unknown type #{type}"
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def migrate_redirect_fields!(front_matter)
|
|
50
|
-
redirects = ContentKeys.normalize_redirects(front_matter)
|
|
51
|
-
front_matter.delete("redirect_from")
|
|
52
|
-
front_matter["redirects"] = redirects unless redirects.empty?
|
|
53
|
-
front_matter
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def ensure_legacy_redirect!(front_matter, type, legacy_segment, canonical_path)
|
|
57
|
-
redirects = ContentKeys.normalize_redirects(front_matter)
|
|
58
|
-
legacy = legacy_detail_path(type, legacy_segment).chomp("/")
|
|
59
|
-
canonical = canonical_path.chomp("/")
|
|
60
|
-
redirects << legacy unless legacy == canonical
|
|
61
|
-
redirects = redirects.map { |path| ContentKeys.normalize_redirect_path(path) }.compact.uniq
|
|
62
|
-
front_matter["redirects"] = redirects unless redirects.empty?
|
|
63
|
-
front_matter.delete("redirects") if redirects.empty?
|
|
64
|
-
front_matter
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
updated = 0
|
|
68
|
-
ContentKeys::CONTENT_PATHS.each do |type, relative_dir|
|
|
69
|
-
directory = CONTENT_ROOT.join(relative_dir.sub(%r{\Acontent/}, ""))
|
|
70
|
-
next unless directory.directory?
|
|
71
|
-
|
|
72
|
-
Dir.glob(directory.join("*.{md,markdown}")).sort.each do |absolute_path|
|
|
73
|
-
entry = ContentKeys.load_entry(absolute_path, type, repo_root: REPO_ROOT)
|
|
74
|
-
text = File.read(absolute_path).sub(/\A\xEF\xBB\xBF/, "")
|
|
75
|
-
front_matter, body = split_front_matter(text)
|
|
76
|
-
original_yaml = front_matter.to_yaml
|
|
77
|
-
|
|
78
|
-
key = infer_key(entry)
|
|
79
|
-
front_matter["key"] = key
|
|
80
|
-
migrate_redirect_fields!(front_matter)
|
|
81
|
-
|
|
82
|
-
if %w[article post].include?(type)
|
|
83
|
-
GitChronology.apply!(front_matter, entry["repo_relative_path"], type)
|
|
84
|
-
end
|
|
85
|
-
|
|
86
|
-
canonical = ContentKeys.detail_path(type, key, date: front_matter["date"])
|
|
87
|
-
ensure_legacy_redirect!(front_matter, type, entry["legacy_segment"], canonical)
|
|
88
|
-
|
|
89
|
-
next if front_matter.to_yaml == original_yaml
|
|
90
|
-
|
|
91
|
-
write_front_matter(Pathname(absolute_path), front_matter, body.sub(/\A\n+/, ""))
|
|
92
|
-
updated += 1
|
|
93
|
-
puts "updated #{entry['repo_relative_path']} (key=#{key})"
|
|
94
|
-
end
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
ContentKeys.validate!(REPO_ROOT)
|
|
98
|
-
ContentKeys.sync_redirects_file!(REPO_ROOT)
|
|
99
|
-
puts "Backfill complete (#{updated} file(s) updated). Redirects synced to _redirects."
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require "pathname"
|
|
5
|
-
|
|
6
|
-
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
7
|
-
DOC = ROOT.join("docs/ops/cloudflare.md")
|
|
8
|
-
|
|
9
|
-
def check!(condition, message = "condition was false")
|
|
10
|
-
raise "Assertion failed: #{message}" unless condition
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
text = DOC.read
|
|
14
|
-
|
|
15
|
-
puts "Testing Cloudflare watch paths documentation..."
|
|
16
|
-
|
|
17
|
-
check! text.include?("`*`"), "missing include pattern `*`"
|
|
18
|
-
check! text.include?("`content/*`"), "missing exclude pattern content/*"
|
|
19
|
-
check! text.include?("`docs/*`"), "missing exclude pattern docs/*"
|
|
20
|
-
check! text.include?("Settings → Build → Build watch paths"),
|
|
21
|
-
"missing dashboard navigation path"
|
|
22
|
-
check! text.include?("SKIP_DEPENDENCY_INSTALL"),
|
|
23
|
-
"missing SKIP_DEPENDENCY_INSTALL (avoids Pages pip install of study deps)"
|
|
24
|
-
check! text.include?("bundle install") && text.include?("jekyll build"),
|
|
25
|
-
"missing lean Pages build command with explicit bundle install"
|
|
26
|
-
check! text.include?("pandorga install-functions"),
|
|
27
|
-
"missing pandorga install-functions in Pages build command (PLT-0.2)"
|
|
28
|
-
check! text.include?("3.4.4"),
|
|
29
|
-
"missing Pages v3 Ruby 3.4.4 pin (avoids compiling Ruby from source)"
|
|
30
|
-
|
|
31
|
-
puts "Cloudflare watch paths documentation OK."
|