Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions crates/jp_config/src/style/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ pub enum HrStyle {
Line,
}

impl HrStyle {
/// Returns `true` if this is a line style.
#[must_use]
pub const fn is_line(&self) -> bool {
matches!(self, Self::Line)
}
}

/// Markdown rendering configuration.
#[derive(Debug, Clone, PartialEq, Config)]
#[config(rename_all = "snake_case")]
Expand Down
32 changes: 28 additions & 4 deletions crates/jp_md/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,35 @@ pub enum Event {
FencedCodeLine(String),

/// The end of a fenced code block.
FencedCodeEnd,
FencedCodeEnd(String),

/// Raw content flushed from the buffer at end-of-stream.
/// The content may be a partial block if the stream ended mid-parse.
Flush(String),
}

impl fmt::Display for Event {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FencedCodeStart {
language,
fence_type,
fence_length,
} => {
let fence = match fence_type {
FenceType::Backtick => "`".repeat(*fence_length),
FenceType::Tilde => "~".repeat(*fence_length),
};

write!(f, "{fence}{language}")
}
Self::FencedCodeEnd(s) | Self::Block(s) | Self::FencedCodeLine(s) | Self::Flush(s) => {
write!(f, "{s}")
}
}
}
}

/// Holds the internal buffer and the current parsing state.
#[derive(Debug)]
pub struct Buffer {
Expand Down Expand Up @@ -76,11 +98,11 @@ impl Buffer {

/// Called at the end of the stream to flush any remaining content.
#[must_use]
pub fn flush(&mut self) -> Option<Event> {
pub fn flush(&mut self) -> Option<String> {
if self.buffer.is_empty() {
None
} else {
Some(Event::Flush(std::mem::take(&mut self.buffer)))
Some(std::mem::take(&mut self.buffer))
}
}

Expand Down Expand Up @@ -365,7 +387,9 @@ impl Buffer {
if after_fence.trim().is_empty() {
// Found closing fence. Drain line and switch state.
let _drained = self.buffer.drain(..=line_end);
return (Some(Event::FencedCodeEnd), State::AtBoundary);
let fence = expected_char.to_string().repeat(fence_length);

return (Some(Event::FencedCodeEnd(fence)), State::AtBoundary);
}
}
}
Expand Down
48 changes: 38 additions & 10 deletions crates/jp_md/src/buffer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,7 @@ impl TestCase<'_> {
assert_eq!(actual, expected, "failed case: {name}");
}

let expected_flush = self.flushed.map(|s| Event::Flush(s.to_string()));
assert_eq!(buf.flush(), expected_flush, "failed case: {name}");
assert_eq!(buf.flush().as_deref(), self.flushed, "failed case: {name}");
}
}

Expand Down Expand Up @@ -166,7 +165,7 @@ fn test_buffer_fenced_code_streaming() {
"fn main() {\n".into(),
)]),
("}\n", vec![Event::FencedCodeLine("}\n".into())]),
("```\n", vec![Event::FencedCodeEnd]),
("```\n", vec![Event::FencedCodeEnd("```".into())]),
("After\n\n", vec![Event::Block("After\n\n".into())]),
],
flushed: None,
Expand All @@ -181,7 +180,7 @@ fn test_buffer_fenced_code_streaming() {
(" fn main() {\n", vec![Event::FencedCodeLine(
"fn main() {\n".into(),
)]),
(" ```\n", vec![Event::FencedCodeEnd]),
(" ```\n", vec![Event::FencedCodeEnd("```".into())]),
],
flushed: None,
}),
Expand All @@ -194,7 +193,7 @@ fn test_buffer_fenced_code_streaming() {
}]),
("}\n```\nAfter\n\n", vec![
Event::FencedCodeLine("fn main() {}\n".into()),
Event::FencedCodeEnd,
Event::FencedCodeEnd("```".into()),
Event::Block("After\n\n".into()),
]),
],
Expand All @@ -208,7 +207,7 @@ fn test_buffer_fenced_code_streaming() {
fence_length: 4,
},
Event::FencedCodeLine("code\n".into()),
Event::FencedCodeEnd,
Event::FencedCodeEnd("````".into()),
])],
flushed: None,
}),
Expand All @@ -222,7 +221,7 @@ fn test_buffer_fenced_code_streaming() {
Event::FencedCodeLine("Hello\n".into()),
Event::FencedCodeLine("\n".into()),
Event::FencedCodeLine("World\n".into()),
Event::FencedCodeEnd,
Event::FencedCodeEnd("~~~".into()),
])],
flushed: None,
}),
Expand Down Expand Up @@ -366,7 +365,7 @@ fn test_fmt_write() {
"This is a paragraph.\nIt has two lines.\n\n".into(),
)]);

assert_eq!(buf.flush(), Some(Event::Flush("And a new one.\n".into())));
assert_eq!(buf.flush(), Some("And a new one.\n".into()));
}

