Skip to content

Commit 1aec5fc

Browse files
authored
Merge pull request #56 from ptr727/develop
Promote develop to main: the gallery conversion damage, and operations as recurring work
2 parents 78d36e4 + 7b1f8ff commit 1aec5fc

8 files changed

Lines changed: 213 additions & 53 deletions

File tree

TODO.md

Lines changed: 24 additions & 11 deletions
Large diffs are not rendered by default.

assets/css/extended/custom.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,16 @@ body {
3838
margin: 1rem auto;
3939
}
4040

41+
/*
42+
The set caption is a flex item like the figures beside it, so without a full-width basis it
43+
packs onto the end of the last row and reads as a caption for whichever image it lands next
44+
to. Full width puts it on its own row under the set, which is what it describes.
45+
*/
46+
.gallery > figcaption {
47+
flex: 0 0 100%;
48+
text-align: center;
49+
}
50+
4151
.gallery-cols-1 figure {
4252
width: 100%;
4353
}

checks/README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,24 @@ A count is all the check can observe, and two causes reach each direction: it ri
8484

8585
**Both directions read absolute references as well as relative ones.** Hugo writes an absolute URL wherever a template resolves one against the base, which the entry-cover image on every list page does. Reading only rooted paths made those files look linked from nowhere while they were being displayed, and left a broken one unchecked in the other direction. The origin is read from the home page's canonical link rather than assumed, since staging and production build with different base URLs and a hardcoded host would check one environment's output against another's. No canonical link is a hard failure, because a guessed origin inflates the orphan count by exactly the pages that use one.
8686

87+
## The gallery check, which no direction above can reach
88+
89+
Every check above reasons about a URL: whether it renders, whether it resolves, whether anything points at it. Content misplaced **inside** a gallery satisfies all of that. The file exists, the reference resolves, and something links it, so the media surface is green in both directions while the page is laid out wrong. The defect is one of structure, which is why three variants of it survived the conversion and every gate since.
90+
91+
A gallery is a flex row whose column widths come from `.gallery-cols-N figure`. Anything in there that is not a `figure` gets no width from that rule and is rendered as one more item in the row. So the check reads the built pages and fails on any direct child of a gallery container that is not a `figure` or the gallery's own `figcaption`.
92+
93+
The three shapes it found, all of them conversion artifacts, and each verified against the captured live site before being changed:
94+
95+
| Shape in the markdown | What the old platform had |
96+
| --- | --- |
97+
| Caption text appended after the last `figure` shortcode's `}}` | `<figcaption class="blocks-gallery-caption">`, a caption for the **set** |
98+
| A bare `![](…)` image | `<li class="blocks-gallery-item"><figure>` |
99+
| A linked `[![](…)](…)` image | the same, with an `<a>` **inside** the figure |
100+
101+
**The first shape is why the capture is consulted rather than the markup.** A reviewer reading only the source reasonably suggests moving the text into the last figure's `caption` parameter, which is what a per-image caption would need. The capture shows all eleven were gallery-level captions, so that fix would have attributed a caption for a set of four images to whichever one happened to be last, and it would have looked correct.
102+
103+
The gallery shortcode therefore takes a `caption` of its own and renders the container as a `figure`, since `figcaption` is only valid as a figure's child. The theme already styles `figure > figcaption`, so a set caption needs no rule beyond a full-width flex basis to keep it off the end of the last row.
104+
87105
## What the orphans are
88106

89107
The count is not a backlog. It opened at 120 and was adjudicated against the captured live site under `blog-capture/mirror/`, which holds a crawl of the old platform including all 328 URLs the contract requires:

checks/check-url-parity.py

Lines changed: 112 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import pathlib
1111
import re
1212
import sys
13+
from html.parser import HTMLParser
1314
from urllib.parse import unquote
1415

1516
# A truncated list would make every assertion below it pass vacuously while the gate stays green.
@@ -37,6 +38,12 @@
3738
# slack can never accumulate for a later regression to hide in.
3839
ORPHANED_MEDIA = 98
3940

41+
# Every check above returns a list, and the shared summary called all of them "missing". That is
42+
# what a URL that did not build is, and it is not what a stray node inside a gallery is: those are
43+
# present, which is the whole complaint. The default stays "missing" so a check added later reads
44+
# the way the older ones do unless it says otherwise.
45+
FAILURE_NOUN = {"gallery": "stray nodes"}
46+
4047

4148
def load(name):
4249
lines = [ln.strip() for ln in (CHECKS / name).read_text().splitlines()]
@@ -189,6 +196,102 @@ def check_orphans(public, refs):
189196
return orphaned
190197

191198

