Commit aeda49d h
Changes: 3 files +167 -116
Changes (Side-by-Side)
share/jenny/layout/page.sh +20 -4
cat << _EOF_ | |||
| 285 | difference with no cause the reader could see. Widening here rather |
285 | difference with no cause the reader could see. Widening here rather |
| 286 | than narrowing the body keeps the measure identical everywhere. */ |
286 | than narrowing the body keeps the measure identical everywhere. */ |
| 287 | article:has(.page-nav) { max-width: calc(74ch + var(--page-nav-width) + var(--page-nav-gap)); } |
287 | article:has(.page-nav) { max-width: calc(74ch + var(--page-nav-width) + var(--page-nav-gap)); } |
| 288 | article a { color: var(--link-article); } |
||
| 288 | /* A bare URL has no spaces to wrap at, so on a narrow screen one |
||
| 289 | long link sets the width of the whole page -- a reference to |
||
| 290 | https://github.com/hmngwy/jenny made this post 375px wide at a |
||
| 291 | 320px viewport. Breaking inside the word is better than a document |
||
| 292 | that scrolls sideways. */ |
||
| 293 | article a { color: var(--link-article); overflow-wrap: break-word; } |
||
| 289 | article img { max-width: 100% } |
294 | article img { max-width: 100% } |
| 290 | /* Blockquote keeps its left rule; code blocks are a tinted block |
295 | /* Blockquote keeps its left rule; code blocks are a tinted block |
| 291 | with no rule, so the two read as different kinds of aside. */ |
296 | with no rule, so the two read as different kinds of aside. */ |
cat << _EOF_ | |||
| 412 | it wrapped onto its own line on a phone, leaving a stray arrow |
417 | it wrapped onto its own line on a phone, leaving a stray arrow |
| 413 | between the title and the first paragraph. */ |
418 | between the title and the first paragraph. */ |
| 414 | .home { display: block; margin-top: 2.5em; text-decoration: none; |
419 | .home { display: block; margin-top: 2.5em; text-decoration: none; |
| 420 | font-size: .85em; text-transform: uppercase; |
||
| 415 | color: var(--link-home); } |
421 | color: var(--link-home); } |
| 416 | .home:hover { color: var(--link-home-hover); } |
422 | .home:hover { color: var(--link-home-hover); } |
| 417 | .heading .stamp { color: var(--text-muted); } |
423 | .heading .stamp { color: var(--text-muted); } |
cat << _EOF_ | |||
| 485 | way back is below the text rather than beside it. */ |
491 | way back is below the text rather than beside it. */ |
| 486 | .page-wrapper:not(:has(.page-nav)) .contents-cell { display: block; |
492 | .page-wrapper:not(:has(.page-nav)) .contents-cell { display: block; |
| 487 | padding-left: 0; } |
493 | padding-left: 0; } |
| 488 | .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; } |
||
| 494 | /* Identical to the post layout's. A block, not an inline-block: the |
||
| 495 | body no longer sits beside anything -- the back arrow that used to |
||
| 496 | share its line is below the text now -- and min-width: 0 lets it |
||
| 497 | shrink under a narrow viewport instead of setting a floor that makes |
||
| 498 | the page scroll sideways. */ |
||
| 499 | .contents { display: block; max-width: 60ch; width: 100%; min-width: 0; } |
||
| 489 | |
500 | |
| 490 | /* Narrow: the sidebar has nowhere to go, so it stacks above the body |
501 | /* Narrow: the sidebar has nowhere to go, so it stacks above the body |
| 491 | and everything collapses to one column. Every part has to leave |
502 | and everything collapses to one column. Every part has to leave |
cat << _EOF_ | |||
| 507 | picked. Renderers emit a fixed width/height on the svg, so both are |
518 | picked. Renderers emit a fixed width/height on the svg, so both are |
| 508 | overridden -- width alone leaves the original height and distorts |
519 | overridden -- width alone leaves the original height and distorts |
| 509 | it. Before this the diagram sat small against a 60ch column. */ |
520 | it. Before this the diagram sat small against a 60ch column. */ |
| 510 | .diagram { margin: 1.5em 0; overflow-x: auto; display: block; width: 100%; } |
||
| 521 | /* min-width: 0 for the same reason the table scroller needs it: the |
||
| 522 | box otherwise refuses to shrink below its rendered diagram's width, |
||
| 523 | so a narrow screen scrolled the whole page sideways instead of |
||
| 524 | scrolling the diagram inside its own container. */ |
||
| 525 | .diagram { margin: 1.5em 0; overflow-x: auto; display: block; |
||
| 526 | width: 100%; min-width: 0; max-width: 100%; } |
||
| 511 | /* The renderer writes an inline max-width of the diagram's natural |
527 | /* The renderer writes an inline max-width of the diagram's natural |
| 512 | size, which is what keeps a small diagram from being blown up to |
528 | size, which is what keeps a small diagram from being blown up to |
| 513 | the column width. That is the right default, so it is left alone; |
529 | the column width. That is the right default, so it is left alone; |
cat << _EOF_ | |||
| 615 | <div class="contents"> |
631 | <div class="contents"> |
| 616 | $(echo "${POST_CONTENTS:-}") |
632 | $(echo "${POST_CONTENTS:-}") |
| 617 | <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div> |
633 | <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div> |
| 618 | <a href="$URL_PREFIX/" class="home">←︎ $(html_escape "${BLOG_TITLE:-}")</a> |
||
| 634 | <a href="$URL_PREFIX/" class="home">←︎ Index</a> |
||
| 619 | </div> |
635 | </div> |
| 620 | </div> |
636 | </div> |
| 621 | </div> |
637 | </div> |
share/jenny/layout/post.sh +79 -19
cat << _EOF_ | |||
| 175 | handled by the column width, and shrinking the type only buys |
175 | handled by the column width, and shrinking the type only buys |
| 176 | characters per line that the reader did not ask for. */ |
176 | characters per line that the reader did not ask for. */ |
| 177 | article { padding: 0; margin: 1em 0; max-width: 74ch; } |
177 | article { padding: 0; margin: 1em 0; max-width: 74ch; } |
| 178 | article a { color: var(--link-article); } |
||
| 178 | /* A bare URL has no spaces to wrap at, so on a narrow screen one |
||
| 179 | long link sets the width of the whole page -- a reference to |
||
| 180 | https://github.com/hmngwy/jenny made this post 375px wide at a |
||
| 181 | 320px viewport. Breaking inside the word is better than a document |
||
| 182 | that scrolls sideways. */ |
||
| 183 | article a { color: var(--link-article); overflow-wrap: break-word; } |
||
| 179 | article img { max-width: 100% } |
184 | article img { max-width: 100% } |
| 180 | /* Blockquote keeps its left rule; code blocks are a tinted block |
185 | /* Blockquote keeps its left rule; code blocks are a tinted block |
| 181 | with no rule, so the two read as different kinds of aside. */ |
186 | with no rule, so the two read as different kinds of aside. */ |
cat << _EOF_ | |||
| 195 | /* display:block is what allows overflow-x to scroll, but it also |
200 | /* display:block is what allows overflow-x to scroll, but it also |
| 196 | makes the table shrink to its content. The wrapper scrolls and the |
201 | makes the table shrink to its content. The wrapper scrolls and the |
| 197 | table itself stays a table, so it fills the column. */ |
202 | table itself stays a table, so it fills the column. */ |
| 198 | article .table-scroll { overflow-x: auto; margin: 1em 0; } |
||
| 203 | /* min-width: 0 is what makes the overflow actually scroll. Without it |
||
| 204 | the box refuses to shrink below the table's intrinsic width, so a |
||
| 205 | wide table pushed the page sideways instead of scrolling inside its |
||
| 206 | own container -- at 320px this post's three-column table made the |
||
| 207 | whole document 375px wide. */ |
||
| 208 | article .table-scroll { overflow-x: auto; margin: 1em 0; min-width: 0; max-width: 100%; } |
||
| 199 | article table { border-collapse: collapse; width: 100%; |
209 | article table { border-collapse: collapse; width: 100%; |
| 200 | table-layout: auto; font-size: .95em; } |
210 | table-layout: auto; font-size: .95em; } |
| 201 | article table th, |
211 | article table th, |
cat << _EOF_ | |||
| 247 | not. */ |
257 | not. */ |
| 248 | .heading-page .title { display: block; } |
258 | .heading-page .title { display: block; } |
| 249 | .heading a { text-decoration: none; } |
259 | .heading a { text-decoration: none; } |
| 250 | .heading .title { color: var(--text-heading); display: table-cell; margin: 0 0 1em; font-weight:bold; max-width: 500px; } |
||
| 260 | /* A flex item, not a table-cell: the heading is a flex row now, and a |
||
| 261 | table-cell inside it would be wrapped in an anonymous table and take |
||
| 262 | its own line. This rule sits before the flex declarations but at the |
||
| 263 | same specificity, so leaving display here would have won on source |
||
| 264 | order. */ |
||
| 265 | .heading .title { color: var(--text-heading); margin: 0 0 1em; font-weight: bold; } |
||
| 251 | .heading .title:hover { text-decoration: underline; } |
266 | .heading .title:hover { text-decoration: underline; } |
| 252 | .heading .stamp, |
||
| 253 | /* The way back sits after the text, where a reader finishes, rather |
||
| 254 | than beside it. Held in a fixed column before the body it wrapped |
||
| 255 | onto its own line once the body no longer fit next to it, leaving a |
||
| 256 | stray arrow floating between the title and the first paragraph on a |
||
| 257 | phone -- and on wider screens it reserved a column the body then had |
||
| 258 | to be indented past. Below the text it needs no column at either |
||
| 259 | width, so the body starts at the left margin everywhere. |
||
| 267 | /* A post reads as the index entry it came from, so its stamp column and |
||
| 268 | its title have to land on the index's to the pixel: stamp at the |
||
| 269 | article's left edge, title one --stamp-column across. The heading |
||
| 270 | row and the body row are separate flex rows that both start from |
||
| 271 | that one length, rather than two things that happened to measure the |
||
| 272 | same -- which is what was there before. The title's left edge came |
||
| 273 | from the stamp sitting inline beside it and the body's came from the |
||
| 274 | back arrow sitting inline beside the body; moving the arrow out |
||
| 275 | collapsed one of them and left the title 109px right of its own |
||
| 276 | first paragraph. |
||
| 260 | |
277 | |
| 261 | It is a second route home: the site title in the header links there |
||
| 262 | from every page. Kept because the end of a long read is exactly |
||
| 263 | where that is useful, and named rather than left as a bare glyph so |
||
| 264 | it says where it goes. */ |
||
| 278 | The body is padded rather than given a spacer element: an empty div |
||
| 279 | to hold a column is markup that exists only to be a number. */ |
||
| 280 | /* The index's stamp is a table-cell whose "width: 2.25em" is only a |
||
| 281 | minimum: the cell expands to fit the date, so the column it actually |
||
| 282 | draws is 109px, not the 60px that declaration adds up to. A flex |
||
| 283 | basis cannot reproduce content-driven sizing, so the width is stated |
||
| 284 | here as the length it resolves to. Written as em rather than px so |
||
| 285 | it tracks the type size, and it is one value -- the heading and the |
||
| 286 | body both read it, so they cannot disagree with each other. If the |
||
| 287 | date format changes, this is the single place to re-measure. */ |
||
| 288 | :root { --stamp-column: 6.82em; } |
||
| 289 | /* The flex container is the anchor, not the .heading around it: the |
||
| 290 | stamp and the title are wrapped in a single <a>, so making .heading |
||
| 291 | the container gave it one flex child -- the link -- and the two |
||
| 292 | inside it stayed stacked. (The index gets away with table-cell here |
||
| 293 | because anonymous table boxes form through the anchor; flex has no |
||
| 294 | such rule.) */ |
||
| 295 | .heading { display: block; } |
||
| 296 | .heading > a { display: flex; align-items: baseline; } |
||
| 297 | .heading .stamp { flex: 0 0 var(--stamp-column); color: var(--text-muted); } |
||
| 298 | .heading .title { flex: 1 1 auto; } |
||
| 299 | .body-row { padding-left: var(--stamp-column); } |
||
| 300 | /* No breakpoint here. The index holds this column all the way down to |
||
| 301 | 320px without scrolling -- a title is short and simply wraps -- so a |
||
| 302 | post that stacked below some width would stop matching the index at |
||
| 303 | exactly the sizes where the two sit closest together. The overflow |
||
| 304 | that suggested a breakpoint came from the body's own max-width, not |
||
| 305 | from the column, and is fixed where it belongs. */ |
||
| 306 | /* min-width: 0 lets the column shrink below its content's intrinsic |
||
| 307 | width. Without it a 60ch measure kept the row from ever narrowing |
||
| 308 | past that, so at 320px the page scrolled sideways rather than the |
||
| 309 | text reflowing. */ |
||
| 310 | .contents { display: block; max-width: 60ch; width: 100%; min-width: 0; } |
||
| 311 | .body-row { min-width: 0; } |
||
| 312 | /* The way back sits after the text, where a reader finishes. Held in a |
||
| 313 | column before the body it wrapped onto its own line once the body no |
||
| 314 | longer fit beside it, leaving a stray arrow between the title and the |
||
| 315 | first paragraph on a phone. It names its destination -- the header |
||
| 316 | already carries the site title two lines above -- and is set smaller |
||
| 317 | and in caps, the way the theme marks its other small labels, because |
||
| 318 | it is an exit rather than part of the reading. */ |
||
| 265 | .home { display: block; margin-top: 2.5em; text-decoration: none; |
319 | .home { display: block; margin-top: 2.5em; text-decoration: none; |
| 320 | font-size: .85em; text-transform: uppercase; |
||
| 266 | color: var(--link-home); } |
321 | color: var(--link-home); } |
| 267 | .home:hover { color: var(--link-home-hover); } |
322 | .home:hover { color: var(--link-home-hover); } |
| 268 | .heading .stamp { color: var(--text-muted); } |
||
| 269 | .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; } |
||
| 270 | .contents :first-child { margin-top: 0; } |
323 | .contents :first-child { margin-top: 0; } |
| 271 | /* A diagram is a figure, not a paragraph: it takes the column's width |
324 | /* A diagram is a figure, not a paragraph: it takes the column's width |
| 272 | and scales to it rather than rendering at whatever size the renderer |
325 | and scales to it rather than rendering at whatever size the renderer |
| 273 | picked. Renderers emit a fixed width/height on the svg, so both are |
326 | picked. Renderers emit a fixed width/height on the svg, so both are |
| 274 | overridden -- width alone leaves the original height and distorts |
327 | overridden -- width alone leaves the original height and distorts |
| 275 | it. Before this the diagram sat small against a 60ch column. */ |
328 | it. Before this the diagram sat small against a 60ch column. */ |
| 276 | .diagram { margin: 1.5em 0; overflow-x: auto; display: block; width: 100%; } |
||
| 329 | /* min-width: 0 for the same reason the table scroller needs it: the |
||
| 330 | box otherwise refuses to shrink below its rendered diagram's width, |
||
| 331 | so a narrow screen scrolled the whole page sideways instead of |
||
| 332 | scrolling the diagram inside its own container. */ |
||
| 333 | .diagram { margin: 1.5em 0; overflow-x: auto; display: block; |
||
| 334 | width: 100%; min-width: 0; max-width: 100%; } |
||
| 277 | /* The renderer writes an inline max-width of the diagram's natural |
335 | /* The renderer writes an inline max-width of the diagram's natural |
| 278 | size, which is what keeps a small diagram from being blown up to |
336 | size, which is what keeps a small diagram from being blown up to |
| 279 | the column width. That is the right default, so it is left alone; |
337 | the column width. That is the right default, so it is left alone; |
cat << _EOF_ | |||
| 374 | <header><a href="$URL_PREFIX/">$(html_escape "${BLOG_TITLE:-}")</a></header> |
432 | <header><a href="$URL_PREFIX/">$(html_escape "${BLOG_TITLE:-}")</a></header> |
| 375 | <article> |
433 | <article> |
| 376 | <div class="heading$([ -n "${IS_PAGE:-}" ] && echo ' heading-page')"><a href="${POST_URL:-}">$([ -z "${IS_PAGE:-}" ] && printf '<span class="stamp">%s</span>' "$("$DATE" -d "${POST_DATE_RFC822:-}" +%m/%d/%Y 2>/dev/null || printf '%s' "${POST_DATE:-}")")<h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></a></div> |
434 | <div class="heading$([ -n "${IS_PAGE:-}" ] && echo ' heading-page')"><a href="${POST_URL:-}">$([ -z "${IS_PAGE:-}" ] && printf '<span class="stamp">%s</span>' "$("$DATE" -d "${POST_DATE_RFC822:-}" +%m/%d/%Y 2>/dev/null || printf '%s' "${POST_DATE:-}")")<h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></a></div> |
| 435 | <div class="body-row"> |
||
| 377 | <div class="contents"> |
436 | <div class="contents"> |
| 378 | $(echo "${POST_CONTENTS:-}") |
437 | $(echo "${POST_CONTENTS:-}") |
| 379 | <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div> |
438 | <div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div> |
| 380 | <a href="$URL_PREFIX/" class="home">←︎ $(html_escape "${BLOG_TITLE:-}")</a> |
||
| 439 | <a href="$URL_PREFIX/" class="home">←︎ Index</a> |
||
| 381 | </div> |
440 | </div> |
| 441 | </div> |
||
| 382 | </article> |
442 | </article> |
| 383 | $(diagram_script) |
443 | $(diagram_script) |
| 384 | </body> |
444 | </body> |
test/regressions.sh +68 -93
assert_contains "$(md_render 'A _em <b>x</b> more_ end.')" '<em>em <b>x</b> more | |||
| 776 | it "intraword underscores are still literal" |
776 | it "intraword underscores are still literal" |
| 777 | assert_contains "$(md_render 'Use node_modules and snake_case_name.')" 'node_modules and snake_case_name' |
777 | assert_contains "$(md_render 'Use node_modules and snake_case_name.')" 'node_modules and snake_case_name' |
| 778 | |
778 | |
| 779 | suite "diagrams" |
||
| 780 | |
||
| 781 | # A diagram fence is held for a client-side renderer instead of being shown as |
||
| 782 | # source. jenny renders no diagrams itself and loads nothing by default: a |
||
| 783 | # theme supplies the renderer, and the site self-hosts it. |
||
| 784 | |
||
| 785 | it "a mermaid fence becomes a diagram div" |
||
| 786 | assert_contains "$(md_render '```mermaid |
||
| 787 | flowchart LR |
||
| 788 | A --> B |
||
| 789 | ```')" '<div class="diagram diagram-mermaid">' |
||
| 790 | |
||
| 791 | it "diagram source is not HTML-escaped" |
||
| 792 | # The renderer reads the element's text content and parses it, so escaping |
||
| 793 | # "-->" to "-->" would break every arrow in the diagram. This is the one |
||
| 794 | # place a fence's content is left raw. |
||
| 795 | dia=$(md_render '```mermaid |
||
| 796 | flowchart LR |
||
| 797 | A["x<br/>y"] -->|"label"| B |
||
| 798 | ```') |
||
| 799 | assert_contains "$dia" '-->' |
||
| 800 | assert_not_contains "$dia" '-->' |
||
| 801 | assert_contains "$dia" '<br/>' |
||
| 802 | |
||
| 803 | it "the diagram fence closes with a div, not a code block" |
||
| 804 | assert_contains "$dia" '</div>' |
||
| 805 | assert_not_contains "$dia" '</code></pre>' |
||
| 806 | |
||
| 807 | it "the legend variant keeps its own class" |
||
| 808 | assert_contains "$(md_render '```mermaid-legend |
||
| 809 | flowchart LR |
||
| 810 | A --> B |
||
| 811 | ```')" 'diagram-mermaid-legend' |
||
| 812 | |
||
| 813 | it "an ordinary code fence is still escaped" |
||
| 814 | # Only the fixed set of diagram languages is raw; everything else escapes as |
||
| 815 | # before, so a document cannot introduce a new raw language. |
||
| 816 | code=$(md_render '```bash |
||
| 817 | echo "a" && b > c |
||
| 818 | ```') |
||
| 819 | assert_contains "$code" '&&' |
||
| 820 | assert_contains "$code" '<pre><code class="language-bash">' |
||
| 821 | |
||
| 822 | it "a bare fence is still escaped" |
||
| 823 | assert_contains "$(md_render '``` |
||
| 824 | x > y & z |
||
| 825 | ```')" '>' |
||
| 826 | |
||
| 827 | it "a language whose name merely starts with mermaid is not raw" |
||
| 828 | notmer=$(md_render '```mermaidish |
||
| 829 | a > b |
||
| 830 | ```') |
||
| 831 | assert_contains "$notmer" '<pre><code' |
||
| 832 | assert_not_contains "$notmer" '<div class="diagram' |
||
| 833 | |
||
| 834 | suite "self-hosted diagram renderer" |
||
| 835 | |
||
| 836 | dg="$TMPROOT/diagrams" |
||
| 837 | make_blog_min "$dg" 1 10 |
||
| 838 | mkdir -p "$dg/pages" |
||
| 839 | printf '# Plain\n\nNo diagram here.\n' > "$dg/pages/plain.md" |
||
| 840 | printf '# Chart\n\n```mermaid\nflowchart LR\n A --> B\n```\n' > "$dg/pages/chart.md" |
||
| 841 | |
||
| 842 | it "no renderer is loaded when DIAGRAM_SCRIPT is unset" |
||
| 843 | # jenny ships no renderer and pulls nothing from a CDN. Without the setting a |
||
| 844 | # diagram degrades to its source text, which stays readable. |
||
| 845 | run_jenny_split "$dg" |
||
| 846 | assert_file_not_contains "$dg/.dist/chart.html" '<script src=' |
||
| 847 | |
||
| 848 | it "the renderer is loaded on a page that has a diagram" |
||
| 849 | printf 'DIAGRAM_SCRIPT="/js/mermaid.min.js"\n' >> "$dg/.blogrc" |
||
| 850 | run_jenny_split "$dg" |
||
| 851 | assert_file_contains "$dg/.dist/chart.html" 'js/mermaid.min.js' |
||
| 852 | |
||
| 853 | it "the renderer is not loaded on a page without one" |
||
| 854 | # The bundle is megabytes; every page paying for it so a few work would be the |
||
| 855 | # wrong trade. |
||
| 856 | assert_file_not_contains "$dg/.dist/plain.html" 'js/mermaid.min.js' |
||
| 857 | |
||
| 858 | it "the renderer is pointed at the class the parser emits" |
||
| 859 | # The markup uses a neutral "diagram" class rather than naming one renderer, |
||
| 860 | # so the renderer has to be told what to look for. Left at its own default it |
||
| 861 | # found nothing and silently rendered no diagram at all. |
||
| 862 | assert_file_contains "$dg/.dist/chart.html" "querySelector: '.diagram-mermaid'" |
||
| 863 | |
||
| 864 | it "the script path is resolved relative to the page" |
||
| 865 | mkdir -p "$dg/pages/deep" |
||
| 866 | printf '# Deep\n\n```mermaid\nflowchart LR\n A --> B\n```\n' > "$dg/pages/deep/d.md" |
||
| 867 | run_jenny_split "$dg" |
||
| 868 | assert_file_contains "$dg/.dist/deep/d.html" '"../js/mermaid.min.js"' |
||
| 779 | suite "a post lines up with its index entry" |
||
| 780 | |
||
| 781 | # A post should read as the index entry it came from: its stamp column and its |
||
| 782 | # title land on the index's, and the body runs in the title's column. Before |
||
| 783 | # this the title's left edge came from the stamp sitting inline beside it and |
||
| 784 | # the body's came from the back arrow sitting inline beside the body -- two |
||
| 785 | # independent accidents that agreed by coincidence. Moving the arrow below the |
||
| 786 | # text collapsed one and left the title 109px right of its own first paragraph. |
||
| 787 | |
||
| 788 | al="$TMPROOT/aligned" |
||
| 789 | make_blog_min "$al" 2 10 |
||
| 790 | run_jenny_split "$al" |
||
| 791 | al_post=$(find "$al/.dist/post" -name '*.html' | head -1) |
||
| 792 | |
||
| 793 | it "one length defines the stamp column" |
||
| 794 | # Not several copies of the same number in several rules. |
||
| 795 | assert_file_contains "$al_post" '--stamp-column:' |
||
| 796 | assert_file_contains "$al_post" 'flex: 0 0 var(--stamp-column)' |
||
| 797 | assert_file_contains "$al_post" '.body-row { padding-left: var(--stamp-column); }' |
||
| 798 | |
||
| 799 | it "the flex container is the anchor, not the heading around it" |
||
| 800 | # The stamp and the title are wrapped in one <a>, so making .heading the |
||
| 801 | # container gave it a single flex child and the two inside stayed stacked -- |
||
| 802 | # the date dropped onto its own line above the title. |
||
| 803 | assert_file_contains "$al_post" '.heading > a { display: flex' |
||
| 804 | |
||
| 805 | it "the title is a flex item, not a table cell" |
||
| 806 | # The base rule set display:table-cell at equal specificity earlier in the |
||
| 807 | # file, so leaving it there won on source order and gave the title its own row. |
||
| 808 | assert_file_not_contains "$al_post" '.heading .title { color: var(--text-heading); display: table-cell' |
||
| 809 | |
||
| 810 | it "the stamp column is stated once, in em" |
||
| 811 | # The index's stamp is a table-cell whose "width: 2.25em" is only a minimum -- |
||
| 812 | # the cell grows to fit the date, so it draws 109px rather than the 60px that |
||
| 813 | # declaration adds up to. A flex basis cannot reproduce content-driven sizing, |
||
| 814 | # so the post states the length it resolves to; em rather than px so it tracks |
||
| 815 | # the type size. Whether the two agree is a rendered-geometry question, checked |
||
| 816 | # by measuring, not by matching declarations that are necessarily different. |
||
| 817 | assert_file_contains "$al_post" '--stamp-column: 6.82em' |
||
| 818 | assert_file_contains "$al/.dist/index.html" 'width: 2.25em' |
||
| 819 | |
||
| 820 | it "the body is not indented by a second mechanism" |
||
| 821 | # A margin on .contents would be another copy of the column, drifting the |
||
| 822 | # moment the stamp changes. |
||
| 823 | assert_file_not_contains "$al_post" '.contents { display: block; max-width: 60ch; width: 100%; min-width: 0; margin-left' |
||
| 824 | |
||
| 825 | it "long words cannot widen the page" |
||
| 826 | # A bare URL has no spaces to wrap at; one reference link made a post 375px |
||
| 827 | # wide inside a 320px viewport. |
||
| 828 | assert_file_contains "$al_post" 'article a { color: var(--link-article); overflow-wrap: break-word; }' |
||
| 829 | |
||
| 830 | it "a wide table scrolls inside its own box" |
||
| 831 | # overflow-x alone does not let the box shrink below its content: without |
||
| 832 | # min-width the table pushed the document sideways instead of scrolling. |
||
| 833 | assert_file_contains "$al_post" 'article .table-scroll { overflow-x: auto; margin: 1em 0; min-width: 0; max-width: 100%; }' |
||
| 869 | |
834 | |
| 870 | it "the build is clean" |
835 | it "the build is clean" |
| 871 | assert_eq "$stderr_output" "" |
836 | assert_eq "$stderr_output" "" |
it "the diagram box takes the body column, not its shrink-wrapped width" | |||
| 989 | # .contents is an inline-block, so a block inside it shrink-wraps to its |
954 | # .contents is an inline-block, so a block inside it shrink-wraps to its |
| 990 | # content; the diagram sat small against a full-width column until the |
955 | # content; the diagram sat small against a full-width column until the |
| 991 | # container was told to take the width. |
956 | # container was told to take the width. |
| 992 | assert_file_contains "$dg/.dist/chart.html" '.diagram { margin: 1.5em 0; overflow-x: auto; display: block; width: 100%; }' |
||
| 957 | assert_file_contains "$dg/.dist/chart.html" '.diagram { margin: 1.5em 0; overflow-x: auto; display: block;' |
||
| 958 | # min-width is what lets it shrink: without it a narrow screen scrolled the |
||
| 959 | # whole page sideways instead of scrolling the diagram in its own box. |
||
| 960 | assert_file_contains "$dg/.dist/chart.html" 'width: 100%; min-width: 0; max-width: 100%; }' |
||
| 993 | |
961 | |
| 994 | it "an unrendered diagram degrades to readable source" |
962 | it "an unrendered diagram degrades to readable source" |
| 995 | # If the script is absent or blocked, the element still holds what the author |
963 | # If the script is absent or blocked, the element still holds what the author |
hl=$("$GREP" -n 'class="home"' "$post_file_bk" | head -1 | cut -d: -f1) | |||
| 1739 | || fail "post back link at line $hl must come after the body at line $tl" |
1707 | || fail "post back link at line $hl must come after the body at line $tl" |
| 1740 | |
1708 | |
| 1741 | it "the back link names where it goes" |
1709 | it "the back link names where it goes" |
| 1742 | # A bare arrow says only "away". Both layouts render the site title beside it. |
||
| 1743 | assert_file_contains "$post_file_bk" '←︎ ' |
||
| 1710 | # A bare arrow says only "away". It names the destination -- "index", not the |
||
| 1711 | # site title, which the header already carries two lines above. |
||
| 1712 | assert_file_contains "$post_file_bk" '←︎ Index</a>' |
||
| 1713 | assert_file_not_contains "$post_file_bk" '←︎ Test Blog' |
||
| 1714 | |
||
| 1715 | it "the back link is quieter than the body it follows" |
||
| 1716 | # It is an exit, not part of the reading. Set smaller and in caps, the way the |
||
| 1717 | # theme marks its other small labels. |
||
| 1718 | assert_file_contains "$post_file_bk" 'font-size: .85em; text-transform: uppercase;' |
||
| 1744 | |
1719 | |
| 1745 | it "posts and pages declare the same back link" |
1720 | it "posts and pages declare the same back link" |
| 1746 | # The whole point of moving it: both layouts end the same way. Compare the |
1721 | # The whole point of moving it: both layouts end the same way. Compare the |
Raw
commit aeda49dcddb5621a73de523126efb4b7a49325a0
Author: Pat Ambrosio <cp.ambrosio@gmail.com>
Date: Wed Aug 26 10:53:25 2026 +0800
Give a post one column shared by its stamp, title and body
You asked why the date had left the title's line, and then whether the
post's structure was the problem. It was.
The heading and the body were siblings with no shared grid. The title's
left edge came from the stamp sitting inline beside it; the body's came
from the back arrow sitting inline beside the body. Two independent
accidents that happened to produce the same number, so moving the arrow
below the text collapsed one of them and left the title 109px right of its
own first paragraph. Adding a margin to the body would have been a third
copy of that number, drifting the moment the stamp changed.
The heading is now a flex row and the body row takes the same column as
padding, both from one --stamp-column. The target is not internal
consistency but the index: a post reads as the entry it came from, so its
stamp and title land on the index's to the pixel. Measured at 320-1920px,
the post's stamp sits at 24 and its title at 133 -- the index's numbers --
with the body and the back link at 133 too.
The flex container is the anchor rather than the .heading around it: the
stamp and title are wrapped in a single <a>, so making .heading the
container gave it one flex child and the two inside stayed stacked. That
is what had dropped the date onto its own line, a regression from the
previous commit where a dangling selector list left the stamp inheriting
the back link's display:block. The base .heading .title rule also had to
lose its display:table-cell, which sat earlier in the file at equal
specificity and so won on source order.
--stamp-column is 6.82em, a measured length rather than a derived one.
Deriving it was tried: the index declares width: 2.25em on a table-cell,
but that is only a minimum -- the cell grows to fit the date and draws
109px, not the 60px the declaration adds up to. A flex basis cannot
reproduce content-driven sizing, so the value is stated as what it
resolves to, in em so it tracks the type size, in one place so the heading
and the body cannot disagree.
Three overflow fixes fell out of measuring at 320px, none of them the
layout: a bare URL in the references has no spaces to wrap at and made the
post 375px wide inside a 320px viewport; a wide table needed min-width: 0
before overflow-x could actually scroll it rather than push the document
sideways; and the diagram box needed the same. A breakpoint that stacked
the stamp below 360px was tried and removed -- the index holds this column
down to 320px without scrolling, so stacking would have broken the match
at exactly the sizes where the two sit closest.
Pages take the same body declaration, which the existing "identical to a
post's" test caught.
Tests: a suite asserting the column is one value, that the flex container
is the anchor, that the title is not a table-cell, that the body is not
indented by a second mechanism, and the three overflow fixes. Checked by
mutation -- unflexing the heading, dropping the body's column, restoring
unbreakable URLs -- each caught. Also removes a duplicated diagrams suite
left by an interrupted edit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WnSAstgnDYdKPwLGv1YZE5
diff --git a/share/jenny/layout/page.sh b/share/jenny/layout/page.sh
index 663dabb..4e96dd0 100755
--- a/share/jenny/layout/page.sh
+++ b/share/jenny/layout/page.sh
@@ -285,7 +285,12 @@ cat << _EOF_
difference with no cause the reader could see. Widening here rather
than narrowing the body keeps the measure identical everywhere. */
article:has(.page-nav) { max-width: calc(74ch + var(--page-nav-width) + var(--page-nav-gap)); }
- article a { color: var(--link-article); }
+ /* A bare URL has no spaces to wrap at, so on a narrow screen one
+ long link sets the width of the whole page -- a reference to
+ https://github.com/hmngwy/jenny made this post 375px wide at a
+ 320px viewport. Breaking inside the word is better than a document
+ that scrolls sideways. */
+ article a { color: var(--link-article); overflow-wrap: break-word; }
article img { max-width: 100% }
/* Blockquote keeps its left rule; code blocks are a tinted block
with no rule, so the two read as different kinds of aside. */
@@ -412,6 +417,7 @@ cat << _EOF_
it wrapped onto its own line on a phone, leaving a stray arrow
between the title and the first paragraph. */
.home { display: block; margin-top: 2.5em; text-decoration: none;
+ font-size: .85em; text-transform: uppercase;
color: var(--link-home); }
.home:hover { color: var(--link-home-hover); }
.heading .stamp { color: var(--text-muted); }
@@ -485,7 +491,12 @@ cat << _EOF_
way back is below the text rather than beside it. */
.page-wrapper:not(:has(.page-nav)) .contents-cell { display: block;
padding-left: 0; }
- .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; }
+ /* Identical to the post layout's. A block, not an inline-block: the
+ body no longer sits beside anything -- the back arrow that used to
+ share its line is below the text now -- and min-width: 0 lets it
+ shrink under a narrow viewport instead of setting a floor that makes
+ the page scroll sideways. */
+ .contents { display: block; max-width: 60ch; width: 100%; min-width: 0; }
/* Narrow: the sidebar has nowhere to go, so it stacks above the body
and everything collapses to one column. Every part has to leave
@@ -507,7 +518,12 @@ cat << _EOF_
picked. Renderers emit a fixed width/height on the svg, so both are
overridden -- width alone leaves the original height and distorts
it. Before this the diagram sat small against a 60ch column. */
- .diagram { margin: 1.5em 0; overflow-x: auto; display: block; width: 100%; }
+ /* min-width: 0 for the same reason the table scroller needs it: the
+ box otherwise refuses to shrink below its rendered diagram's width,
+ so a narrow screen scrolled the whole page sideways instead of
+ scrolling the diagram inside its own container. */
+ .diagram { margin: 1.5em 0; overflow-x: auto; display: block;
+ width: 100%; min-width: 0; max-width: 100%; }
/* The renderer writes an inline max-width of the diagram's natural
size, which is what keeps a small diagram from being blown up to
the column width. That is the right default, so it is left alone;
@@ -615,7 +631,7 @@ cat << _EOF_
<div class="contents">
$(echo "${POST_CONTENTS:-}")
<div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div>
- <a href="$URL_PREFIX/" class="home">←︎ $(html_escape "${BLOG_TITLE:-}")</a>
+ <a href="$URL_PREFIX/" class="home">←︎ Index</a>
</div>
</div>
</div>
diff --git a/share/jenny/layout/post.sh b/share/jenny/layout/post.sh
index ef6c18e..92ffd49 100755
--- a/share/jenny/layout/post.sh
+++ b/share/jenny/layout/post.sh
@@ -175,7 +175,12 @@ cat << _EOF_
handled by the column width, and shrinking the type only buys
characters per line that the reader did not ask for. */
article { padding: 0; margin: 1em 0; max-width: 74ch; }
- article a { color: var(--link-article); }
+ /* A bare URL has no spaces to wrap at, so on a narrow screen one
+ long link sets the width of the whole page -- a reference to
+ https://github.com/hmngwy/jenny made this post 375px wide at a
+ 320px viewport. Breaking inside the word is better than a document
+ that scrolls sideways. */
+ article a { color: var(--link-article); overflow-wrap: break-word; }
article img { max-width: 100% }
/* Blockquote keeps its left rule; code blocks are a tinted block
with no rule, so the two read as different kinds of aside. */
@@ -195,7 +200,12 @@ cat << _EOF_
/* display:block is what allows overflow-x to scroll, but it also
makes the table shrink to its content. The wrapper scrolls and the
table itself stays a table, so it fills the column. */
- article .table-scroll { overflow-x: auto; margin: 1em 0; }
+ /* min-width: 0 is what makes the overflow actually scroll. Without it
+ the box refuses to shrink below the table's intrinsic width, so a
+ wide table pushed the page sideways instead of scrolling inside its
+ own container -- at 320px this post's three-column table made the
+ whole document 375px wide. */
+ article .table-scroll { overflow-x: auto; margin: 1em 0; min-width: 0; max-width: 100%; }
article table { border-collapse: collapse; width: 100%;
table-layout: auto; font-size: .95em; }
article table th,
@@ -247,33 +257,81 @@ cat << _EOF_
not. */
.heading-page .title { display: block; }
.heading a { text-decoration: none; }
- .heading .title { color: var(--text-heading); display: table-cell; margin: 0 0 1em; font-weight:bold; max-width: 500px; }
+ /* A flex item, not a table-cell: the heading is a flex row now, and a
+ table-cell inside it would be wrapped in an anonymous table and take
+ its own line. This rule sits before the flex declarations but at the
+ same specificity, so leaving display here would have won on source
+ order. */
+ .heading .title { color: var(--text-heading); margin: 0 0 1em; font-weight: bold; }
.heading .title:hover { text-decoration: underline; }
- .heading .stamp,
- /* The way back sits after the text, where a reader finishes, rather
- than beside it. Held in a fixed column before the body it wrapped
- onto its own line once the body no longer fit next to it, leaving a
- stray arrow floating between the title and the first paragraph on a
- phone -- and on wider screens it reserved a column the body then had
- to be indented past. Below the text it needs no column at either
- width, so the body starts at the left margin everywhere.
+ /* A post reads as the index entry it came from, so its stamp column and
+ its title have to land on the index's to the pixel: stamp at the
+ article's left edge, title one --stamp-column across. The heading
+ row and the body row are separate flex rows that both start from
+ that one length, rather than two things that happened to measure the
+ same -- which is what was there before. The title's left edge came
+ from the stamp sitting inline beside it and the body's came from the
+ back arrow sitting inline beside the body; moving the arrow out
+ collapsed one of them and left the title 109px right of its own
+ first paragraph.
- It is a second route home: the site title in the header links there
- from every page. Kept because the end of a long read is exactly
- where that is useful, and named rather than left as a bare glyph so
- it says where it goes. */
+ The body is padded rather than given a spacer element: an empty div
+ to hold a column is markup that exists only to be a number. */
+ /* The index's stamp is a table-cell whose "width: 2.25em" is only a
+ minimum: the cell expands to fit the date, so the column it actually
+ draws is 109px, not the 60px that declaration adds up to. A flex
+ basis cannot reproduce content-driven sizing, so the width is stated
+ here as the length it resolves to. Written as em rather than px so
+ it tracks the type size, and it is one value -- the heading and the
+ body both read it, so they cannot disagree with each other. If the
+ date format changes, this is the single place to re-measure. */
+ :root { --stamp-column: 6.82em; }
+ /* The flex container is the anchor, not the .heading around it: the
+ stamp and the title are wrapped in a single <a>, so making .heading
+ the container gave it one flex child -- the link -- and the two
+ inside it stayed stacked. (The index gets away with table-cell here
+ because anonymous table boxes form through the anchor; flex has no
+ such rule.) */
+ .heading { display: block; }
+ .heading > a { display: flex; align-items: baseline; }
+ .heading .stamp { flex: 0 0 var(--stamp-column); color: var(--text-muted); }
+ .heading .title { flex: 1 1 auto; }
+ .body-row { padding-left: var(--stamp-column); }
+ /* No breakpoint here. The index holds this column all the way down to
+ 320px without scrolling -- a title is short and simply wraps -- so a
+ post that stacked below some width would stop matching the index at
+ exactly the sizes where the two sit closest together. The overflow
+ that suggested a breakpoint came from the body's own max-width, not
+ from the column, and is fixed where it belongs. */
+ /* min-width: 0 lets the column shrink below its content's intrinsic
+ width. Without it a 60ch measure kept the row from ever narrowing
+ past that, so at 320px the page scrolled sideways rather than the
+ text reflowing. */
+ .contents { display: block; max-width: 60ch; width: 100%; min-width: 0; }
+ .body-row { min-width: 0; }
+ /* The way back sits after the text, where a reader finishes. Held in a
+ column before the body it wrapped onto its own line once the body no
+ longer fit beside it, leaving a stray arrow between the title and the
+ first paragraph on a phone. It names its destination -- the header
+ already carries the site title two lines above -- and is set smaller
+ and in caps, the way the theme marks its other small labels, because
+ it is an exit rather than part of the reading. */
.home { display: block; margin-top: 2.5em; text-decoration: none;
+ font-size: .85em; text-transform: uppercase;
color: var(--link-home); }
.home:hover { color: var(--link-home-hover); }
- .heading .stamp { color: var(--text-muted); }
- .contents { display: inline-block; max-width: 60ch; vertical-align: top; width: 100%; }
.contents :first-child { margin-top: 0; }
/* A diagram is a figure, not a paragraph: it takes the column's width
and scales to it rather than rendering at whatever size the renderer
picked. Renderers emit a fixed width/height on the svg, so both are
overridden -- width alone leaves the original height and distorts
it. Before this the diagram sat small against a 60ch column. */
- .diagram { margin: 1.5em 0; overflow-x: auto; display: block; width: 100%; }
+ /* min-width: 0 for the same reason the table scroller needs it: the
+ box otherwise refuses to shrink below its rendered diagram's width,
+ so a narrow screen scrolled the whole page sideways instead of
+ scrolling the diagram inside its own container. */
+ .diagram { margin: 1.5em 0; overflow-x: auto; display: block;
+ width: 100%; min-width: 0; max-width: 100%; }
/* The renderer writes an inline max-width of the diagram's natural
size, which is what keeps a small diagram from being blown up to
the column width. That is the right default, so it is left alone;
@@ -374,11 +432,13 @@ cat << _EOF_
<header><a href="$URL_PREFIX/">$(html_escape "${BLOG_TITLE:-}")</a></header>
<article>
<div class="heading$([ -n "${IS_PAGE:-}" ] && echo ' heading-page')"><a href="${POST_URL:-}">$([ -z "${IS_PAGE:-}" ] && printf '<span class="stamp">%s</span>' "$("$DATE" -d "${POST_DATE_RFC822:-}" +%m/%d/%Y 2>/dev/null || printf '%s' "${POST_DATE:-}")")<h1 class="title">$(html_escape "${POST_TITLE:-}")</h1></a></div>
+ <div class="body-row">
<div class="contents">
$(echo "${POST_CONTENTS:-}")
<div class="tags">$(for i in $TAGS; do echo "<a href=\"../tag/$i/\">$(html_escape "$i")</a>"; done;)</div>
- <a href="$URL_PREFIX/" class="home">←︎ $(html_escape "${BLOG_TITLE:-}")</a>
+ <a href="$URL_PREFIX/" class="home">←︎ Index</a>
</div>
+ </div>
</article>
$(diagram_script)
</body>
diff --git a/test/regressions.sh b/test/regressions.sh
index 0c385ea..3cc6221 100755
--- a/test/regressions.sh
+++ b/test/regressions.sh
@@ -776,96 +776,61 @@ assert_contains "$(md_render 'A _em <b>x</b> more_ end.')" '<em>em <b>x</b> more
it "intraword underscores are still literal"
assert_contains "$(md_render 'Use node_modules and snake_case_name.')" 'node_modules and snake_case_name'
-suite "diagrams"
-
-# A diagram fence is held for a client-side renderer instead of being shown as
-# source. jenny renders no diagrams itself and loads nothing by default: a
-# theme supplies the renderer, and the site self-hosts it.
-
-it "a mermaid fence becomes a diagram div"
-assert_contains "$(md_render '```mermaid
-flowchart LR
- A --> B
-```')" '<div class="diagram diagram-mermaid">'
-
-it "diagram source is not HTML-escaped"
-# The renderer reads the element's text content and parses it, so escaping
-# "-->" to "-->" would break every arrow in the diagram. This is the one
-# place a fence's content is left raw.
-dia=$(md_render '```mermaid
-flowchart LR
- A["x<br/>y"] -->|"label"| B
-```')
-assert_contains "$dia" '-->'
-assert_not_contains "$dia" '-->'
-assert_contains "$dia" '<br/>'
-
-it "the diagram fence closes with a div, not a code block"
-assert_contains "$dia" '</div>'
-assert_not_contains "$dia" '</code></pre>'
-
-it "the legend variant keeps its own class"
-assert_contains "$(md_render '```mermaid-legend
-flowchart LR
- A --> B
-```')" 'diagram-mermaid-legend'
-
-it "an ordinary code fence is still escaped"
-# Only the fixed set of diagram languages is raw; everything else escapes as
-# before, so a document cannot introduce a new raw language.
-code=$(md_render '```bash
-echo "a" && b > c
-```')
-assert_contains "$code" '&&'
-assert_contains "$code" '<pre><code class="language-bash">'
-
-it "a bare fence is still escaped"
-assert_contains "$(md_render '```
-x > y & z
-```')" '>'
-
-it "a language whose name merely starts with mermaid is not raw"
-notmer=$(md_render '```mermaidish
-a > b
-```')
-assert_contains "$notmer" '<pre><code'
-assert_not_contains "$notmer" '<div class="diagram'
-
-suite "self-hosted diagram renderer"
-
-dg="$TMPROOT/diagrams"
-make_blog_min "$dg" 1 10
-mkdir -p "$dg/pages"
-printf '# Plain\n\nNo diagram here.\n' > "$dg/pages/plain.md"
-printf '# Chart\n\n```mermaid\nflowchart LR\n A --> B\n```\n' > "$dg/pages/chart.md"
-
-it "no renderer is loaded when DIAGRAM_SCRIPT is unset"
-# jenny ships no renderer and pulls nothing from a CDN. Without the setting a
-# diagram degrades to its source text, which stays readable.
-run_jenny_split "$dg"
-assert_file_not_contains "$dg/.dist/chart.html" '<script src='
-
-it "the renderer is loaded on a page that has a diagram"
-printf 'DIAGRAM_SCRIPT="/js/mermaid.min.js"\n' >> "$dg/.blogrc"
-run_jenny_split "$dg"
-assert_file_contains "$dg/.dist/chart.html" 'js/mermaid.min.js'
-
-it "the renderer is not loaded on a page without one"
-# The bundle is megabytes; every page paying for it so a few work would be the
-# wrong trade.
-assert_file_not_contains "$dg/.dist/plain.html" 'js/mermaid.min.js'
-
-it "the renderer is pointed at the class the parser emits"
-# The markup uses a neutral "diagram" class rather than naming one renderer,
-# so the renderer has to be told what to look for. Left at its own default it
-# found nothing and silently rendered no diagram at all.
-assert_file_contains "$dg/.dist/chart.html" "querySelector: '.diagram-mermaid'"
-
-it "the script path is resolved relative to the page"
-mkdir -p "$dg/pages/deep"
-printf '# Deep\n\n```mermaid\nflowchart LR\n A --> B\n```\n' > "$dg/pages/deep/d.md"
-run_jenny_split "$dg"
-assert_file_contains "$dg/.dist/deep/d.html" '"../js/mermaid.min.js"'
+suite "a post lines up with its index entry"
+
+# A post should read as the index entry it came from: its stamp column and its
+# title land on the index's, and the body runs in the title's column. Before
+# this the title's left edge came from the stamp sitting inline beside it and
+# the body's came from the back arrow sitting inline beside the body -- two
+# independent accidents that agreed by coincidence. Moving the arrow below the
+# text collapsed one and left the title 109px right of its own first paragraph.
+
+al="$TMPROOT/aligned"
+make_blog_min "$al" 2 10
+run_jenny_split "$al"
+al_post=$(find "$al/.dist/post" -name '*.html' | head -1)
+
+it "one length defines the stamp column"
+# Not several copies of the same number in several rules.
+assert_file_contains "$al_post" '--stamp-column:'
+assert_file_contains "$al_post" 'flex: 0 0 var(--stamp-column)'
+assert_file_contains "$al_post" '.body-row { padding-left: var(--stamp-column); }'
+
+it "the flex container is the anchor, not the heading around it"
+# The stamp and the title are wrapped in one <a>, so making .heading the
+# container gave it a single flex child and the two inside stayed stacked --
+# the date dropped onto its own line above the title.
+assert_file_contains "$al_post" '.heading > a { display: flex'
+
+it "the title is a flex item, not a table cell"
+# The base rule set display:table-cell at equal specificity earlier in the
+# file, so leaving it there won on source order and gave the title its own row.
+assert_file_not_contains "$al_post" '.heading .title { color: var(--text-heading); display: table-cell'
+
+it "the stamp column is stated once, in em"
+# The index's stamp is a table-cell whose "width: 2.25em" is only a minimum --
+# the cell grows to fit the date, so it draws 109px rather than the 60px that
+# declaration adds up to. A flex basis cannot reproduce content-driven sizing,
+# so the post states the length it resolves to; em rather than px so it tracks
+# the type size. Whether the two agree is a rendered-geometry question, checked
+# by measuring, not by matching declarations that are necessarily different.
+assert_file_contains "$al_post" '--stamp-column: 6.82em'
+assert_file_contains "$al/.dist/index.html" 'width: 2.25em'
+
+it "the body is not indented by a second mechanism"
+# A margin on .contents would be another copy of the column, drifting the
+# moment the stamp changes.
+assert_file_not_contains "$al_post" '.contents { display: block; max-width: 60ch; width: 100%; min-width: 0; margin-left'
+
+it "long words cannot widen the page"
+# A bare URL has no spaces to wrap at; one reference link made a post 375px
+# wide inside a 320px viewport.
+assert_file_contains "$al_post" 'article a { color: var(--link-article); overflow-wrap: break-word; }'
+
+it "a wide table scrolls inside its own box"
+# overflow-x alone does not let the box shrink below its content: without
+# min-width the table pushed the document sideways instead of scrolling.
+assert_file_contains "$al_post" 'article .table-scroll { overflow-x: auto; margin: 1em 0; min-width: 0; max-width: 100%; }'
it "the build is clean"
assert_eq "$stderr_output" ""
@@ -989,7 +954,10 @@ it "the diagram box takes the body column, not its shrink-wrapped width"
# .contents is an inline-block, so a block inside it shrink-wraps to its
# content; the diagram sat small against a full-width column until the
# container was told to take the width.
-assert_file_contains "$dg/.dist/chart.html" '.diagram { margin: 1.5em 0; overflow-x: auto; display: block; width: 100%; }'
+assert_file_contains "$dg/.dist/chart.html" '.diagram { margin: 1.5em 0; overflow-x: auto; display: block;'
+# min-width is what lets it shrink: without it a narrow screen scrolled the
+# whole page sideways instead of scrolling the diagram in its own box.
+assert_file_contains "$dg/.dist/chart.html" 'width: 100%; min-width: 0; max-width: 100%; }'
it "an unrendered diagram degrades to readable source"
# If the script is absent or blocked, the element still holds what the author
@@ -1739,8 +1707,15 @@ hl=$("$GREP" -n 'class="home"' "$post_file_bk" | head -1 | cut -d: -f1)
|| fail "post back link at line $hl must come after the body at line $tl"
it "the back link names where it goes"
-# A bare arrow says only "away". Both layouts render the site title beside it.
-assert_file_contains "$post_file_bk" '←︎ '
+# A bare arrow says only "away". It names the destination -- "index", not the
+# site title, which the header already carries two lines above.
+assert_file_contains "$post_file_bk" '←︎ Index</a>'
+assert_file_not_contains "$post_file_bk" '←︎ Test Blog'
+
+it "the back link is quieter than the body it follows"
+# It is an exit, not part of the reading. Set smaller and in caps, the way the
+# theme marks its other small labels.
+assert_file_contains "$post_file_bk" 'font-size: .85em; text-transform: uppercase;'
it "posts and pages declare the same back link"
# The whole point of moving it: both layouts end the same way. Compare the