|
1 | 1 | def _is_section(section): is_dict(section) && section[:type] == :section; |
2 | 2 |
|
| 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 | + |
3 | 18 | def _h_level_num(md): |
4 | 19 | if (is_h1(md)): 1 |
5 | 20 | elif (is_h2(md)): 2 |
|
13 | 28 | # Returns sections whose title contains the specified pattern. |
14 | 29 | # If depth is true, each section spans until the next header at the same or higher level. |
15 | 30 | 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)): |
18 | 62 | [] |
19 | 63 | else: |
20 | 64 | 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) |
23 | 68 | | var result = [] |
24 | 69 | | 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 |
33 | 76 | | result |
34 | 77 | end |
35 | 78 | 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 |
60 | 79 | end |
61 | 80 |
|
62 | 81 | # Filters sections based on a given predicate function. |
|
71 | 90 |
|
72 | 91 | # 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. |
73 | 92 | 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 |
92 | 112 | end |
93 | 113 |
|
94 | 114 | # Filters the given list of sections, returning only those whose title contains the specified text. |
|
0 commit comments