199+
class GalleryScan(HTMLParser):
200+
"""Collect anything inside a gallery container that is not one of its permitted children.
201+
202+
A gallery is a flex row of figures, so its column widths are set by `.gallery-cols-N figure`.
203+
Anything else landing in there is not laid out by that rule and is rendered as one more item
204+
in the row.
205+
206+
This reads the built HTML, so it sees fewer shapes than the markdown has, and deliberately:
207+
three source patterns reached it, a set caption written as text trailing a figure shortcode,
208+
a bare markdown image, and a linked one, and they arrive here as a bare text node, a `<p>`
209+
wrapping images, and an `<a>` with a `<br>` beside it. Enumerating source patterns would
210+
make this a list to extend every time the conversion surprises us again. Naming the one
211+
invariant instead, that a gallery holds figures and its own caption, covers the shape nobody
212+
has thought of yet, which is how the third of the three was found after the first two.
213+
"""
214+
215+
# A void element never closes, so counting it as an open tag desynchronizes the depth for the
216+
# rest of the document and every later gallery reads as containing whatever follows it.
217+
VOID = {"area", "base", "br", "col", "embed", "hr", "img", "input",
218+
"link", "meta", "param", "source", "track", "wbr"}
219+
ALLOWED = {"figure", "figcaption"}
220+
221+
# There is deliberately no handle_startendtag override. HTMLParser's own implementation
222+
# forwards a self-closing tag to handle_starttag and then handle_endtag, so `<br/>`, `<br />`
223+
# and `<img/>` are already reported and already leave the depth balanced. Adding an override
224+
# to "support" them is what would break it, by counting a pair the base class already splits.
225+
# Verified on those three spellings and on a self-closing non-void `<figure/>`.
226+
227+
def __init__(self):
228+
super().__init__(convert_charrefs=True)
229+
self.findings = []
230+
self.saw_gallery = False
231+
# None outside a gallery; otherwise the number of elements open within the current one,
232+
# so zero means the parser is looking at a direct child.
233+
self.depth = None
234+
235+
def handle_starttag(self, tag, attrs):
236+
classes = dict(attrs).get("class", "").split()
237+
if self.depth is None:
238+
if tag == "figure" and "gallery" in classes:
239+
self.depth = 0
240+
self.saw_gallery = True
241+
return
242+
if self.depth == 0 and tag not in self.ALLOWED:
243+
self.findings.append(f"<{tag}> as a direct child")
244+
if tag not in self.VOID:
245+
self.depth += 1
246+
247+
def handle_endtag(self, tag):
248+
if self.depth is None or tag in self.VOID:
249+
return
250+
if self.depth == 0:
251+
# The gallery's own closing tag.
252+
self.depth = None
253+
else:
254+
self.depth -= 1
255+
256+
def handle_data(self, data):
257+
# Whitespace between elements is just the template's formatting.
258+
if self.depth == 0 and data.strip():
259+
self.findings.append(f"bare text {data.strip()[:60]!r}")
260+
261+
262+
def check_galleries(public):
263+
"""Check that every gallery holds only figures and its own caption.
264+
265+
Neither the assets nor the orphans check can see this: both ask whether a reference resolves
266+
or is reached, and content misplaced inside a gallery resolves and is reached exactly as it
267+
would anywhere else. The defect is purely one of structure, so nothing that reasons about
268+
URLs can observe it, which is why it survived the conversion and every gate since.
269+
"""
270+
findings, pages = [], 0
271+
for path in sorted(public.rglob("index.html")):
272+
html = path.read_text(encoding="utf-8", errors="replace")
273+
# Cheap reject first, since parsing every built page costs far more than one substring
274+
# test and galleries appear on a handful of them. The test is the bare word rather than
275+
# `class="gallery`, because minification drops the quotes around a value that does not
276+
# need them and says nothing about class order, so the quoted form skips a page whose
277+
# markup is merely spelled differently and the gate passes vacuously. This form cannot:
278+
# the parser below requires the class token `gallery`, so a page it would find always
279+
# contains this string. Matching a page that only mentions the word costs one parse.
280+
if "gallery" not in html:
281+
continue
282+
scan = GalleryScan()
283+
scan.feed(html)
284+
# Counted from what the parser actually found rather than from the reject above, so the
285+
# reported number stays "pages carrying a gallery" and not "pages the word appears on".
286+
if not scan.saw_gallery:
287+
continue
288+
pages += 1
289+
rel = str(path.relative_to(public)).replace("\\", "/")
290+
findings += [f"{rel}: {finding}" for finding in scan.findings]
291+
print(f"gallery: {pages} pages with galleries, {len(findings)} stray nodes inside one")
292+
return findings
293+
294+
192295
def main(argv):
193296
if len(argv) != 2:
194297
sys.exit(f"usage: {argv[0]} <public-dir>")
@@ -198,26 +301,27 @@ def main(argv):
198301