#[test]
Expand Down Expand Up @@ -501,7 +500,7 @@ fn test_tabs_in_block_detection() {
buf.push("\t# Not Header\n\n");
let actual: Vec<Event> = buf.by_ref().collect();
assert_eq!(actual, Vec::<Event>::new());
assert_eq!(buf.flush(), Some(Event::Flush("\t# Not Header\n\n".into())));
assert_eq!(buf.flush(), Some("\t# Not Header\n\n".into()));

// 3 spaces before # = valid header
let mut buf = Buffer::new();
Expand All @@ -520,7 +519,7 @@ fn test_tabs_in_block_detection() {
buf.push("\t***\n\n");
let actual: Vec<Event> = buf.by_ref().collect();
assert_eq!(actual, Vec::<Event>::new());
assert_eq!(buf.flush(), Some(Event::Flush("\t***\n\n".into())));
assert_eq!(buf.flush(), Some("\t***\n\n".into()));

// 3 spaces before *** = valid thematic break
let mut buf = Buffer::new();
Expand All @@ -534,3 +533,32 @@ fn test_tabs_in_block_detection() {
let actual: Vec<Event> = buf.by_ref().collect();
assert_eq!(actual, vec![Event::Block("*\t*\t*\t\n".into())]);
}

#[test]
fn test_buffer_event_display() {
let cases = vec![
(Event::Block("Hello".into()), "Hello"),
(
Event::FencedCodeStart {
language: "rust".into(),
fence_type: FenceType::Backtick,
fence_length: 3,
},
"```rust",
),
(
Event::FencedCodeStart {
language: "python".into(),
fence_type: FenceType::Tilde,
fence_length: 4,
},
"~~~~python",
),
(Event::FencedCodeLine("Hello".into()), "Hello"),
(Event::FencedCodeEnd("```".into()), "```"),
];

for (event, expected) in cases {
assert_eq!(event.to_string(), expected);
}
}
18 changes: 18 additions & 0 deletions crates/jp_md/src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,24 @@ impl Formatter {
self
}

/// Set the theme.
#[must_use]
pub fn theme(mut self, theme: Option<&str>) -> Self {
self.theme = theme::resolve(theme);
self
}

/// Set the HR style.
#[must_use]
pub const fn pretty_hr(mut self, pretty: bool) -> Self {
self.hr_style = if pretty {
HrStyle::Line
} else {
HrStyle::Markdown
};
self
}

/// Set the actual terminal width in columns.
///
/// When [`HrStyle::Line`] is active, horizontal rules are rendered as
Expand Down
2 changes: 2 additions & 0 deletions crates/jp_md/src/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ pub struct HrOptions {
/// Format a comrak AST as styled terminal output.
///
/// This is the public entry point, called from [`Formatter::format_terminal`].
///
/// [`Formatter::format_terminal`]: crate::format::Formatter::format_terminal
pub fn format_terminal(
root: Node<'_>,
width: usize,
Expand Down
2 changes: 1 addition & 1 deletion crates/jp_md/tests/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ fn test_buffer_chunks() {
chunks.push(event);
}
if let Some(flushed) = buffer.flush() {
chunks.push(flushed);
chunks.push(jp_md::buffer::Event::Flush(flushed));
}

insta::with_settings!({
Expand Down
4 changes: 2 additions & 2 deletions crates/jp_md/tests/fuzz_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn run_fuzz_test(original: &str, chunks: Vec<&str>) {
}

if let Some(flushed) = buffer.flush() {
output.push(flushed);
output.push(jp_md::buffer::Event::Flush(flushed));
}

// 1. Run full text through buffer -> expected
Expand All @@ -26,7 +26,7 @@ fn run_fuzz_test(original: &str, chunks: Vec<&str>) {
expected.push(event);
}
if let Some(flushed) = expected_buffer.flush() {
expected.push(flushed);
expected.push(jp_md::buffer::Event::Flush(flushed));
}

assert_eq!(output, expected, "Failed with fragmented input");
Expand Down
28 changes: 21 additions & 7 deletions crates/jp_md/tests/snapshots/buffer__document.snap
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,9 @@ description: "Source: document"
FencedCodeLine(
"}\n",
),
FencedCodeEnd,
FencedCodeEnd(
"```",
),
Block(
"Bash Script:\n\n",
),
Expand All @@ -164,7 +166,9 @@ description: "Source: document"
FencedCodeLine(
"jp --version\n",
),
FencedCodeEnd,
FencedCodeEnd(
"```",
),
Block(
"JSON Configuration:\n\n",
),
Expand All @@ -188,7 +192,9 @@ description: "Source: document"
FencedCodeLine(
"}\n",
),
FencedCodeEnd,
FencedCodeEnd(
"```",
),
Block(
"-----\n",
),
Expand Down Expand Up @@ -263,7 +269,9 @@ description: "Source: document"
FencedCodeLine(
"}\n",
),
FencedCodeEnd,
FencedCodeEnd(
"```",
),
Block(
"#### Detailed Feature Matrix\n",
),
Expand Down Expand Up @@ -317,7 +325,9 @@ description: "Source: document"
FencedCodeLine(
"auto_copy = false\n",
),
FencedCodeEnd,
FencedCodeEnd(
"```",
),
Block(
"#### Code Snippet: CLI Definition\n",
),
Expand Down Expand Up @@ -368,7 +378,9 @@ description: "Source: document"
FencedCodeLine(
"}\n",
),
FencedCodeEnd,
FencedCodeEnd(
"```",
),
Block(
"#### Citation Block\n",
),
Expand Down Expand Up @@ -440,7 +452,9 @@ description: "Source: document"
FencedCodeLine(
"}\n",
),
FencedCodeEnd,
FencedCodeEnd(
"```",
),
Block(
"#### Markdown Special Cases\n",
),
Expand Down