Ticking a section off crosses out the section
It struck the heading and left everything underneath looking untouched, which is the opposite of what somebody ticking "this section is done" means. The strike now covers every block under that heading, down to the next heading of the same or higher rank - so a top-level section takes its sub-sections with it. That needed the rank, and the HTML importer was throwing it away: h2, h3 and h4 all arrived as a flat 'heading', so a parent heading had no children to cross out. It was the visible half of a real loss - a converted article kept its words and lost its shape. Headings now carry their level and render at it, and a guide written before this reads as flat, exactly as it did. Code blocks, tables and images inside a finished section fade but are not struck. A line through a command makes it unreadable, and re-reading the command is usually the reason to look back at a step already done. The five imported guides were re-imported so they carry their structure.
This commit is contained in:
11
CHANGELOG.md
11
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,
|
||||
|
||||
@@ -88,6 +88,6 @@ Manifest-less directories under `plugins/` are core frontend surface and always
|
||||
|
||||
## Size
|
||||
|
||||
- test functions defined: **1417** (parametrised cases collect higher)
|
||||
- test functions defined: **1418** (parametrised cases collect higher)
|
||||
- documented API paths: **305** (`docs/openapi.json`, regenerate with `scripts/gen_openapi.py`)
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
Reading only: in the editor a tick would be an edit.
|
||||
-->
|
||||
<h2 v-if="block.type === 'heading'" :id="anchorFor(block)" class="gb-heading"
|
||||
:class="{ 'gb-sectiondone': isTicked(block.id) }">
|
||||
:class="['gb-h' + levelOf(block),
|
||||
{ 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }]">
|
||||
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
|
||||
:checked="isTicked(block.id)"
|
||||
:aria-label="'Mark this section done'"
|
||||
@@ -21,10 +22,11 @@
|
||||
</h2>
|
||||
|
||||
<p v-else-if="block.type === 'paragraph'" class="gb-para"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }"
|
||||
v-html="inline(block.text)"></p>
|
||||
|
||||
<div v-else-if="block.type === 'step'" class="gb-step"
|
||||
:class="{ 'gb-sectiondone': isTicked(block.id) }">
|
||||
:class="{ 'gb-sectiondone': isTicked(block.id) || inDoneSection(index) }">
|
||||
<label class="gb-stepnum">
|
||||
<input v-if="interactive" type="checkbox" class="gb-sectiontick"
|
||||
:checked="isTicked(block.id)"
|
||||
@@ -41,7 +43,8 @@
|
||||
editor a tick would be an edit, and an author clicking around their own
|
||||
draft would quietly change what the next reader sees as "done".
|
||||
-->
|
||||
<ul v-else-if="block.type === 'checklist'" class="gb-checklist">
|
||||
<ul v-else-if="block.type === 'checklist'" class="gb-checklist"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||
<li v-for="item in itemsOf(block)" :key="item.id" class="gb-check">
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
@@ -54,21 +57,25 @@
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<ul v-else-if="block.type === 'bullets'" class="gb-list">
|
||||
<ul v-else-if="block.type === 'bullets'" class="gb-list"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
|
||||
</ul>
|
||||
|
||||
<ol v-else-if="block.type === 'numbers'" class="gb-list">
|
||||
<ol v-else-if="block.type === 'numbers'" class="gb-list"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||
<li v-for="item in itemsOf(block)" :key="item.id" v-html="inline(item.text)"></li>
|
||||
</ol>
|
||||
|
||||
<div v-else-if="block.type === 'callout'"
|
||||
class="gb-callout" :class="'gb-' + (block.variant || 'info')">
|
||||
<div v-else-if="block.type === 'callout'" class="gb-callout"
|
||||
:class="['gb-' + (block.variant || 'info'),
|
||||
{ 'gb-sectiondone': inDoneSection(index) }]">
|
||||
<strong v-if="block.title">{{ block.title }}</strong>
|
||||
<div v-html="inline(block.text)"></div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="block.type === 'code'" class="gb-code">
|
||||
<div v-else-if="block.type === 'code'" class="gb-code"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||
<div class="gb-codehead">
|
||||
<span class="gb-lang">{{ block.language || 'text' }}</span>
|
||||
<span v-if="block.caption" class="gb-caption">{{ block.caption }}</span>
|
||||
@@ -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.
|
||||
-->
|
||||
<figure v-else-if="block.type === 'image'" class="gb-figure">
|
||||
<figure v-else-if="block.type === 'image'" class="gb-figure"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||
<img v-if="imageState(block).src" :src="imageState(block).src"
|
||||
:alt="block.alt || ''">
|
||||
<p v-else-if="imageState(block).failed" class="gb-imagefailed">
|
||||
@@ -118,7 +126,8 @@
|
||||
<figcaption v-if="block.caption">{{ block.caption }}</figcaption>
|
||||
</figure>
|
||||
|
||||
<div v-else-if="block.type === 'table'" class="gb-tablewrap">
|
||||
<div v-else-if="block.type === 'table'" class="gb-tablewrap"
|
||||
:class="{ 'gb-sectiondone': inDoneSection(index) }">
|
||||
<table class="gb-table">
|
||||
<thead v-if="block.header && block.header.length">
|
||||
<tr><th v-for="(cell, c) in block.header" :key="c" v-html="inline(cell)"></th></tr>
|
||||
@@ -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;
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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('<h1>T</h1><h2>Credentials</h2><p>Intro.</p>'
|
||||
'<h3>SFLD share</h3><p>Detail.</p><h2>Next</h2>')
|
||||
headings = [(b['text'], b.get('level'))
|
||||
for b in parsed['blocks'] if b['type'] == 'heading']
|
||||
assert headings == [('Credentials', 2), ('SFLD share', 3), ('Next', 2)]
|
||||
|
||||
Reference in New Issue
Block a user