pandorga 1.7.4 → 1.7.6
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 +38 -0
- data/_includes/content-runtime/50-detail.html +64 -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 +83 -19
- data/assets/css/ledger/detail.css +12 -0
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/test-css-structure.rb +127 -0
- data/scripts/test/test-shell-a11y.rb +16 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5073aa0e7217490eff99e42008ea7eb10fdd6d5f7218d924d7ede563a77c2f4
|
|
4
|
+
data.tar.gz: 38b8947ef41f97e1e1c6c6c2d2048fd5a16ebd1a441d4dc6ae62a9b5237582c1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 73d9948f8662e076b4f652697969ed7da53c108b336efea4b91c497310c7332f3c82f3d4ee854f6630a248c27356d219b6d50ffec70e437b098340a1153b1ee0
|
|
7
|
+
data.tar.gz: b2dde560408d681b3cfcf3a4f5bc377542d1179b28279a19bbd065b8f7f9d04a994ebe7800a20645a18eb6562c9335d6e785f22f952d6c4b605c4f61f1ad912e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.7.6] — 2026-10-12
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- A long inline formula scrolls inside the article column below 768px.
|
|
8
|
+
Display math was already contained, and a 390px phone still measured
|
|
9
|
+
scrollWidth 667 because `mjx-container` without `display="true"` is
|
|
10
|
+
`display: inline` and cannot scroll. Below 768px that box is
|
|
11
|
+
`inline-block`, `max-width: 100%`, `overflow-x: auto`, and
|
|
12
|
+
`overflow-y: hidden`. Wider viewports keep it inline. `html` and
|
|
13
|
+
`body` do not set overflow.
|
|
14
|
+
|
|
15
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
16
|
+
ledger fallbacks are unchanged.
|
|
17
|
+
|
|
18
|
+
## [1.7.5] — 2026-10-12
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- A display equation scrolls inside the article column. On a 390px
|
|
23
|
+
viewport a MathJax display box about 455px wide was stretching the
|
|
24
|
+
document to a scrollWidth of 667, so the page moved sideways and the
|
|
25
|
+
sticky header scrolled with it. `mjx-container[display="true"]` and
|
|
26
|
+
`mjx-container[jax][display="true"]` are `max-width: 100%` with
|
|
27
|
+
`overflow-x: auto` and `overflow-y: hidden`. Tables, figures, and
|
|
28
|
+
article SVG are bounded to the column. Mermaid still scrolls inside
|
|
29
|
+
`.mermaid`. `html` and `body` do not set overflow, so the sticky
|
|
30
|
+
header still sticks.
|
|
31
|
+
- A detail URL served by the listing shell had two `h1` elements: the
|
|
32
|
+
listing title (Articles, Rascunhos, Projects, Curriculum) and the
|
|
33
|
+
entry title. While the entry is showing, the listing heading is not
|
|
34
|
+
an `h1`. The document title is the entry title, and `html` `lang`
|
|
35
|
+
follows the entry (`en`, `pt-BR`, `fr`, `es`). Articles, posts,
|
|
36
|
+
projects, and products.
|
|
37
|
+
|
|
38
|
+
No `pandorga:` config keys were added or renamed. Font roles and the
|
|
39
|
+
ledger fallbacks are unchanged.
|
|
40
|
+
|
|
3
41
|
## [1.7.4] — 2026-10-12
|
|
4
42
|
|
|
5
43
|
### 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');
|
|
@@ -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
|
@@ -99,6 +99,23 @@
|
|
|
99
99
|
vertical-align: baseline;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/* Below 768px a long inline formula is still `display: inline`, so it has
|
|
103
|
+
no scrollport and widens the document. Display math was already contained,
|
|
104
|
+
and a 390px phone still measured scrollWidth 667 from the inline boxes.
|
|
105
|
+
`inline-block` keeps the formula in the line and lets max-width and
|
|
106
|
+
overflow apply. `[jax]` stays ahead of MathJax's own container rule.
|
|
107
|
+
Desktop stays inline. Do not put this overflow on html or body. */
|
|
108
|
+
@media (max-width: 767px) {
|
|
109
|
+
.article-content mjx-container:not([display="true"]),
|
|
110
|
+
.article-content mjx-container[jax]:not([display="true"]) {
|
|
111
|
+
display: inline-block;
|
|
112
|
+
max-width: 100%;
|
|
113
|
+
overflow-x: auto;
|
|
114
|
+
overflow-y: hidden;
|
|
115
|
+
vertical-align: baseline;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
102
119
|
/* A display equation is a block in the prose flow, so it takes the paragraph
|
|
103
120
|
gap and nothing more. Two things used to inflate it, to a measured 52px
|
|
104
121
|
above the equation and 39px below against a 22px paragraph gap.
|
|
@@ -112,44 +129,58 @@
|
|
|
112
129
|
which outranks `.article-content mjx-container[display="true"]` on
|
|
113
130
|
specificity. The 0.5rem this file asked for was never what the page drew.
|
|
114
131
|
|
|
115
|
-
|
|
116
|
-
inside it owns none of them. */
|
|
132
|
+
The wrapper owns the paragraph gap. The container owns the scroll. */
|
|
117
133
|
.article-content .math-display {
|
|
118
134
|
display: block;
|
|
135
|
+
max-width: 100%;
|
|
119
136
|
/* The bottom gap is the paragraph gap `.article-content p` sets in
|
|
120
137
|
ledger/detail.css; the top gap is left to the previous block, exactly as
|
|
121
138
|
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. */
|
|
139
|
+
container padding below, which a margin collapse cannot absorb. The two
|
|
140
|
+
are one number, so change them together or the rhythm moves. */
|
|
124
141
|
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;
|
|
142
|
+
padding: 0;
|
|
133
143
|
overflow-x: auto;
|
|
144
|
+
overflow-y: hidden;
|
|
134
145
|
text-align: center;
|
|
135
146
|
}
|
|
136
147
|
|
|
137
|
-
/* `[jax]`
|
|
138
|
-
|
|
139
|
-
|
|
148
|
+
/* `[jax]` lifts this rule past MathJax's injected margin
|
|
149
|
+
(`mjx-container[jax="CHTML"][display="true"]`). The container is the clip
|
|
150
|
+
edge: on a 390px phone a display `mjx-math` about 455px wide was stretching
|
|
151
|
+
the document to a scrollWidth of 667, because this rule was
|
|
152
|
+
`overflow: visible`. The page then scrolled sideways and the sticky header
|
|
153
|
+
moved with it. `max-width: 100%` plus `overflow-x: auto` and
|
|
154
|
+
`overflow-y: hidden` scroll the equation inside the column, the way `pre`
|
|
155
|
+
already scrolls code. `overflow-y: hidden` is stated so a radical does not
|
|
156
|
+
grow a vertical scrollbar, and so a specified `visible` cannot let the
|
|
157
|
+
equation's min-content widen the column. The 1.25rem padding is the
|
|
158
|
+
allowance for a bracket, radical, or matrix rule MathJax draws past its
|
|
159
|
+
line box (a 3-row bmatrix, measured at 12px past a 0.5rem allowance).
|
|
160
|
+
Do not move this overflow onto `html` or `body`. That scrollport breaks
|
|
161
|
+
`position: sticky` on the header. */
|
|
140
162
|
.article-content .math-display mjx-container[jax][display="true"] {
|
|
163
|
+
display: block;
|
|
141
164
|
margin: 0;
|
|
142
|
-
padding: 0;
|
|
143
|
-
|
|
165
|
+
padding: 1.25rem 0;
|
|
166
|
+
max-width: 100%;
|
|
167
|
+
overflow-x: auto;
|
|
168
|
+
overflow-y: hidden;
|
|
169
|
+
text-align: center;
|
|
144
170
|
}
|
|
145
171
|
|
|
146
172
|
/* Display math the wrapper regex cannot reach, because the `$$` sits inside a
|
|
147
173
|
list item or a table cell rather than alone in a paragraph. It still has to
|
|
148
174
|
break the line; its vertical margin stays MathJax's own 1em, which lands
|
|
149
|
-
within a pixel of the paragraph gap.
|
|
150
|
-
|
|
175
|
+
within a pixel of the paragraph gap. `[jax]` names the same box. Every
|
|
176
|
+
MathJax container carries it, and the attribute keeps this rule ahead of
|
|
177
|
+
the injected stylesheet. */
|
|
178
|
+
.article-content mjx-container[display="true"],
|
|
179
|
+
.article-content mjx-container[jax][display="true"] {
|
|
151
180
|
display: block;
|
|
181
|
+
max-width: 100%;
|
|
152
182
|
overflow-x: auto;
|
|
183
|
+
overflow-y: hidden;
|
|
153
184
|
text-align: center;
|
|
154
185
|
}
|
|
155
186
|
|
|
@@ -248,7 +279,9 @@
|
|
|
248
279
|
:is(.article-content, .article-cite-block) pre {
|
|
249
280
|
background: var(--color-surface-container);
|
|
250
281
|
padding: 1rem;
|
|
282
|
+
max-width: 100%;
|
|
251
283
|
overflow-x: auto;
|
|
284
|
+
overflow-y: hidden;
|
|
252
285
|
margin-bottom: 1rem;
|
|
253
286
|
font-family: var(--font-code);
|
|
254
287
|
}
|
|
@@ -359,11 +392,30 @@
|
|
|
359
392
|
margin: 1.5rem auto;
|
|
360
393
|
text-align: center;
|
|
361
394
|
width: 100%;
|
|
395
|
+
max-width: 100%;
|
|
362
396
|
display: block;
|
|
363
397
|
background: var(--color-surface-container-low);
|
|
364
398
|
padding: var(--space-md);
|
|
365
399
|
}
|
|
366
400
|
|
|
401
|
+
/* A wide SVG, video, or iframe must stay inside the plate. Cut-in plates
|
|
402
|
+
and YouTube shorts set a narrower max-width on a more specific rule.
|
|
403
|
+
Mermaid and display-math SVG opt back out below so those boxes scroll
|
|
404
|
+
instead of shrinking the drawing. Iframes keep their own height: `height:
|
|
405
|
+
auto` collapses a YouTube or Plotly frame that has no intrinsic ratio. */
|
|
406
|
+
.article-content figure svg,
|
|
407
|
+
.article-content svg {
|
|
408
|
+
max-width: 100%;
|
|
409
|
+
height: auto;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.article-content figure video,
|
|
413
|
+
.article-content figure iframe,
|
|
414
|
+
.article-content figure canvas,
|
|
415
|
+
.article-content iframe {
|
|
416
|
+
max-width: 100%;
|
|
417
|
+
}
|
|
418
|
+
|
|
367
419
|
.article-content figure img {
|
|
368
420
|
margin-inline: auto;
|
|
369
421
|
}
|
|
@@ -554,7 +606,9 @@ body.figure-lightbox-open {
|
|
|
554
606
|
padding: var(--space-xl) var(--space-lg);
|
|
555
607
|
background: var(--color-surface-container);
|
|
556
608
|
text-align: center;
|
|
609
|
+
max-width: 100%;
|
|
557
610
|
overflow-x: auto;
|
|
611
|
+
overflow-y: hidden;
|
|
558
612
|
-webkit-overflow-scrolling: touch;
|
|
559
613
|
}
|
|
560
614
|
|
|
@@ -567,6 +621,15 @@ body.figure-lightbox-open {
|
|
|
567
621
|
height: auto;
|
|
568
622
|
}
|
|
569
623
|
|
|
624
|
+
/* SVG output of a display equation keeps its own width and scrolls inside
|
|
625
|
+
`mjx-container`. Inline math is not included: `display: block` here would
|
|
626
|
+
pull an inline equation out of the line. */
|
|
627
|
+
.article-content mjx-container[display="true"] svg {
|
|
628
|
+
max-width: none;
|
|
629
|
+
width: auto;
|
|
630
|
+
height: auto;
|
|
631
|
+
}
|
|
632
|
+
|
|
570
633
|
/* Captions are sentences. The include centres the whole figure, and a centred
|
|
571
634
|
paragraph of provenance is harder to read than a left-set one; the rule on
|
|
572
635
|
the element itself beats the `text-center` utility the runtime emits. The
|
|
@@ -628,6 +691,7 @@ body.figure-lightbox-open {
|
|
|
628
691
|
/* ── Tables ── */
|
|
629
692
|
.article-content table {
|
|
630
693
|
width: 100%;
|
|
694
|
+
max-width: 100%;
|
|
631
695
|
border-collapse: collapse;
|
|
632
696
|
margin-bottom: 1.5rem;
|
|
633
697
|
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;
|
|
@@ -1120,6 +1131,7 @@ nav.text-page-nav:not(:has(a)) {
|
|
|
1120
1131
|
width: 100%;
|
|
1121
1132
|
max-width: 100%;
|
|
1122
1133
|
overflow-x: auto;
|
|
1134
|
+
overflow-y: hidden;
|
|
1123
1135
|
}
|
|
1124
1136
|
|
|
1125
1137
|
.article-content th,
|
|
@@ -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.7.
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.7.6"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -99,6 +99,133 @@ sheets.each do |path|
|
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
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
|
+
# PLT-AC-38. Inline formulas scroll inside the column below 768px. The
|
|
164
|
+
# unscoped rule stays display:inline so a short formula on a wide column
|
|
165
|
+
# remains in the line.
|
|
166
|
+
inline_selector = '.article-content mjx-container:not([display="true"])'
|
|
167
|
+
inline_jax = '.article-content mjx-container[jax]:not([display="true"])'
|
|
168
|
+
inline_bodies = declaration_bodies(article_sheet, inline_selector)
|
|
169
|
+
inline_base = inline_bodies.find { |body| body.match?(/display:\s*inline\s*;/) && !body.match?(/inline-block/) }
|
|
170
|
+
unless inline_base
|
|
171
|
+
errors << "assets/css/article.css: #{inline_selector} must stay display:inline outside the phone query"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
mobile_inline = nil
|
|
175
|
+
article_sheet.scan(/@media\s*\(\s*max-width:\s*767px\s*\)\s*\{((?:[^{}]|\{[^{}]*\})*)\}/m) do |inner,|
|
|
176
|
+
mobile_inline = inner if inner.include?(inline_selector)
|
|
177
|
+
end
|
|
178
|
+
if mobile_inline.nil?
|
|
179
|
+
errors << "assets/css/article.css: inline math scroll is not limited to max-width 767px"
|
|
180
|
+
else
|
|
181
|
+
require_declarations(errors, "assets/css/article.css", mobile_inline, inline_selector, math_box + ["display: inline-block"])
|
|
182
|
+
require_declarations(errors, "assets/css/article.css", mobile_inline, inline_jax, math_box + ["display: inline-block"])
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
require_declarations(
|
|
186
|
+
errors, "assets/css/article.css", article_sheet,
|
|
187
|
+
".article-content figure", ["max-width: 100%"]
|
|
188
|
+
)
|
|
189
|
+
require_declarations(
|
|
190
|
+
errors, "assets/css/article.css", article_sheet,
|
|
191
|
+
".article-content svg", ["max-width: 100%"]
|
|
192
|
+
)
|
|
193
|
+
require_declarations(
|
|
194
|
+
errors, "assets/css/article.css", article_sheet,
|
|
195
|
+
".article-content .mermaid", math_box
|
|
196
|
+
)
|
|
197
|
+
require_declarations(
|
|
198
|
+
errors, "assets/css/article.css", article_sheet,
|
|
199
|
+
".article-content .mermaid svg", ["max-width: none"]
|
|
200
|
+
)
|
|
201
|
+
require_declarations(
|
|
202
|
+
errors, "assets/css/article.css", article_sheet,
|
|
203
|
+
".article-content table", ["max-width: 100%"]
|
|
204
|
+
)
|
|
205
|
+
require_declarations(
|
|
206
|
+
errors, "assets/css/ledger/detail.css", detail_sheet,
|
|
207
|
+
".article-content table", ["max-width: 100%", "overflow-x: auto", "overflow-y: hidden"]
|
|
208
|
+
)
|
|
209
|
+
require_declarations(
|
|
210
|
+
errors, "assets/css/ledger/detail.css", detail_sheet,
|
|
211
|
+
".grid.text-detail-grid > *", ["min-width: 0"]
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
sheets.each do |path|
|
|
215
|
+
relative = Pathname.new(path).relative_path_from(REPO_ROOT)
|
|
216
|
+
cleaned = strip_noise(File.read(path))
|
|
217
|
+
cleaned.scan(/([^{}]+)\{([^{}]*)\}/m) do |sel, body|
|
|
218
|
+
next unless body.match?(/overflow/i)
|
|
219
|
+
|
|
220
|
+
sel.split(",").each do |part|
|
|
221
|
+
name = part.gsub(/\s+/, " ").strip
|
|
222
|
+
next unless name == "html" || name == "body" || name == "html body"
|
|
223
|
+
|
|
224
|
+
errors << "#{relative}: #{name} sets overflow and would break the sticky header"
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
102
229
|
fail!("CSS structure problems:\n - #{errors.join("\n - ")}") unless errors.empty?
|
|
103
230
|
|
|
104
231
|
puts " #{sheets.size} stylesheet(s) balanced, no conflict markers."
|
|
@@ -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"
|