Skip to content

Commit a48ddc2

Browse files
Add support for nested tabs (#17)
1 parent 4a6ae3b commit a48ddc2

File tree

7 files changed

+184
-50
lines changed

7 files changed

+184
-50
lines changed

book/src/tabs.md

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ Plugin for rendering content in tabs.
44

55
## Example
66

7+
All examples are part of the [book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book).
8+
9+
### Basic
10+
711
{{#tabs }}
812
{{#tab name="Tab 1" }}
913
**Tab content 1**
@@ -16,6 +20,8 @@ _Tab content 2_
1620
{{#endtab }}
1721
{{#endtabs }}
1822

23+
### Global
24+
1925
{{#tabs global="example" }}
2026
{{#tab name="Global tab 1" }}
2127
**Other tab content 1**
@@ -52,7 +58,36 @@ const a = 1 + 2;
5258
{{#endtab }}
5359
{{#endtabs }}
5460

55-
- [Book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book)
61+
### Nested Tabs
62+
63+
{{#tabs }}
64+
{{#tab name="Top tab 1" }}
65+
Level 1 - Item 1
66+
67+
{{#tabs }}
68+
{{#tab name="Nested tab 1.1" }}
69+
Level 2 - Item 1.1
70+
{{#endtab }}
71+
{{#tab name="Nested tab 1.2" }}
72+
Level 2 - Item 1.2
73+
{{#endtab }}
74+
{{#endtabs }}
75+
76+
{{#endtab }}
77+
{{#tab name="Top tab 2" }}
78+
Level 1 - Item 2
79+
80+
{{#tabs }}
81+
{{#tab name="Nested tab 2.1" }}
82+
Level 2 - Item 2.1
83+
{{#endtab }}
84+
{{#tab name="Nested tab 2.2" }}
85+
Level 2 - Item 2.2
86+
{{#endtab }}
87+
{{#endtabs }}
88+
89+
{{#endtab }}
90+
{{#endtabs }}
5691

5792
## Installation
5893

book/src/trunk.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@ Plugin which bundles packages using Trunk and includes them as iframes.
44

55
## Example
66

7+
All examples are part of the [book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book).
8+
79
```toml,trunk
810
package = "book-example"
911
features = ["button"]
1012
files = ["src/button.rs"]
1113
```
1214

13-
- [Book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book)
14-
- [Example source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book-example)
15-
1615
## Installation
1716

1817
```shell

packages/mdbook-plugin-utils/src/markdown/block.rs

Lines changed: 109 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ pub struct Block<'a> {
1010
pub events: Vec<(Event<'a>, Range<usize>)>,
1111
pub span: Range<usize>,
1212
pub inner_span: Range<usize>,
13+
pub has_nested: bool,
1314
}
1415

1516
impl<'a> Block<'a> {
@@ -22,6 +23,7 @@ impl<'a> Block<'a> {
2223
events: vec![(first_event, first_span)],
2324
span,
2425
inner_span,
26+
has_nested: false,
2527
}
2628
}
2729
}
@@ -30,27 +32,42 @@ pub fn parse_blocks<IsStartFn, IsEndFn>(
3032
content: &str,
3133
is_start: IsStartFn,
3234
is_end: IsEndFn,
35+
skip_nested: bool,
3336
) -> Result<Vec<Block>>
3437
where
3538
IsStartFn: Fn(&Event) -> bool,
3639
IsEndFn: Fn(&Event) -> bool,
3740
{
3841
let mut blocks: Vec<Block> = vec![];
42+
let mut nested_level = 0;
3943

4044
for (event, span) in Parser::new(content).into_offset_iter() {
4145
debug!("{:?} {:?}", event, span);
4246

4347
if is_start(&event) {
4448
if let Some(block) = blocks.last_mut() {
4549
if !block.closed {
46-
bail!("Block is not closed. Nested blocks are not supported.");
50+
if skip_nested {
51+
nested_level += 1;
52+
block.has_nested = true;
53+
block.events.push((event, span));
54+
continue;
55+
} else {
56+
bail!("Block is not closed. Nested blocks are not allowed.");
57+
}
4758
}
4859
}
4960

5061
blocks.push(Block::new(event, span));
5162
} else if is_end(&event) {
5263
if let Some(block) = blocks.last_mut() {
5364
if !block.closed {
65+
if nested_level > 0 {
66+
nested_level -= 1;
67+
block.events.push((event, span));
68+
continue;
69+
}
70+
5471
block.closed = true;
5572
block.span = block.span.start..span.end;
5673
block.events.push((event, span));
@@ -114,12 +131,14 @@ mod test {
114131
],
115132
span: 0..43,
116133
inner_span: 8..40,
134+
has_nested: false,
117135
}];
118136

119137
let actual = parse_blocks(
120138
content,
121139
|event| matches!(event, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) if tag == &CowStr::from("toml")),
122140
|event| matches!(event, Event::End(TagEnd::CodeBlock)),
141+
false,
123142
)?;
124143

125144
assert_eq!(expected, actual);
@@ -153,12 +172,14 @@ mod test {
153172
],
154173
span: 34..77,
155174
inner_span: 42..74,
175+
has_nested: false,
156176
}];
157177

158178
let actual = parse_blocks(
159179
content,
160180
|event| matches!(event, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) if tag == &CowStr::from("toml")),
161181
|event| matches!(event, Event::End(TagEnd::CodeBlock)),
182+
false,
162183
)?;
163184

164185
assert_eq!(expected, actual);
@@ -199,6 +220,7 @@ mod test {
199220
],
200221
span: 18..61,
201222
inner_span: 26..58,
223+
has_nested: false,
202224
},
203225
Block {
204226
closed: true,
@@ -215,48 +237,22 @@ mod test {
215237
],
216238
span: 126..169,
217239
inner_span: 134..166,
240+
has_nested: false,
218241
},
219242
];
220243

221244
let actual = parse_blocks(
222245
content,
223246
|event| matches!(event, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) if tag == &CowStr::from("toml")),
224247
|event| matches!(event, Event::End(TagEnd::CodeBlock)),
248+
false,
225249
)?;
226250

227251
assert_eq!(expected, actual);
228252

229253
Ok(())
230254
}
231255

232-
#[test]
233-
fn test_parse_blocks_nested() -> Result<()> {
234-
let content = "*a **sentence** with **some** words*";
235-
236-
let actual = parse_blocks(
237-
content,
238-
|event| {
239-
matches!(
240-
event,
241-
Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong)
242-
)
243-
},
244-
|event| {
245-
matches!(
246-
event,
247-
Event::End(TagEnd::Emphasis) | Event::End(TagEnd::Strong)
248-
)
249-
},
250-
);
251-
252-
assert_eq!(
253-
"Block is not closed. Nested blocks are not supported.",
254-
format!("{}", actual.unwrap_err().root_cause())
255-
);
256-
257-
Ok(())
258-
}
259-
260256
#[test]
261257
fn test_parse_blocks_text() -> Result<()> {
262258
let content = "\
@@ -283,6 +279,7 @@ mod test {
283279
],
284280
span: 0..36,
285281
inner_span: 9..24,
282+
has_nested: false,
286283
},
287284
Block {
288285
closed: true,
@@ -298,13 +295,96 @@ mod test {
298295
],
299296
span: 37..88,
300297
inner_span: 48..74,
298+
has_nested: false,
301299
},
302300
];
303301

304302
let actual = parse_blocks(
305303
content,
306304
|event| matches!(event, Event::Text(text) if text.starts_with("{{#tab ")),
307305
|event| matches!(event, Event::Text(text) if text.starts_with("{{#endtab ")),
306+
false,
307+
)?;
308+
309+
assert_eq!(expected, actual);
310+
311+
Ok(())
312+
}
313+
314+
#[test]
315+
fn test_parse_blocks_nested_error() -> Result<()> {
316+
let content = "*a **sentence** with **some** words*";
317+
318+
let actual = parse_blocks(
319+
content,
320+
|event| {
321+
matches!(
322+
event,
323+
Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong)
324+
)
325+
},
326+
|event| {
327+
matches!(
328+
event,
329+
Event::End(TagEnd::Emphasis) | Event::End(TagEnd::Strong)
330+
)
331+
},
332+
false,
333+
);
334+
335+
assert_eq!(
336+
"Block is not closed. Nested blocks are not allowed.",
337+
format!("{}", actual.unwrap_err().root_cause())
338+
);
339+
340+
Ok(())
341+
}
342+
343+
#[test]
344+
fn test_parse_blocks_nested() -> Result<()> {
345+
let content = "\
346+
{{#tabs }}\n\
347+
Level 1\n\
348+
{{#tabs }}\n\
349+
Level 2\n\
350+
{{#tabs }}\n\
351+
Level 3\n\
352+
{{#endtabs }}\n\
353+
{{#endtabs }}\n\
354+
{{#endtabs }}\n\
355+
";
356+
357+
let expected: Vec<Block> = vec![Block {
358+
closed: true,
359+
events: vec![
360+
(Event::Text(CowStr::from("{{#tabs }}")), 0..10),
361+
(Event::SoftBreak, 10..11),
362+
(Event::Text(CowStr::from("Level 1")), 11..18),
363+
(Event::SoftBreak, 18..19),
364+
(Event::Text(CowStr::from("{{#tabs }}")), 19..29),
365+
(Event::SoftBreak, 29..30),
366+
(Event::Text(CowStr::from("Level 2")), 30..37),
367+
(Event::SoftBreak, 37..38),
368+
(Event::Text(CowStr::from("{{#tabs }}")), 38..48),
369+
(Event::SoftBreak, 48..49),
370+
(Event::Text(CowStr::from("Level 3")), 49..56),
371+
(Event::SoftBreak, 56..57),
372+
(Event::Text(CowStr::from("{{#endtabs }}")), 57..70),
373+
(Event::SoftBreak, 70..71),
374+
(Event::Text(CowStr::from("{{#endtabs }}")), 71..84),
375+
(Event::SoftBreak, 84..85),
376+
(Event::Text(CowStr::from("{{#endtabs }}")), 85..98),
377+
],
378+
span: 0..98,
379+
inner_span: 10..85,
380+
has_nested: true,
381+
}];
382+
383+
let actual = parse_blocks(
384+
content,
385+
|event| matches!(event, Event::Text(text) if text.starts_with("{{#tabs ")),
386+
|event| matches!(event, Event::Text(text) if text.starts_with("{{#endtabs ")),
387+
true,
308388
)?;
309389

310390
assert_eq!(expected, actual);

packages/mdbook-plugin-utils/src/markdown/code_block.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,10 @@ pub fn parse_code_blocks<IsTagsFn>(content: &str, is_tags: IsTagsFn) -> Result<V
2828
where
2929
IsTagsFn: Fn(Vec<String>) -> bool + 'static,
3030
{
31-
parse_blocks(content, is_code_block_start(is_tags), is_code_block_end)
31+
parse_blocks(
32+
content,
33+
is_code_block_start(is_tags),
34+
is_code_block_end,
35+
false,
36+
)
3237
}

packages/mdbook-tabs/src/parser/tabs.rs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ fn is_tabs_end(event: &Event) -> bool {
3030

3131
fn is_tab_start(event: &Event) -> bool {
3232
match event {
33-
Event::Text(text) => text.to_string() == "{{#tab}}" || text.starts_with("{{#tab"),
33+
Event::Text(text) => text.to_string() == "{{#tab}}" || text.starts_with("{{#tab "),
3434
_ => false,
3535
}
3636
}
@@ -42,13 +42,15 @@ fn is_tab_end(event: &Event) -> bool {
4242
}
4343
}
4444

45-
pub fn parse_tabs(chapter: &Chapter) -> Result<Vec<(Range<usize>, TabsConfig)>> {
45+
type SpanAndTabs = (Range<usize>, TabsConfig);
46+
47+
pub fn parse_tabs(chapter: &Chapter) -> Result<(Vec<SpanAndTabs>, bool)> {
4648
let mut configs: Vec<(Range<usize>, TabsConfig)> = vec![];
4749

48-
let blocks = parse_blocks(&chapter.content, is_tabs_start, is_tabs_end)?;
50+
let blocks = parse_blocks(&chapter.content, is_tabs_start, is_tabs_end, true)?;
4951
debug!("{:?}", blocks);
5052

51-
for block in blocks {
53+
for block in &blocks {
5254
let start_text = match &block.events[0].0 {
5355
Event::Text(text) => text.to_string(),
5456
_ => bail!("First event should be text."),
@@ -66,6 +68,7 @@ pub fn parse_tabs(chapter: &Chapter) -> Result<Vec<(Range<usize>, TabsConfig)>>
6668
&chapter.content[block.inner_span.clone()],
6769
is_tab_start,
6870
is_tab_end,
71+
true,
6972
)?;
7073
debug!("{:?}", subblocks);
7174

@@ -89,10 +92,10 @@ pub fn parse_tabs(chapter: &Chapter) -> Result<Vec<(Range<usize>, TabsConfig)>>
8992
));
9093
}
9194

92-
configs.push((block.span, tabs));
95+
configs.push((block.span.clone(), tabs));
9396
}
9497

9598
debug!("{:?}", configs);
9699

97-
Ok(configs)
100+
Ok((configs, blocks.iter().any(|block| block.has_nested)))
98101
}

0 commit comments

Comments
 (0)