Skip to content

Commit fe10bb2

Browse files
authored
Merge pull request #2080 from harehare/fix/section-single-node-warning
🐛 fix(mq-lang): warn when section functions receive a single node
2 parents ef9c1e6 + 0a03c3e commit fe10bb2

3 files changed

Lines changed: 97 additions & 55 deletions

File tree

crates/mq-lang/modules/section.mq

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
def _is_section(section): is_dict(section) && section[:type] == :section;
22

3+
# Normalizes input into an array of Markdown nodes.
4+
# section::* functions operate on the full document, so calling them without
5+
# aggregating input first (missing `-A` on the CLI, or `nodes |` inline) means
6+
# a single node is passed instead of an array, which silently produces
7+
# meaningless results. Detect that case, warn on stderr, and treat the single
8+
# node as a one-element array so the result stays sane either way.
9+
def _ensure_aggregated(md_nodes):
10+
if (is_array(md_nodes)):
11+
md_nodes
12+
else:
13+
do
14+
[md_nodes] | stderr("mq: section functions expect all document nodes; got a single node. Pass `-A` on the command line or pipe through `nodes` first.")
15+
end
16+
end
17+
318
def _h_level_num(md):
419
if (is_h1(md)): 1
520
elif (is_h2(md)): 2
@@ -13,50 +28,54 @@ end
1328
# Returns sections whose title contains the specified pattern.
1429
# If depth is true, each section spans until the next header at the same or higher level.
1530
def section(md_nodes, pattern, depth = false):
16-
if (depth):
17-
if (is_empty(md_nodes)):
31+
let agg_nodes = _ensure_aggregated(md_nodes)
32+
| if (depth):
33+
if (is_empty(agg_nodes)):
34+
[]
35+
else:
36+
do
37+
let n = len(agg_nodes)
38+
| let match_indices = do foreach (i, range(n - 1)): if (is_h(agg_nodes[i]) && contains(to_text(agg_nodes[i]), pattern)): i; | compact();
39+
| var result = []
40+
| var i = 0
41+
| while (i < len(match_indices)):
42+
let start = match_indices[i]
43+
| let lvl = _h_level_num(agg_nodes[start])
44+
| let boundaries = do foreach (j, range(n - 1)): if (is_h(agg_nodes[j]) && _h_level_num(agg_nodes[j]) <= lvl): j; | compact();
45+
| let boundaries_with_end = boundaries + n
46+
| let end_node = first(filter(boundaries_with_end, fn(b): b > start;))
47+
| result += [{type: :section, header: agg_nodes[start], children: agg_nodes[start + 1:end_node]}]
48+
| i += 1
49+
| result
50+
end
51+
end
52+
else:
53+
do
54+
sections(agg_nodes) | title_contains(pattern)
55+
end
56+
end
57+
58+
# Splits markdown nodes into sections based on headers.
59+
def sections(md_nodes):
60+
let agg_nodes = _ensure_aggregated(md_nodes)
61+
| if (is_empty(agg_nodes)):
1862
[]
1963
else:
2064
do
21-
let n = len(md_nodes)
22-
| let match_indices = do foreach (i, range(n - 1)): if (is_h(md_nodes[i]) && contains(to_text(md_nodes[i]), pattern)): i; | compact();
65+
let indices = do foreach (i, range(len(agg_nodes) - 1)): if (is_h(agg_nodes[i])): i; | compact();
66+
| let indices_with_end = indices + len(agg_nodes)
67+
| let indices_len = len(indices)
2368
| var result = []
2469
| var i = 0
25-
| while (i < len(match_indices)):
26-
let start = match_indices[i]
27-
| let lvl = _h_level_num(md_nodes[start])
28-
| let boundaries = do foreach (j, range(n - 1)): if (is_h(md_nodes[j]) && _h_level_num(md_nodes[j]) <= lvl): j; | compact();
29-
| let boundaries_with_end = boundaries + n
30-
| let end_node = first(filter(boundaries_with_end, fn(b): b > start;))
31-
| result += [{type: :section, header: md_nodes[start], children: md_nodes[start + 1:end_node]}]
32-
| i += 1
70+
| while (i < indices_len):
71+
let start_node = indices[i]
72+
| let end_node = indices_with_end[i + 1]
73+
| let children = agg_nodes[start_node + 1:end_node]
74+
| result += [{type: :section, header: agg_nodes[start_node], children: children}]
75+
| i = i + 1
3376
| result
3477
end
3578
end
36-
else:
37-
sections(md_nodes) | title_contains(pattern)
38-
end
39-
40-
# Splits markdown nodes into sections based on headers.
41-
def sections(md_nodes):
42-
if (is_empty(md_nodes)):
43-
[]
44-
else:
45-
do
46-
let indices = do foreach (i, range(len(md_nodes) - 1)): if (is_h(md_nodes[i])): i; | compact();
47-
| let indices_with_end = indices + len(md_nodes)
48-
| let indices_len = len(indices)
49-
| var result = []
50-
| var i = 0
51-
| while (i < indices_len):
52-
let start_node = indices[i]
53-
| let end_node = indices_with_end[i + 1]
54-
| let children = md_nodes[start_node + 1:end_node]
55-
| result += [{type: :section, header: md_nodes[start_node], children: children}]
56-
| i = i + 1
57-
| result
58-
end
59-
end
6079
end
6180

6281
# Filters sections based on a given predicate function.
@@ -71,24 +90,25 @@ end
7190

7291
# Returns an array of sections, each section is an array of markdown nodes between the specified header and the next header of the same level.
7392
def split(md_nodes, level):
74-
if (is_empty(md_nodes)):
75-
[]
76-
else:
77-
do
78-
let indices = do foreach (i, range(len(md_nodes) - 1)): if (is_h_level(md_nodes[i], level)): i; | compact();
79-
| let indices_with_end = indices + len(md_nodes)
80-
| let indices_len = len(indices)
81-
| var result = []
82-
| var i = 0
83-
| while (i < len(indices)):
84-
let start_node = indices[i]
85-
| let end_node = indices_with_end[i + 1]
86-
| let children = md_nodes[start_node + 1:end_node]
87-
| result += [{type: :section, header: md_nodes[start_node], children: children}]
88-
| i += 1
89-
| result
90-
end
91-
end
93+
let agg_nodes = _ensure_aggregated(md_nodes)
94+
| if (is_empty(agg_nodes)):
95+
[]
96+
else:
97+
do
98+
let indices = do foreach (i, range(len(agg_nodes) - 1)): if (is_h_level(agg_nodes[i], level)): i; | compact();
99+
| let indices_with_end = indices + len(agg_nodes)
100+
| let indices_len = len(indices)
101+
| var result = []
102+
| var i = 0
103+
| while (i < len(indices)):
104+
let start_node = indices[i]
105+
| let end_node = indices_with_end[i + 1]
106+
| let children = agg_nodes[start_node + 1:end_node]
107+
| result += [{type: :section, header: agg_nodes[start_node], children: children}]
108+
| i += 1
109+
| result
110+
end
111+
end
92112
end
93113

94114
# Filters the given list of sections, returning only those whose title contains the specified text.

crates/mq-lang/modules/section_test.mq

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,25 @@ def test_section_by_level_empty():
137137
let secs = section::by_level([], 1)
138138
| assert_eq(secs, [])
139139
end
140+
141+
# A single node (not wrapped in an array) means the caller forgot to aggregate
142+
# input (missing `-A`/`nodes |`). This should be normalized to a one-element
143+
# array instead of silently producing meaningless results.
144+
def test_section_sections_single_node_normalized():
145+
let single = first(test_sections)
146+
| let secs = section::sections(single)
147+
| assert_eq(len(secs), 1)
148+
| assert_eq(section::title(first(secs)), "Introduction")
149+
end
150+
151+
def test_section_section_single_node_normalized():
152+
let single = first(test_sections)
153+
| let secs = section::section(single, "Introduction")
154+
| assert_eq(len(secs), 1)
155+
end
156+
157+
def test_section_split_single_node_normalized():
158+
let single = first(test_sections)
159+
| let secs = section::split(single, 1)
160+
| assert_eq(len(secs), 1)
161+
end

docs/books/src/start/example.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ The section module provides functions for splitting and filtering Markdown docum
219219
| `include` | `include "section"` then `fn()` | No namespace prefix |
220220
| `-A` flag | `mq -A 'section::fn()'` | Aggregate mode: processes all nodes at once |
221221

222-
> **Note**: Section functions need all document nodes at once. Use `-A` on the command line, or `nodes` in inline queries.
222+
> **Note**: Section functions need all document nodes at once. Use `-A` on the command line, or `nodes` in inline queries. If you forget and call a `section::*` function on a single node, mq prints a warning on stderr (e.g. `mq: section functions expect all document nodes; got a single node. Pass -A on the command line or pipe through nodes first.`) and treats the node as a one-element array instead of silently producing meaningless results.
223223
224224
### Extract Sections by Title
225225

0 commit comments

Comments
 (0)