1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
//! `dbmd sections <file>` — list the `##` sections in a file.
//!
//! Thin wrapper: parse [`SectionsArgs`], read the raw file text, run the
//! whole-file section extractor, and print each `##`+ heading (text:
//! `<indent><heading> (L<line>)`) or a structured array (`--json`). All logic —
//! frontmatter offset, fenced-code-aware heading scan — lives in
//! `dbmd_core::parser::extract_sections_in_file`, which numbers `Section::line`
//! against the source file (1-based) so an agent can jump straight to it; this
//! body only reads the file and formats.
use std::path::Path;
use dbmd_core::parser::{extract_sections_in_file, Section};
use crate::cli::SectionsArgs;
use crate::context::Context;
use crate::error::{CliError, CliResult, ExitCode};
/// Run `dbmd sections`.
pub fn run(ctx: &Context, args: &SectionsArgs) -> CliResult {
let path = Path::new(&args.file);
// Read the raw file text; a missing / unreadable path is a runtime error
// (exit 1), mirroring `dbmd outline`. Sections are then extracted with
// source-relative line numbers (frontmatter offset applied in the parser).
let text = dbmd_core::fsx::read_bounded_nofollow(path, dbmd_core::parser::MAX_DBMD_FILE_BYTES)
.and_then(|bytes| {
String::from_utf8(bytes)
.map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))
})
.map_err(|e| {
CliError::new(ExitCode::Runtime, "IO_ERROR", e.to_string())
.with_hint(format!("could not read sections from `{}`", args.file))
})?;
let sections = extract_sections_in_file(&text);
if ctx.json {
print!("{}", sections_json(§ions));
} else {
print!("{}", sections_text(§ions));
}
Ok(())
}
/// Human form: one heading per line, indented two spaces per level past `##`,
/// with a right-aligned 1-based source line. Empty (no `##`+ headings) prints
/// nothing — a clean, pipe-safe "no sections" signal.
fn sections_text(sections: &[Section]) -> String {
let mut out = String::new();
for s in sections {
// `##` is depth 2 and sits flush-left; each deeper level indents two
// spaces so the outline nesting is visible at a glance.
let indent = " ".repeat(s.level.saturating_sub(2) as usize);
out.push_str(&format!("{indent}{} (L{})\n", s.heading, s.line));
}
out
}
/// Machine form: a JSON array of `{heading, level, line}` — the body slice is
/// omitted (use `dbmd outline` for spans); this command answers "what sections
/// exist". Pretty-printed with a trailing newline for stable snapshots.
fn sections_json(sections: &[Section]) -> String {
let arr: Vec<serde_json::Value> = sections
.iter()
.map(|s| {
serde_json::json!({
"heading": s.heading,
"level": s.level,
"line": s.line,
})
})
.collect();
let mut s = serde_json::to_string_pretty(&serde_json::Value::Array(arr))
.unwrap_or_else(|_| "[]".to_string());
s.push('\n');
s
}