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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8935cbf5ca2ee93ea990c5e18d61831877f04f03b3ba569b3d4347dec3387718
4
- data.tar.gz: 5c9663302fcce8b2ecf4a5197b03ca66f1f0cca0c7eb2ee0b9d9c9ab2d5e320e
3
+ metadata.gz: b5073aa0e7217490eff99e42008ea7eb10fdd6d5f7218d924d7ede563a77c2f4
4
+ data.tar.gz: 38b8947ef41f97e1e1c6c6c2d2048fd5a16ebd1a441d4dc6ae62a9b5237582c1
5
5
  SHA512:
6
- metadata.gz: ba771e313ba7ecbc5d13473c75454468d22d18f6f2ec1d22ca896d3bf9bec124aa9b32f5efff0dce38c7495a20a422110313910d56808f7b123ce4e07b29e322
7
- data.tar.gz: b29d2e32efc698af41f0fdcf7b9da605adfbc238a249dd050c157103493b9305e9fb71a355f919e8189a4a43d41b76b25df2a96b446627a3ba6380f6631fc1f5
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,
@@ -434,6 +434,7 @@ layout: shell
434
434
  if (detailSidebar) detailSidebar.innerHTML = '';
435
435
  runtime.removeRuntimeBackLink();
436
436
  runtime.removeArticleToc();
437
+ runtime.restoreListingDocument();
437
438
  }
438
439
 
439
440
  async function renderArticleDetail(article, articles) {
data/_layouts/blog.html CHANGED
@@ -425,6 +425,7 @@ layout: shell
425
425
  if (detailSidebar) detailSidebar.innerHTML = '';
426
426
  runtime.removeRuntimeBackLink();
427
427
  runtime.removeArticleToc();
428
+ runtime.restoreListingDocument();
428
429
  }
429
430
 
430
431
  async function renderPostDetail(post, posts) {
data/_layouts/cv.html CHANGED
@@ -280,6 +280,7 @@ layout: shell
280
280
  if (detailMain) detailMain.innerHTML = '';
281
281
  if (detailSidebar) detailSidebar.innerHTML = '';
282
282
  runtime.removeRuntimeBackLink();
283
+ runtime.restoreListingDocument();
283
284
  }
284
285
 
285
286
  async function renderProductDetail(item) {
@@ -354,6 +354,7 @@ layout: shell
354
354
  if (detailHero) detailHero.innerHTML = '';
355
355
  runtime.removeArticleToc();
356
356
  runtime.removeRuntimeBackLink();
357
+ runtime.restoreListingDocument();
357
358
  }
358
359
 
359
360
  function renderListing(projects) {
@@ -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
- So the wrapper owns the gap, the scroll and the padding; the container
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 — the two are one
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
- /* overflow-x lets an over-wide equation scroll sideways. CSS forces
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]` is redundant as a selector — every MathJax container carries it —
138
- but it lifts this rule past MathJax's own injected margin, which is the
139
- whole point of the rule. */
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
- overflow: visible;
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
- .article-content mjx-container[display="true"] {
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.4"
27
+ # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.7.6"
28
28
  gem "csv"
29
29
  gem "base64"
30
30
  RUBY
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.7.4"
4
+ VERSION = "1.7.6"
5
5
  end
@@ -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"
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
4
+ version: 1.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors