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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8935cbf5ca2ee93ea990c5e18d61831877f04f03b3ba569b3d4347dec3387718
4
- data.tar.gz: 5c9663302fcce8b2ecf4a5197b03ca66f1f0cca0c7eb2ee0b9d9c9ab2d5e320e
3
+ metadata.gz: 50cefbd5818ff3cacd667823f637d8dd3aca3a0dd06fb78a01f44d68954b8a28
4
+ data.tar.gz: a602af73319ba4acc679ffdc30d75b555663dfb0ce54febf32823ce8e213cc03
5
5
  SHA512:
6
- metadata.gz: ba771e313ba7ecbc5d13473c75454468d22d18f6f2ec1d22ca896d3bf9bec124aa9b32f5efff0dce38c7495a20a422110313910d56808f7b123ce4e07b29e322
7
- data.tar.gz: b29d2e32efc698af41f0fdcf7b9da605adfbc238a249dd050c157103493b9305e9fb71a355f919e8189a4a43d41b76b25df2a96b446627a3ba6380f6631fc1f5
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,
@@ -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) {
@@ -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
- So the wrapper owns the gap, the scroll and the padding; the container
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 — the two are one
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
- /* 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;
125
+ padding: 0;
133
126
  overflow-x: auto;
127
+ overflow-y: hidden;
134
128
  text-align: center;
135
129
  }
136
130
 
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. */
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
- overflow: visible;
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
- .article-content mjx-container[display="true"] {
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.4"
27
+ # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.7.5"
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.5"
5
5
  end
@@ -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"
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.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors