Skip to content

Commit

Permalink
Add support for nested tabs (#17)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielleHuisman authored Oct 23, 2024
1 parent 4a6ae3b commit a48ddc2
Show file tree
Hide file tree
Showing 7 changed files with 184 additions and 50 deletions.
37 changes: 36 additions & 1 deletion book/src/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ Plugin for rendering content in tabs.

## Example

All examples are part of the [book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book).

### Basic

{{#tabs }}
{{#tab name="Tab 1" }}
**Tab content 1**
Expand All @@ -16,6 +20,8 @@ _Tab content 2_
{{#endtab }}
{{#endtabs }}

### Global

{{#tabs global="example" }}
{{#tab name="Global tab 1" }}
**Other tab content 1**
Expand Down Expand Up @@ -52,7 +58,36 @@ const a = 1 + 2;
{{#endtab }}
{{#endtabs }}

- [Book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book)
### Nested Tabs

{{#tabs }}
{{#tab name="Top tab 1" }}
Level 1 - Item 1

{{#tabs }}
{{#tab name="Nested tab 1.1" }}
Level 2 - Item 1.1
{{#endtab }}
{{#tab name="Nested tab 1.2" }}
Level 2 - Item 1.2
{{#endtab }}
{{#endtabs }}

{{#endtab }}
{{#tab name="Top tab 2" }}
Level 1 - Item 2

{{#tabs }}
{{#tab name="Nested tab 2.1" }}
Level 2 - Item 2.1
{{#endtab }}
{{#tab name="Nested tab 2.2" }}
Level 2 - Item 2.2
{{#endtab }}
{{#endtabs }}

{{#endtab }}
{{#endtabs }}

## Installation

Expand Down
5 changes: 2 additions & 3 deletions book/src/trunk.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,14 @@ Plugin which bundles packages using Trunk and includes them as iframes.

## Example

All examples are part of the [book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book).

```toml,trunk
package = "book-example"
features = ["button"]
files = ["src/button.rs"]
```

- [Book source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book)
- [Example source code](https://github.com/RustForWeb/mdbook-plugins/tree/main/book-example)

## Installation

```shell
Expand Down
138 changes: 109 additions & 29 deletions packages/mdbook-plugin-utils/src/markdown/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ pub struct Block<'a> {
pub events: Vec<(Event<'a>, Range<usize>)>,
pub span: Range<usize>,
pub inner_span: Range<usize>,
pub has_nested: bool,
}

impl<'a> Block<'a> {
Expand All @@ -22,6 +23,7 @@ impl<'a> Block<'a> {
events: vec![(first_event, first_span)],
span,
inner_span,
has_nested: false,
}
}
}
Expand All @@ -30,27 +32,42 @@ pub fn parse_blocks<IsStartFn, IsEndFn>(
content: &str,
is_start: IsStartFn,
is_end: IsEndFn,
skip_nested: bool,
) -> Result<Vec<Block>>
where
IsStartFn: Fn(&Event) -> bool,
IsEndFn: Fn(&Event) -> bool,
{
let mut blocks: Vec<Block> = vec![];
let mut nested_level = 0;

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

if is_start(&event) {
if let Some(block) = blocks.last_mut() {
if !block.closed {
bail!("Block is not closed. Nested blocks are not supported.");
if skip_nested {
nested_level += 1;
block.has_nested = true;
block.events.push((event, span));
continue;
} else {
bail!("Block is not closed. Nested blocks are not allowed.");
}
}
}

blocks.push(Block::new(event, span));
} else if is_end(&event) {
if let Some(block) = blocks.last_mut() {
if !block.closed {
if nested_level > 0 {
nested_level -= 1;
block.events.push((event, span));
continue;
}

block.closed = true;
block.span = block.span.start..span.end;
block.events.push((event, span));
Expand Down Expand Up @@ -114,12 +131,14 @@ mod test {
],
span: 0..43,
inner_span: 8..40,
has_nested: false,
}];

let actual = parse_blocks(
content,
|event| matches!(event, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) if tag == &CowStr::from("toml")),
|event| matches!(event, Event::End(TagEnd::CodeBlock)),
false,
)?;

assert_eq!(expected, actual);
Expand Down Expand Up @@ -153,12 +172,14 @@ mod test {
],
span: 34..77,
inner_span: 42..74,
has_nested: false,
}];

let actual = parse_blocks(
content,
|event| matches!(event, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) if tag == &CowStr::from("toml")),
|event| matches!(event, Event::End(TagEnd::CodeBlock)),
false,
)?;

assert_eq!(expected, actual);
Expand Down Expand Up @@ -199,6 +220,7 @@ mod test {
],
span: 18..61,
inner_span: 26..58,
has_nested: false,
},
Block {
closed: true,
Expand All @@ -215,48 +237,22 @@ mod test {
],
span: 126..169,
inner_span: 134..166,
has_nested: false,
},
];

let actual = parse_blocks(
content,
|event| matches!(event, Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(tag))) if tag == &CowStr::from("toml")),
|event| matches!(event, Event::End(TagEnd::CodeBlock)),
false,
)?;

assert_eq!(expected, actual);

Ok(())
}

#[test]
fn test_parse_blocks_nested() -> Result<()> {
let content = "*a **sentence** with **some** words*";

let actual = parse_blocks(
content,
|event| {
matches!(
event,
Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong)
)
},
|event| {
matches!(
event,
Event::End(TagEnd::Emphasis) | Event::End(TagEnd::Strong)
)
},
);

assert_eq!(
"Block is not closed. Nested blocks are not supported.",
format!("{}", actual.unwrap_err().root_cause())
);

Ok(())
}

#[test]
fn test_parse_blocks_text() -> Result<()> {
let content = "\
Expand All @@ -283,6 +279,7 @@ mod test {
],
span: 0..36,
inner_span: 9..24,
has_nested: false,
},
Block {
closed: true,
Expand All @@ -298,13 +295,96 @@ mod test {
],
span: 37..88,
inner_span: 48..74,
has_nested: false,
},
];

let actual = parse_blocks(
content,
|event| matches!(event, Event::Text(text) if text.starts_with("{{#tab ")),
|event| matches!(event, Event::Text(text) if text.starts_with("{{#endtab ")),
false,
)?;

assert_eq!(expected, actual);

Ok(())
}

#[test]
fn test_parse_blocks_nested_error() -> Result<()> {
let content = "*a **sentence** with **some** words*";

let actual = parse_blocks(
content,
|event| {
matches!(
event,
Event::Start(Tag::Emphasis) | Event::Start(Tag::Strong)
)
},
|event| {
matches!(
event,
Event::End(TagEnd::Emphasis) | Event::End(TagEnd::Strong)
)
},
false,
);

assert_eq!(
"Block is not closed. Nested blocks are not allowed.",
format!("{}", actual.unwrap_err().root_cause())
);

Ok(())
}

#[test]
fn test_parse_blocks_nested() -> Result<()> {
let content = "\
{{#tabs }}\n\
Level 1\n\
{{#tabs }}\n\
Level 2\n\
{{#tabs }}\n\
Level 3\n\
{{#endtabs }}\n\
{{#endtabs }}\n\
{{#endtabs }}\n\
";

let expected: Vec<Block> = vec![Block {
closed: true,
events: vec![
(Event::Text(CowStr::from("{{#tabs }}")), 0..10),
(Event::SoftBreak, 10..11),
(Event::Text(CowStr::from("Level 1")), 11..18),
(Event::SoftBreak, 18..19),
(Event::Text(CowStr::from("{{#tabs }}")), 19..29),
(Event::SoftBreak, 29..30),
(Event::Text(CowStr::from("Level 2")), 30..37),
(Event::SoftBreak, 37..38),
(Event::Text(CowStr::from("{{#tabs }}")), 38..48),
(Event::SoftBreak, 48..49),
(Event::Text(CowStr::from("Level 3")), 49..56),
(Event::SoftBreak, 56..57),
(Event::Text(CowStr::from("{{#endtabs }}")), 57..70),
(Event::SoftBreak, 70..71),
(Event::Text(CowStr::from("{{#endtabs }}")), 71..84),
(Event::SoftBreak, 84..85),
(Event::Text(CowStr::from("{{#endtabs }}")), 85..98),
],
span: 0..98,
inner_span: 10..85,
has_nested: true,
}];

let actual = parse_blocks(
content,
|event| matches!(event, Event::Text(text) if text.starts_with("{{#tabs ")),
|event| matches!(event, Event::Text(text) if text.starts_with("{{#endtabs ")),
true,
)?;

assert_eq!(expected, actual);
Expand Down
7 changes: 6 additions & 1 deletion packages/mdbook-plugin-utils/src/markdown/code_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,10 @@ pub fn parse_code_blocks<IsTagsFn>(content: &str, is_tags: IsTagsFn) -> Result<V
where
IsTagsFn: Fn(Vec<String>) -> bool + 'static,
{
parse_blocks(content, is_code_block_start(is_tags), is_code_block_end)
parse_blocks(
content,
is_code_block_start(is_tags),
is_code_block_end,
false,
)
}
15 changes: 9 additions & 6 deletions packages/mdbook-tabs/src/parser/tabs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ fn is_tabs_end(event: &Event) -> bool {

fn is_tab_start(event: &Event) -> bool {
match event {
Event::Text(text) => text.to_string() == "{{#tab}}" || text.starts_with("{{#tab"),
Event::Text(text) => text.to_string() == "{{#tab}}" || text.starts_with("{{#tab "),
_ => false,
}
}
Expand All @@ -42,13 +42,15 @@ fn is_tab_end(event: &Event) -> bool {
}
}

pub fn parse_tabs(chapter: &Chapter) -> Result<Vec<(Range<usize>, TabsConfig)>> {
type SpanAndTabs = (Range<usize>, TabsConfig);

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

let blocks = parse_blocks(&chapter.content, is_tabs_start, is_tabs_end)?;
let blocks = parse_blocks(&chapter.content, is_tabs_start, is_tabs_end, true)?;
debug!("{:?}", blocks);

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

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

configs.push((block.span, tabs));
configs.push((block.span.clone(), tabs));
}

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

Ok(configs)
Ok((configs, blocks.iter().any(|block| block.has_nested)))
}
Loading

0 comments on commit a48ddc2

Please sign in to comment.