diff --git a/CHANGELOG.md b/CHANGELOG.md
index aeba1ac2..30191b88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -115,6 +115,17 @@ ADR-007 and ADR-002.
### Fixed
+- **Ticking a section off crossed out its title and nothing else.** The strike
+ now covers everything under that heading, including its sub-sections, which
+ is what "this section is done" means to somebody who ticked the thing that
+ contained them. It needed the heading RANK, which the HTML importer was
+ discarding: every heading arrived as a sibling, so a parent had no children
+ to cross out. Headings now carry their level, render at their rank, and a
+ guide written before this reads as flat, exactly as it did. Code blocks,
+ tables and images inside a finished section are faded but not struck - a line
+ through a command makes it unreadable, and re-reading the command is usually
+ why somebody looks back at a step they have already done.
+
- **Tech Tools was invisible to anyone without an account**, even though every
tool on the page is deliberately usable without one and Secure Send is the
single write this product offers a guest. The nav item named no permission,
diff --git a/plugins/guides/frontend/components/GuideBlocks.vue b/plugins/guides/frontend/components/GuideBlocks.vue
index b446ca12..c839a4a5 100644
--- a/plugins/guides/frontend/components/GuideBlocks.vue
+++ b/plugins/guides/frontend/components/GuideBlocks.vue
@@ -12,7 +12,8 @@
Reading only: in the editor a tick would be an edit.
-->
+ :class="['gb-h' + levelOf(block),
+ { 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }]">
+ :class="{ 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }">
-
+
-
+
-
+
-
+
-
+
{{ block.language || 'text' }}
{{ block.caption }}
@@ -108,7 +115,8 @@
A failure says so in words. A guide whose screenshot is missing must not
look like a guide that never had one.
-->
-
+
@@ -118,7 +126,8 @@
{{ block.caption }}
-
+
@@ -179,6 +188,42 @@ function toggle (itemid, checked) {
emit('toggle', itemid, checked)
}
+// Which heading governs each block: the nearest one above it. Ticking a
+// heading crosses off everything under it down to the next heading, because
+// that is what "this section is done" means to somebody reading - striking the
+// title alone leaves the wall of text it was introducing looking untouched.
+// Headings nest. A block belongs to every heading above it that has not been
+// closed by a heading of the same or higher rank, so ticking a top-level
+// section crosses off its sub-sections too - which is what "this section is
+// done" means to somebody who ticked the thing that contained them.
+//
+// A guide written before levels existed has none; those all read as level 2,
+// which is the flat behaviour they already had.
+function levelOf (block) {
+ const level = Number(block.level)
+ return Number.isFinite(level) && level >= 2 && level <= 6 ? level : 2
+}
+
+const ancestors = computed(() => {
+ const map = {}
+ const stack = []
+ props.blocks.forEach((block, index) => {
+ if (block.type === 'heading') {
+ const level = levelOf(block)
+ while (stack.length && stack[stack.length - 1].level >= level) stack.pop()
+ map[index] = stack.map(entry => entry.id)
+ stack.push({ id: block.id, level })
+ } else {
+ map[index] = stack.map(entry => entry.id)
+ }
+ })
+ return map
+})
+
+function inDoneSection (index) {
+ return (ancestors.value[index] || []).some(id => isTicked(id))
+}
+
// Steps number themselves by position among steps, so inserting a paragraph
// between two of them does not renumber anything by hand.
const stepIndex = computed(() => {
@@ -215,11 +260,24 @@ function select (block) {
dark mode work with no dark-mode code. */
.gb-blocks { line-height: 1.65; }
.gb-heading { margin: 1.6rem 0 0.6rem; font-size: 1.35rem; color: var(--text); }
+/* Rank is visible, so a reader can see what contains what - and so ticking a
+ section off looks like it covered the right amount of page. */
+.gb-h3 { font-size: 1.15rem; margin-top: 1.2rem; }
+.gb-h4, .gb-h5, .gb-h6 { font-size: 1.02rem; margin-top: 1rem; }
/* A ticked section is struck through and faded, not hidden: somebody scanning
- back up the page has to be able to read what they already did. */
+ back up the page has to be able to read what they already did.
+
+ The strike lands on the heading and on everything under it. A code block is
+ faded but NOT struck - a line through a command makes it unreadable, and the
+ reason to look back at a finished step is usually to re-read the command. */
.gb-sectiondone { text-decoration: line-through; opacity: 0.55; }
.gb-sectiondone .gb-stepnum { text-decoration: none; }
+.gb-code.gb-sectiondone,
+.gb-sectiondone pre,
+.gb-sectiondone code,
+.gb-figure.gb-sectiondone,
+.gb-tablewrap.gb-sectiondone { text-decoration: none; }
.gb-sectiontick {
/* Touch-sized: a guide is read on a shopfloor PC, often with gloves on. */
width: 1.1rem;
diff --git a/plugins/guides/services/htmlimport.py b/plugins/guides/services/htmlimport.py
index ad90d2ed..05373839 100644
--- a/plugins/guides/services/htmlimport.py
+++ b/plugins/guides/services/htmlimport.py
@@ -244,7 +244,12 @@ class _Builder:
if tag in ('h2', 'h3', 'h4', 'h5'):
text = _inline(node)
if text:
- self._add({'type': 'heading', 'text': text})
+ # The LEVEL travels with it. Without it every heading is a
+ # sibling, and a reader who ticks a section off crosses out its
+ # title and nothing underneath - the sub-headings that belong to
+ # it look untouched, because nothing recorded that they belong.
+ self._add({'type': 'heading', 'level': int(tag[1]),
+ 'text': text})
return
if tag == 'p':
diff --git a/tests/test_plugins/test_guides_htmlimport.py b/tests/test_plugins/test_guides_htmlimport.py
index ad3bf636..e40cdbea 100644
--- a/tests/test_plugins/test_guides_htmlimport.py
+++ b/tests/test_plugins/test_guides_htmlimport.py
@@ -212,3 +212,14 @@ def test_an_empty_document_produces_a_named_guide_and_no_blocks():
parsed = parse('')
assert parsed['title'] == 'Untitled'
assert parsed['blocks'] == []
+
+
+def test_a_heading_carries_its_rank():
+ """Ticking a section off has to cross out what belongs to it, and nothing
+ records that a sub-heading belongs to the heading above unless the level
+ travels with them."""
+ parsed = parse('T Credentials Intro.
'
+ 'SFLD share Detail.
Next ')
+ headings = [(b['text'], b.get('level'))
+ for b in parsed['blocks'] if b['type'] == 'heading']
+ assert headings == [('Credentials', 2), ('SFLD share', 3), ('Next', 2)]