Download crates/forge_markdown_stream/src/lib.rs from SaylorTwift/forgecode: direct link, hf CLI and curl.
- Browser
- Download file 8.29 kB
-
https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_markdown_stream/src/lib.rs
- Command line
-
hf download hf://SaylorTwift/forgecode/crates/forge_markdown_stream/src/lib.rs
-
curl -L -o lib.rs https://huggingface.co/SaylorTwift/forgecode/resolve/main/crates/forge_markdown_stream/src/lib.rs
8.29 kB
| //! Forge Markdown Stream - Streaming markdown renderer for terminal output. | |
| //! | |
| //! This crate provides a streaming markdown renderer optimized for LLM output. | |
| //! It renders markdown with syntax highlighting, styled headings, tables, | |
| //! lists, and more. | |
| //! | |
| //! # Example | |
| //! | |
| //! ```no_run | |
| //! use forge_markdown_stream::StreamdownRenderer; | |
| //! use std::io; | |
| //! | |
| //! fn main() -> io::Result<()> { | |
| //! let mut renderer = StreamdownRenderer::new(io::stdout(), 80); | |
| //! | |
| //! // Push tokens as they arrive from LLM | |
| //! renderer.push("Hello ")?; | |
| //! renderer.push("**world**!\n")?; | |
| //! | |
| //! // Finish rendering | |
| //! let _ = renderer.finish()?; | |
| //! Ok(()) | |
| //! } | |
| //! ``` | |
| mod code; | |
| mod heading; | |
| mod inline; | |
| mod list; | |
| mod renderer; | |
| mod repair; | |
| mod style; | |
| mod table; | |
| mod theme; | |
| mod utils; | |
| use std::io::{self, Write}; | |
| pub use renderer::Renderer; | |
| pub use repair::repair_line; | |
| pub use streamdown_parser::Parser; | |
| pub use theme::{Style, Theme}; | |
| /// Streaming markdown renderer for terminal output. | |
| /// | |
| /// Buffers incoming tokens and renders complete lines with syntax highlighting, | |
| /// styled headings, tables, lists, and more. | |
| /// | |
| /// The renderer is generic over the writer type `W`, which must implement | |
| /// `Write`. | |
| pub struct StreamdownRenderer<W: Write> { | |
| parser: Parser, | |
| renderer: Renderer<W>, | |
| line_buffer: String, | |
| } | |
| impl<W: Write> StreamdownRenderer<W> { | |
| /// Create a new renderer with the given writer and terminal width. | |
| pub fn new(writer: W, width: usize) -> Self { | |
| Self { | |
| parser: Parser::new(), | |
| renderer: Renderer::new(writer, width), | |
| line_buffer: String::new(), | |
| } | |
| } | |
| /// Create a new renderer with a custom theme. | |
| pub fn with_theme(writer: W, width: usize, theme: Theme) -> Self { | |
| Self { | |
| parser: Parser::new(), | |
| renderer: Renderer::with_theme(writer, width, theme), | |
| line_buffer: String::new(), | |
| } | |
| } | |
| /// Push a token to the renderer. | |
| /// | |
| /// Tokens are buffered until a complete line is received, then rendered. | |
| pub fn push(&mut self, token: &str) -> io::Result<()> { | |
| self.line_buffer.push_str(token); | |
| while let Some(pos) = self.line_buffer.find('\n') { | |
| let line = self.line_buffer.get(..pos).unwrap_or("").to_string(); | |
| for repaired in repair_line(&line, self.parser.state()) { | |
| for event in self.parser.parse_line(&repaired) { | |
| self.renderer.render_event(&event)?; | |
| } | |
| } | |
| self.line_buffer = self.line_buffer.get(pos + 1..).unwrap_or("").to_string(); | |
| } | |
| Ok(()) | |
| } | |
| /// Finish rendering, flushing any remaining buffered content. | |
| /// Returns the underlying writer. | |
| pub fn finish(mut self) -> io::Result<()> { | |
| if !self.line_buffer.is_empty() { | |
| for repaired in repair_line(&self.line_buffer, self.parser.state()) { | |
| for event in self.parser.parse_line(&repaired) { | |
| self.renderer.render_event(&event)?; | |
| } | |
| } | |
| } | |
| for event in self.parser.finalize() { | |
| self.renderer.render_event(&event)?; | |
| } | |
| Ok(()) | |
| } | |
| } | |
| mod tests { | |
| use pretty_assertions::assert_eq; | |
| use super::StreamdownRenderer; | |
| fn fixture_rendered_output(markdown: &str, width: usize) -> String { | |
| let mut output = Vec::new(); | |
| let mut fixture = StreamdownRenderer::new(&mut output, width); | |
| fixture.push(markdown).unwrap(); | |
| fixture.finish().unwrap(); | |
| let actual = strip_ansi_escapes::strip(output); | |
| String::from_utf8(actual) | |
| .unwrap() | |
| .trim_matches('\n') | |
| .to_string() | |
| } | |
| fn fixture_rendered_output_from_chunks(chunks: &[&str], width: usize) -> String { | |
| let mut output = Vec::new(); | |
| let mut fixture = StreamdownRenderer::new(&mut output, width); | |
| for chunk in chunks { | |
| fixture.push(chunk).unwrap(); | |
| } | |
| fixture.finish().unwrap(); | |
| let actual = strip_ansi_escapes::strip(output); | |
| String::from_utf8(actual) | |
| .unwrap() | |
| .trim_matches('\n') | |
| .to_string() | |
| } | |
| fn test_streaming_renderer_preserves_korean_spacing_in_structured_markdown() { | |
| let fixture = concat!( | |
| "## ꡬν μμ½\n", | |
| "- κ° μλΉμ€μμ metadata keyλ₯Ό κ°λ³ μμ νμ§ μκ³ , object storage κ³΅ν΅ λ μ΄μ΄μμ μΌκ΄ μ κ·ννλλ‘ λ°μνμ΅λλ€.\n", | |
| "## κ²ν μ¬ν\n", | |
| "- λ³Έ μμ μ μ λ‘λ μ metadata header μ΄λ¦ λ¬Έμ λ₯Ό ν΄κ²°ν©λλ€.\n", | |
| "- μΆκ°μ μΈ κΆν μ μ± , bucket policy, reverse proxy μ νμ΄ μμΌλ©΄ λ³λ μ€λ₯κ° λ°μν μ μμ΅λλ€.\n", | |
| ); | |
| let actual = fixture_rendered_output(fixture, 200); | |
| let expected = concat!( | |
| "## ꡬν μμ½\n", | |
| "β’ κ° μλΉμ€μμ metadata keyλ₯Ό κ°λ³ μμ νμ§ μκ³ , object storage κ³΅ν΅ λ μ΄μ΄μμ μΌκ΄ μ κ·ννλλ‘ λ°μνμ΅λλ€.\n", | |
| "\n", | |
| "## κ²ν μ¬ν\n", | |
| "β’ λ³Έ μμ μ μ λ‘λ μ metadata header μ΄λ¦ λ¬Έμ λ₯Ό ν΄κ²°ν©λλ€.\n", | |
| "β’ μΆκ°μ μΈ κΆν μ μ± , bucket policy, reverse proxy μ νμ΄ μμΌλ©΄ λ³λ μ€λ₯κ° λ°μν μ μμ΅λλ€.", | |
| ); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_streaming_renderer_preserves_korean_spacing_when_structured_tail_arrives_in_chunks() { | |
| let fixture = [ | |
| "## κ²ν κ²°κ³Ό\n", | |
| "- λ³Έ μ¬λ‘λ μ€νΈλ¦¬λ° λ§ν¬λ€μ΄ λ λλ§μ 곡백 μ¬μ‘°ν© λ¬Έμ μ κ΄λ ¨μ΄ μμ΅λλ€.\n", | |
| "- ν΅μ¬ ꡬνμ 곡백 보쑴 λνΌμ μμΉν©λλ€.\n", | |
| "- νκ· ν μ€νΈλ μ€νΈλ¦¬λ° λ λλ¬ κ²μ¦ νλͺ©μ μΆκ°λμ΄ μμ΅λλ€.\n\n", | |
| "νμ μμ μ λ€μκ³Ό κ°μ΅λλ€.\n", | |
| "1. λ³κ²½ μ¬νμ κ²ν κ°λ₯ν νμμΌλ‘ μ 리ν©λλ€.\n", | |
| "2. μ€μ λν μΆλ ₯κ³Ό μ μ¬ν ν΅ν© ν μ€νΈ λ²μλ₯Ό ", | |
| "νμ₯ν©λλ€.", | |
| ]; | |
| let actual = fixture_rendered_output_from_chunks(&fixture, 200); | |
| let expected = concat!( | |
| "## κ²ν κ²°κ³Ό\n", | |
| "β’ λ³Έ μ¬λ‘λ μ€νΈλ¦¬λ° λ§ν¬λ€μ΄ λ λλ§μ 곡백 μ¬μ‘°ν© λ¬Έμ μ κ΄λ ¨μ΄ μμ΅λλ€.\n", | |
| "β’ ν΅μ¬ ꡬνμ 곡백 보쑴 λνΌμ μμΉν©λλ€.\n", | |
| "β’ νκ· ν μ€νΈλ μ€νΈλ¦¬λ° λ λλ¬ κ²μ¦ νλͺ©μ μΆκ°λμ΄ μμ΅λλ€.\n", | |
| "\n", | |
| "νμ μμ μ λ€μκ³Ό κ°μ΅λλ€.\n", | |
| "1. λ³κ²½ μ¬νμ κ²ν κ°λ₯ν νμμΌλ‘ μ 리ν©λλ€.\n", | |
| "2. μ€μ λν μΆλ ₯κ³Ό μ μ¬ν ν΅ν© ν μ€νΈ λ²μλ₯Ό νμ₯ν©λλ€.", | |
| ); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_streaming_renderer_wraps_blockquotes_with_prefix_width_and_long_tokens() { | |
| let fixture = "> supercalifragilistic\n> νκΈ κ³΅λ°±\n"; | |
| let actual = fixture_rendered_output(fixture, 10); | |
| let expected = concat!( | |
| "β supercal\n", | |
| "β ifragili\n", | |
| "β stic\n", | |
| "β νκΈ\n", | |
| "β 곡백" | |
| ); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_streaming_renderer_wraps_blockquote_links_without_losing_separator() { | |
| let fixture = "> [λ§ν¬](https://example.com/very/long/path) μ€λͺ \n"; | |
| let actual = fixture_rendered_output(fixture, 20); | |
| let expected = concat!( | |
| "β λ§ν¬\n", | |
| "β (https://example.c\n", | |
| "β om/very/long/path)\n", | |
| "β μ€λͺ " | |
| ); | |
| assert_eq!(actual, expected); | |
| } | |
| fn test_streaming_renderer_wraps_nested_blockquotes_with_correct_prefix_width() { | |
| let fixture = ">> supercalifragilistic\n"; | |
| let actual = fixture_rendered_output(fixture, 12); | |
| let expected = concat!("β β supercal\n", "β β ifragili\n", "β β stic"); | |
| assert_eq!(actual, expected); | |
| } | |
| } | |