Skip to content

Commit c229b2f

Browse files
Render nested checkboxes with indentation (#315)
1 parent 72d4433 commit c229b2f

9 files changed

Lines changed: 86 additions & 16 deletions

File tree

‎crates/procnote-core/src/execution/engine.rs‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ pub struct ExecutionCheckbox {
9494
pub text: String,
9595
pub initial_checked: bool,
9696
pub checked: bool,
97+
pub nesting_level: u32,
9798
pub toggled_at: Option<DateTime<Utc>>,
9899
}
99100

@@ -519,13 +520,19 @@ impl ExecutionState {
519520
.content
520521
.iter()
521522
.map(|item| match item {
522-
StepContent::Checkbox { text, checked, .. } => {
523+
StepContent::Checkbox {
524+
text,
525+
checked,
526+
nesting_level,
527+
..
528+
} => {
523529
let id = format!("{step_id}/cb-{cb_index}");
524530
cb_index += 1;
525531
StepContent::Checkbox {
526532
id: Some(id),
527533
text: text.clone(),
528534
checked: *checked,
535+
nesting_level: *nesting_level,
529536
}
530537
}
531538
// InputDefinition already has its own `id` from the template YAML.
@@ -1059,7 +1066,12 @@ fn execution_content_with_checkbox_ids(
10591066
.iter()
10601067
.map(|item| match item {
10611068
StepContent::Prose { text } => ExecutionStepContent::Prose { text: text.clone() },
1062-
StepContent::Checkbox { id, text, checked } => {
1069+
StepContent::Checkbox {
1070+
id,
1071+
text,
1072+
checked,
1073+
nesting_level,
1074+
} => {
10631075
let id = id.clone().unwrap_or_else(|| {
10641076
loop {
10651077
let candidate = format!("{step_id}/cb-{checkbox_index}");
@@ -1074,6 +1086,7 @@ fn execution_content_with_checkbox_ids(
10741086
text: text.clone(),
10751087
initial_checked: *checked,
10761088
checked: *checked,
1089+
nesting_level: nesting_level.unwrap_or_default(),
10771090
toggled_at: None,
10781091
})
10791092
}
@@ -1275,6 +1288,7 @@ mod tests {
12751288
id: None,
12761289
text: "Ready".to_string(),
12771290
checked: false,
1291+
nesting_level: None,
12781292
}],
12791293
},
12801294
Step {
@@ -1816,6 +1830,7 @@ mod tests {
18161830
id: None,
18171831
text: "Already verified by setup".to_string(),
18181832
checked: true,
1833+
nesting_level: None,
18191834
}];
18201835
let mut state = ExecutionState::new();
18211836
state.start(&template).unwrap();
@@ -1835,6 +1850,7 @@ mod tests {
18351850
id: None,
18361851
text: "Initially checked".to_string(),
18371852
checked: true,
1853+
nesting_level: None,
18381854
}];
18391855
let mut state = ExecutionState::new();
18401856
state.start(&template).unwrap();
@@ -1897,6 +1913,7 @@ mod tests {
18971913
id: None,
18981914
text: "Dynamic check".to_string(),
18991915
checked: false,
1916+
nesting_level: None,
19001917
}],
19011918
Some("step-1"),
19021919
)

‎crates/procnote-core/src/snapshot.rs‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,18 @@ fn render_step(number: usize, step: &StepState, output: &mut String) {
166166
ExecutionStepContent::Prose { text } => render_prose(text, output),
167167
ExecutionStepContent::Checkbox(checkbox) => {
168168
let marker = if checkbox.checked { "x" } else { " " };
169+
let indent = " ".repeat(checkbox.nesting_level as usize);
169170
push_fmt(
170171
output,
171-
format_args!("- [{marker}] {}\n", escape_inline_text(&checkbox.text)),
172+
format_args!(
173+
"{indent}- [{marker}] {}\n",
174+
escape_inline_text(&checkbox.text)
175+
),
172176
);
173177
if let Some(toggled_at) = checkbox.toggled_at {
174178
push_fmt(
175179
output,
176-
format_args!(" Toggled at: {}\n", toggled_at.to_rfc3339()),
180+
format_args!("{indent} Toggled at: {}\n", toggled_at.to_rfc3339()),
177181
);
178182
}
179183
push_fmt(output, format_args!("\n"));
@@ -527,6 +531,7 @@ mod tests {
527531
text: "Probe connected".to_string(),
528532
initial_checked: false,
529533
checked: true,
534+
nesting_level: 0,
530535
toggled_at: Some(timestamp(1)),
531536
}),
532537
ExecutionStepContent::InputBlock {

‎crates/procnote-core/src/template/parser.rs‎

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -303,20 +303,36 @@ fn collect_task_checkboxes(
303303
list_start_index: usize,
304304
list_end_index: usize,
305305
) -> Vec<StepContent> {
306-
events
306+
let mut checkboxes = Vec::new();
307+
let mut list_depth = 1usize;
308+
309+
for (index, (event, _)) in events
307310
.iter()
308311
.enumerate()
309312
.take(list_end_index)
310313
.skip(list_start_index + 1)
311-
.filter_map(|(index, (event, _))| match event {
312-
Event::TaskListMarker(checked) => Some(StepContent::Checkbox {
314+
{
315+
match event {
316+
Event::Start(Tag::List(_)) => list_depth += 1,
317+
Event::End(TagEnd::List(_)) => {
318+
list_depth = list_depth.saturating_sub(1);
319+
}
320+
Event::TaskListMarker(checked) => checkboxes.push(StepContent::Checkbox {
313321
id: None,
314322
text: collect_task_text(events, index),
315323
checked: *checked,
324+
nesting_level: checkbox_nesting_level(list_depth),
316325
}),
317-
_ => None,
318-
})
319-
.collect()
326+
_ => {}
327+
}
328+
}
329+
330+
checkboxes
331+
}
332+
333+
fn checkbox_nesting_level(list_depth: usize) -> Option<u32> {
334+
let level = list_depth.saturating_sub(1);
335+
(level > 0).then(|| u32::try_from(level).unwrap_or(u32::MAX))
320336
}
321337

322338
/// Collect the text of one task item, excluding nested lists from the parent text.
@@ -889,15 +905,22 @@ version: "0.1"
889905
.content
890906
.iter()
891907
.filter_map(|c| match c {
892-
StepContent::Checkbox { text, .. } => Some(text.clone()),
908+
StepContent::Checkbox {
909+
text,
910+
nesting_level,
911+
..
912+
} => Some((text.clone(), nesting_level.unwrap_or_default())),
893913
_ => None,
894914
})
895915
.collect();
896916

897-
// Both should be captured as independent checkboxes with no duplicated prose.
917+
// Both should be captured as independent checkboxes with their nesting.
898918
assert_eq!(
899919
checkboxes,
900-
vec!["parent checkbox".to_string(), "nested checkbox".to_string()]
920+
vec![
921+
("parent checkbox".to_string(), 0),
922+
("nested checkbox".to_string(), 1)
923+
]
901924
);
902925
assert_eq!(
903926
template.steps[0]

‎crates/procnote-core/src/template/types.rs‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ pub enum StepContent {
6060
id: Option<String>,
6161
text: String,
6262
checked: bool,
63+
/// Zero-based nesting level within a pure Markdown task list.
64+
#[serde(default, skip_serializing_if = "Option::is_none")]
65+
#[ts(optional)]
66+
nesting_level: Option<u32>,
6367
},
6468
/// A block of input definitions from a fenced `inputs` code block.
6569
InputBlock { inputs: Vec<InputDefinition> },

‎procedures/example-tvt/template.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ requirement_traces:
2121
Connect PSU to DUT J1 connector. Set voltage to 5.0V. Enable output.
2222

2323
- [ ] a checkbox item
24-
- [ ] a nested checkbox item
24+
- [ ] a nested checkbox item
2525

2626
### Some heading
2727

‎src-tauri/src/commands/execution.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ pub enum StepContentSummary {
7373
id: Option<String>,
7474
text: String,
7575
checked: bool,
76+
/// Zero-based nesting level within a Markdown task list.
77+
nesting_level: u32,
7678
/// ISO 8601 timestamp of the last toggle, if any.
7779
#[ts(optional)]
7880
at: Option<String>,
@@ -177,6 +179,7 @@ pub(super) fn summarize(
177179
id: Some(checkbox.id.clone()),
178180
text: checkbox.text.clone(),
179181
checked: checkbox.checked,
182+
nesting_level: checkbox.nesting_level,
180183
at: datetime_string(checkbox.toggled_at),
181184
},
182185
ExecutionStepContent::InputBlock { inputs } => {

‎src/lib/components/CheckboxItem.svelte‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@
1414
ontoggle: (checkboxId: string, checked: boolean) => Promise<boolean> | boolean;
1515
} = $props();
1616
17+
function nestingIndent(level: number): string {
18+
const safeLevel = Math.min(Math.max(Math.trunc(level), 0), 12);
19+
return `${safeLevel * 20}px`;
20+
}
21+
1722
async function handleChange(event: Event) {
1823
const input = event.currentTarget as HTMLInputElement;
1924
if (!checkbox.id) {
@@ -27,7 +32,12 @@
2732
}
2833
</script>
2934

30-
<label class="checkbox-item" class:checked={checkbox.checked} class:disabled>
35+
<label
36+
class="checkbox-item"
37+
class:checked={checkbox.checked}
38+
class:disabled
39+
style={`--checkbox-indent: ${nestingIndent(checkbox.nesting_level)};`}
40+
>
3141
<input
3242
type="checkbox"
3343
checked={checkbox.checked}
@@ -45,7 +55,7 @@
4555
display: flex;
4656
align-items: flex-start;
4757
gap: 8px;
48-
padding: 6px 0;
58+
padding: 6px 0 6px var(--checkbox-indent, 0px);
4959
cursor: pointer;
5060
font-size: 13px;
5161
}

‎src/lib/types/generated/StepContent.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,9 @@ export type StepContent =
1414
id?: string;
1515
text: string;
1616
checked: boolean;
17+
/**
18+
* Zero-based nesting level within a pure Markdown task list.
19+
*/
20+
nesting_level?: number;
1721
}
1822
| { type: "InputBlock"; inputs: Array<InputDefinition> };

‎src/lib/types/generated/StepContentSummary.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ export type StepContentSummary =
1111
id?: string;
1212
text: string;
1313
checked: boolean;
14+
/**
15+
* Zero-based nesting level within a Markdown task list.
16+
*/
17+
nesting_level: number;
1418
/**
1519
* ISO 8601 timestamp of the last toggle, if any.
1620
*/

0 commit comments

Comments
 (0)