pandorga 1.7.4 → 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 +23 -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 +66 -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 +105 -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: 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,28 @@
|
|
|
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
|
+
|
|
3
26
|
## [1.7.4] — 2026-10-12
|
|
4
27
|
|
|
5
28
|
### 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
|
@@ -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;
|
|
@@ -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.5"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -99,6 +99,111 @@ 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
|
+
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
|
+
|
|
102
207
|
fail!("CSS structure problems:\n - #{errors.join("\n - ")}") unless errors.empty?
|
|
103
208
|
|
|
104
209
|
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"
|