199302
refs = collect_refs(public)
200303
failures = []
201-
for label, missing in (
304+
for label, found in (
202305
("render", check_render(public)),
203306
("media", check_media(public)),
204307
("assets", check_assets(public, refs)),
205308
("orphans", check_orphans(public, refs)),
309+
("gallery", check_galleries(public)),
206310
):
207-
if missing:
208-
failures.append((label, missing))
311+
if found:
312+
failures.append((label, found))
209313

210314
if not failures:
211315
print("\nPASS - the built site honors the URL contract")
212316
return 0
213317

214318
print()
215-
for label, missing in failures:
216-
print(f"FAIL {label}: {len(missing)} missing")
217-
for item in missing[:20]:
319+
for label, found in failures:
320+
print(f"FAIL {label}: {len(found)} {FAILURE_NOUN.get(label, 'missing')}")
321+
for item in found[:20]:
218322
print(f" {item}")
219-
if len(missing) > 20:
220-
print(f" ... and {len(missing) - 20} more")
323+
if len(found) > 20:
324+
print(f" ... and {len(found) - 20} more")
221325
return 1
222326

223327

content/posts/2020/02/02/recovering-the-firmware-on-a-supermicro-bpn-sas3-846el1-backplane.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ I removed the motherboard from the old chassis and installed it in the new SC846
3737
{{< figure src="/media/2020/01/img%5F5897.jpg?w=1024" alt="" caption="" >}}
3838
{{< figure src="/media/2020/01/img%5F5898.jpg?w=1024" alt="" caption="" >}}
3939
{{< figure src="/media/2020/01/img%5F5899.jpg?w=768" alt="" caption="" >}}
40-
![](/media/2020/02/img_5973.jpg?w=1024)
41-
![](/media/2020/02/img_5980.jpg?w=768)
40+
{{< figure src="/media/2020/02/img_5973.jpg?w=1024" alt="" caption="" >}}
41+
{{< figure src="/media/2020/02/img_5980.jpg?w=768" alt="" caption="" >}}
4242
{{< /gallery >}}
4343

4444
I powered the machine up through remote IPMI KVM, all looked good, and I booted into my Ubuntu Server USB stick so I could SSH into the box, and update the firmware.

content/posts/2020/06/21/moving-from-unraid-to-proxmox-ve.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -610,12 +610,12 @@ Here are a few screenshot of the end result:
610610

611611

612612
{{< gallery cols="3" >}}
613-
[![](/media/2020/06/2020-06-21-1.png?w=1024)](/media/2020/06/2020-06-21-1.png?w=1024)
614-
[![](/media/2020/06/2020-06-21-2.png?w=1024)](/media/2020/06/2020-06-21-2.png?w=1024)
615-
[![](/media/2020/06/2020-06-21-3.png?w=1024)](/media/2020/06/2020-06-21-3.png?w=1024)
616-
[![](/media/2020/06/2020-06-21-4.png?w=1024)](/media/2020/06/2020-06-21-4.png?w=1024)
617-
[![](/media/2020/06/2020-06-21-9.png?w=1024)](/media/2020/06/2020-06-21-9.png?w=1024)
618-
[![](/media/2020/06/2020-06-21-6.png?w=1024)](/media/2020/06/2020-06-21-6.png?w=1024)
619-
[![](/media/2020/06/2020-06-21-7.png?w=1024)](/media/2020/06/2020-06-21-7.png?w=1024)
620-
[![](/media/2020/06/2020-06-21-8.png?w=1024)](/media/2020/06/2020-06-21-8.png?w=1024)
613+
{{< figure src="/media/2020/06/2020-06-21-1.png?w=1024" alt="" link="/media/2020/06/2020-06-21-1.png?w=1024" caption="" >}}
614+
{{< figure src="/media/2020/06/2020-06-21-2.png?w=1024" alt="" link="/media/2020/06/2020-06-21-2.png?w=1024" caption="" >}}
615+
{{< figure src="/media/2020/06/2020-06-21-3.png?w=1024" alt="" link="/media/2020/06/2020-06-21-3.png?w=1024" caption="" >}}
616+
{{< figure src="/media/2020/06/2020-06-21-4.png?w=1024" alt="" link="/media/2020/06/2020-06-21-4.png?w=1024" caption="" >}}
617+
{{< figure src="/media/2020/06/2020-06-21-9.png?w=1024" alt="" link="/media/2020/06/2020-06-21-9.png?w=1024" caption="" >}}
618+
{{< figure src="/media/2020/06/2020-06-21-6.png?w=1024" alt="" link="/media/2020/06/2020-06-21-6.png?w=1024" caption="" >}}
619+
{{< figure src="/media/2020/06/2020-06-21-7.png?w=1024" alt="" link="/media/2020/06/2020-06-21-7.png?w=1024" caption="" >}}
620+
{{< figure src="/media/2020/06/2020-06-21-8.png?w=1024" alt="" link="/media/2020/06/2020-06-21-8.png?w=1024" caption="" >}}
621621
{{< /gallery >}}

0 commit comments

Comments
 (0)