pandorga 1.7.5 → 1.8.1

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: 50cefbd5818ff3cacd667823f637d8dd3aca3a0dd06fb78a01f44d68954b8a28
4
- data.tar.gz: a602af73319ba4acc679ffdc30d75b555663dfb0ce54febf32823ce8e213cc03
3
+ metadata.gz: 03c8db4a237e9092a2f0d9256c118a162476cb1d056fc88518cfeccd60379bed
4
+ data.tar.gz: e8a2725fbc96d9c02ad55e39dce2c8e98d1eec14f20996ae5636d60f2d54181e
5
5
  SHA512:
6
- metadata.gz: 97f05ec16f99648f22c74146bb9f8ce497e3afa759727b8cc5d09f50dcdceee955e3c993e1ada003cd1492a5cd0c9975dbfa9d05bd56668aaf97eec945236425
7
- data.tar.gz: a6a9494a944b8807f594e1f3c1dd6bc8337aade5ba8c53a7b29e3a905575d39f6b5fd310ad99ac00d7d017d44b182e1d78db85e52c218057d430848455d0135f
6
+ metadata.gz: a4e9108eac5982492245285b4954dc1e43a0ca10534411a8a221af35c5520a5a65c3a0fc9e8700bfc9c27588e41ee332a0204ebb697b9339514128e70b5237d1
7
+ data.tar.gz: 8ae715831b65d21cf206f071841f6c444f0b99598d93a2917ccaa09f4eecce658e2e93c0b61a75e375afa9b9a9baf0a6f17a47c70584f4ac6c5f656f8a728e75
data/CHANGELOG.md CHANGED
@@ -1,5 +1,49 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.8.1] — 2026-10-12
4
+
5
+ ### Fixed
6
+
7
+ - Theorems are bare (no gray plate); only proofs carry the surface band
8
+ - Proof gray bleeds to the edges of `.text-page-main` while body text stays
9
+ on the reading measure
10
+ - Proofs prefix **Proof.** from the runtime, like theorem heads — authors
11
+ no longer write `*Proof.*` in the body
12
+
13
+ No `pandorga:` config keys were added or renamed. Font roles and the
14
+ ledger fallbacks are unchanged.
15
+
16
+ ## [1.8.0] — 2026-10-12
17
+
18
+ ### Added
19
+
20
+ - Theorem and proof Markdown wrappers in the content runtime, same
21
+ `markdown="1"` contract as `article-note` and `poem`:
22
+ - `<div class="theorem" name="Corollary 1" description="nadir camera" markdown="1">…</div>`
23
+ builds the head from `name` / `description` and italicizes the body
24
+ - `<div class="proof" markdown="1">…</div>` wraps a proof and places a
25
+ QED square on the last line
26
+ - Full-column styles for `.theorem` and `.proof` in `article.css`
27
+ - Render-parity fixtures `theorem.json` and `proof.json`
28
+
29
+ No `pandorga:` config keys were added or renamed. Font roles and the
30
+ ledger fallbacks are unchanged.
31
+
32
+ ## [1.7.6] — 2026-10-12
33
+
34
+ ### Fixed
35
+
36
+ - A long inline formula scrolls inside the article column below 768px.
37
+ Display math was already contained, and a 390px phone still measured
38
+ scrollWidth 667 because `mjx-container` without `display="true"` is
39
+ `display: inline` and cannot scroll. Below 768px that box is
40
+ `inline-block`, `max-width: 100%`, `overflow-x: auto`, and
41
+ `overflow-y: hidden`. Wider viewports keep it inline. `html` and
42
+ `body` do not set overflow.
43
+
44
+ No `pandorga:` config keys were added or renamed. Font roles and the
45
+ ledger fallbacks are unchanged.
46
+
3
47
  ## [1.7.5] — 2026-10-12
4
48
 
5
49
  ### Fixed
@@ -109,6 +109,31 @@
109
109
  return parts.join('');
110
110
  }
111
111
 
112
+ function attrValue(openTag, name) {
113
+ var re = new RegExp('\\b' + name + '\\s*=\\s*(?:"([^"]*)"|\'([^\']*)\')', 'i');
114
+ var m = String(openTag || '').match(re);
115
+ return m ? String(m[1] != null ? m[1] : m[2] || '') : '';
116
+ }
117
+
118
+ function renderTheoremHtml(openTag, inner) {
119
+ var name = attrValue(openTag, 'name').trim();
120
+ var description = attrValue(openTag, 'description').trim();
121
+ var head = '';
122
+ if (name) {
123
+ head = '<p class="theorem-head"><span class="theorem-name">' + escapeHtml(name) + '</span>';
124
+ if (description) {
125
+ head += ' <span class="theorem-description">(' + escapeHtml(description) + ')</span>';
126
+ }
127
+ head += '.</p>';
128
+ }
129
+ return '<div class="theorem">' + head + '<div class="theorem-body">' + parseInlineMarkdown(inner) + '</div></div>';
130
+ }
131
+
132
+ function renderProofHtml(inner) {
133
+ var head = '<p class="proof-head"><span class="proof-name">Proof</span>.</p>';
134
+ return '<div class="proof">' + head + '<div class="proof-body">' + parseInlineMarkdown(inner) + '</div></div>';
135
+ }
136
+
112
137
  function preprocessMarkdownDivs(markdown) {
113
138
  var text = String(markdown || '');
114
139
  text = text.replace(/<div\s+([^>]*class="[^"]*\barticle-note\b[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi, function (match, openTag, inner) {
@@ -119,6 +144,16 @@
119
144
  if (!hasMarkdownAttr(openTag)) return match;
120
145
  return renderPoemHtml(inner);
121
146
  });
147
+ // Theorem / proof: same markdown="1" contract as article-note. Without
148
+ // this pass, CommonMark ends the HTML block at the first blank line.
149
+ text = text.replace(/<div\s+([^>]*class="[^"]*\btheorem\b[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi, function (match, openTag, inner) {
150
+ if (!hasMarkdownAttr(openTag)) return match;
151
+ return renderTheoremHtml(openTag, inner);
152
+ });
153
+ text = text.replace(/<div\s+([^>]*class="[^"]*\bproof\b[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi, function (match, openTag, inner) {
154
+ if (!hasMarkdownAttr(openTag)) return match;
155
+ return renderProofHtml(inner);
156
+ });
122
157
  return text;
123
158
  }
124
159
 
@@ -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.
@@ -990,6 +1007,91 @@ a.xcite-ref:hover {
990
1007
  margin-bottom: 0;
991
1008
  }
992
1009
 
1010
+ /* ── Theorem and proof blocks ──
1011
+ Authored as `<div class="theorem" name="…" description="…" markdown="1">`
1012
+ and `<div class="proof" markdown="1">`. Theorems are bare (no plate): the
1013
+ runtime builds the head from name / description and italicizes the body.
1014
+ Proofs sit on a gray band that bleeds to the edges of `.text-page-main`
1015
+ while the text keeps the body measure; the runtime prefixes "Proof." and
1016
+ a QED square lands on the last line of the body. */
1017
+ .article-content .theorem {
1018
+ display: block;
1019
+ width: 100%;
1020
+ box-sizing: border-box;
1021
+ margin: 1.5em 0;
1022
+ font-size: inherit;
1023
+ line-height: inherit;
1024
+ color: var(--color-on-surface);
1025
+ }
1026
+
1027
+ .article-content .theorem-head,
1028
+ .article-content .proof-head {
1029
+ margin: 0 0 0.5em;
1030
+ font-style: normal;
1031
+ font-weight: var(--weight-body);
1032
+ }
1033
+
1034
+ .article-content .theorem-name,
1035
+ .article-content .proof-name {
1036
+ font-weight: var(--weight-emphasis);
1037
+ font-style: normal;
1038
+ }
1039
+
1040
+ .article-content .theorem-description {
1041
+ font-weight: var(--weight-body);
1042
+ font-style: normal;
1043
+ }
1044
+
1045
+ .article-content .theorem-body {
1046
+ font-style: italic;
1047
+ }
1048
+
1049
+ .article-content .theorem-body > :first-child,
1050
+ .article-content .proof-body > :first-child {
1051
+ margin-top: 0;
1052
+ }
1053
+
1054
+ .article-content .theorem-body > :last-child,
1055
+ .article-content .proof-body > :last-child {
1056
+ margin-bottom: 0;
1057
+ }
1058
+
1059
+ .article-content .proof {
1060
+ display: flow-root;
1061
+ box-sizing: border-box;
1062
+ position: relative;
1063
+ width: auto;
1064
+ max-width: none;
1065
+ margin: 1.5em calc(-1 * var(--space-md));
1066
+ padding: 1rem var(--space-md);
1067
+ background: var(--color-surface-container-low);
1068
+ font-size: inherit;
1069
+ line-height: inherit;
1070
+ color: var(--color-on-surface);
1071
+ -webkit-print-color-adjust: exact;
1072
+ print-color-adjust: exact;
1073
+ }
1074
+
1075
+ @media (min-width: 768px) {
1076
+ .article-content .proof {
1077
+ margin-left: calc(-1 * var(--space-2xl));
1078
+ margin-right: calc(-1 * var(--space-2xl));
1079
+ padding-left: var(--space-2xl);
1080
+ padding-right: var(--space-2xl);
1081
+ }
1082
+ }
1083
+
1084
+ .article-content .proof-body > :last-child::after {
1085
+ content: "□";
1086
+ float: right;
1087
+ margin-left: 0.35em;
1088
+ font-size: inherit;
1089
+ line-height: inherit;
1090
+ color: var(--color-on-surface);
1091
+ pointer-events: none;
1092
+ user-select: none;
1093
+ }
1094
+
993
1095
  /* ══ Code blocks ══
994
1096
  A fence used to render as a bare grey box: no indication of what language
995
1097
  was in it, and nothing to do with it but select the text by hand. It now
@@ -818,9 +818,10 @@ aside:has(> .text-detail-sidebar) {
818
818
  font-weight: inherit;
819
819
  }
820
820
 
821
- /* ── Theorem / proof / key-takeaways callout ──
822
- Authored per article as `<div class="article-note" markdown="1">`; styled
823
- here, never injected. Ochre wash + tertiary spine = scannable field note. */
821
+ /* ── Field-note callout ──
822
+ Authored as `<div class="article-note" markdown="1">`; styled here, never
823
+ injected. Ochre wash + tertiary spine = scannable field note. Formal
824
+ theorem / proof blocks use `.theorem` / `.proof` in article.css instead. */
824
825
  .article-content .article-note {
825
826
  position: relative;
826
827
  background: color-mix(in srgb, var(--color-tertiary) 10%, var(--color-panel-contrast));
@@ -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.5"
27
+ # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.8.1"
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.5"
4
+ VERSION = "1.8.1"
5
5
  end
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "proof",
3
+ "markdown": "<div class=\"proof\" markdown=\"1\">\n\nA short claim with **bold**.\n\n</div>",
4
+ "selectors": ["proof", "proof-head", "proof-name", "proof-body"]
5
+ }
@@ -128,6 +128,25 @@ function preprocessMarkdownDivs(markdown) {
128
128
  if (!hasMarkdownAttr(openTag)) return match;
129
129
  return `<div class="poem"><p>${inner.trim()}</p></div>`;
130
130
  });
131
+ text = text.replace(/<div\s+([^>]*class="[^"]*\btheorem\b[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi, (match, openTag, inner) => {
132
+ if (!hasMarkdownAttr(openTag)) return match;
133
+ const nameMatch = openTag.match(/\bname\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
134
+ const descMatch = openTag.match(/\bdescription\s*=\s*(?:"([^"]*)"|'([^']*)')/i);
135
+ const n = nameMatch ? (nameMatch[1] ?? nameMatch[2] ?? '') : '';
136
+ const d = descMatch ? (descMatch[1] ?? descMatch[2] ?? '') : '';
137
+ let head = '';
138
+ if (n) {
139
+ head = `<p class="theorem-head"><span class="theorem-name">${escapeAttr(n)}</span>`;
140
+ if (d) head += ` <span class="theorem-description">(${escapeAttr(d)})</span>`;
141
+ head += '.</p>';
142
+ }
143
+ return `<div class="theorem">${head}<div class="theorem-body">${marked.parse(inner.trim())}</div></div>`;
144
+ });
145
+ text = text.replace(/<div\s+([^>]*class="[^"]*\bproof\b[^"]*"[^>]*)>([\s\S]*?)<\/div>/gi, (match, openTag, inner) => {
146
+ if (!hasMarkdownAttr(openTag)) return match;
147
+ const head = '<p class="proof-head"><span class="proof-name">Proof</span>.</p>';
148
+ return `<div class="proof">${head}<div class="proof-body">${marked.parse(inner.trim())}</div></div>`;
149
+ });
131
150
  return text;
132
151
  }
133
152
 
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "theorem",
3
+ "markdown": "<div class=\"theorem\" name=\"Corollary 1\" description=\"nadir camera\" markdown=\"1\">\n\nLet the camera look straight down.\n\n</div>",
4
+ "selectors": ["theorem", "theorem-head", "theorem-name", "theorem-description", "theorem-body"]
5
+ }
@@ -160,6 +160,28 @@ if article_sheet.match?(/mjx-container[^{]*\{[^}]*overflow\s*:\s*visible/m)
160
160
  errors << "assets/css/article.css: display math sets overflow: visible"
161
161
  end
162
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
+
163
185
  require_declarations(
164
186
  errors, "assets/css/article.css", article_sheet,
165
187
  ".article-content figure", ["max-width: 100%"]
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.5
4
+ version: 1.8.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -393,12 +393,14 @@ files:
393
393
  - scripts/test/render-parity/ongoing-product.json
394
394
  - scripts/test/render-parity/plotly.json
395
395
  - scripts/test/render-parity/poem.json
396
+ - scripts/test/render-parity/proof.json
396
397
  - scripts/test/render-parity/run-absent-field-checks.mjs
397
398
  - scripts/test/render-parity/run-block-fixture.mjs
398
399
  - scripts/test/render-parity/run-detail-parity-checks.mjs
399
400
  - scripts/test/render-parity/run-field-flow-checks.mjs
400
401
  - scripts/test/render-parity/run-fixture.mjs
401
402
  - scripts/test/render-parity/selectors.mjs
403
+ - scripts/test/render-parity/theorem.json
402
404
  - scripts/test/render-parity/video.json
403
405
  - scripts/test/render-parity/youtube-short.json
404
406
  - scripts/test/render-parity/youtube.json