From 9e9e8ecbab7c3b344d35dd7e3e194c2f70823aec Mon Sep 17 00:00:00 2001 From: Sebastien Rousseau Date: Mon, 30 Sep 2024 19:04:24 +0100 Subject: [PATCH] =?UTF-8?q?feat(ssg):=20=E2=9C=A8=20migrating=20to=20`mdx-?= =?UTF-8?q?gen`=20crate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 95 ++--- Cargo.toml | 2 +- ssg-html/Cargo.toml | 1 + ssg-html/src/generator.rs | 100 ++--- ssg-markdown/Cargo.toml | 58 --- ssg-markdown/README.md | 113 ------ ssg-markdown/benches/markdown_benchmark.rs | 107 ------ ssg-markdown/build.rs | 56 --- ssg-markdown/examples/basic_conversion.rs | 40 -- ssg-markdown/src/error.rs | 50 --- ssg-markdown/src/extensions.rs | 222 ----------- ssg-markdown/src/lib.rs | 31 -- ssg-markdown/src/markdown.rs | 423 --------------------- ssg-markdown/tests/integration_tests.rs | 54 --- ssg-metadata/Cargo.toml | 2 +- ssg/Cargo.toml | 1 - 16 files changed, 92 insertions(+), 1263 deletions(-) delete mode 100644 ssg-markdown/Cargo.toml delete mode 100644 ssg-markdown/README.md delete mode 100644 ssg-markdown/benches/markdown_benchmark.rs delete mode 100644 ssg-markdown/build.rs delete mode 100644 ssg-markdown/examples/basic_conversion.rs delete mode 100644 ssg-markdown/src/error.rs delete mode 100644 ssg-markdown/src/extensions.rs delete mode 100644 ssg-markdown/src/lib.rs delete mode 100644 ssg-markdown/src/markdown.rs delete mode 100644 ssg-markdown/tests/integration_tests.rs diff --git a/Cargo.lock b/Cargo.lock index fc6bbf7a..e866ab9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,12 +59,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" - [[package]] name = "anes" version = "0.1.6" @@ -1256,16 +1250,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ "ahash 0.8.11", - "allocator-api2", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", ] [[package]] @@ -1744,6 +1728,28 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "mdx-gen" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eba95ca121d5154b8ec38928b5421207ce7354d9b924e04dc648589d09d9861c" +dependencies = [ + "anyhow", + "comrak", + "env_logger", + "html-escape", + "lazy_static", + "log", + "regex", + "serde", + "serde_json", + "syntect", + "thiserror", + "tokio", + "toml", + "version_check", +] + [[package]] name = "memchr" version = "2.7.4" @@ -2503,9 +2509,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick 1.1.3", "memchr", @@ -2515,9 +2521,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick 1.1.3", "memchr", @@ -2526,9 +2532,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" @@ -3088,7 +3094,6 @@ dependencies = [ "ssg-frontmatter", "ssg-html", "ssg-i18n", - "ssg-markdown", "ssg-metadata", "ssg-server", "ssg-sitemap", @@ -3098,7 +3103,7 @@ dependencies = [ "uuid", "version_check", "vrd 0.0.8", - "yaml-rust2 0.9.0", + "yaml-rust2", ] [[package]] @@ -3144,7 +3149,7 @@ dependencies = [ "uuid", "version_check", "vrd 0.0.8", - "yaml-rust2 0.9.0", + "yaml-rust2", ] [[package]] @@ -3166,6 +3171,7 @@ dependencies = [ "criterion", "html-escape", "htmlescape", + "mdx-gen", "minify-html", "once_cell", "regex", @@ -3197,30 +3203,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "ssg-markdown" -version = "0.0.1" -dependencies = [ - "anyhow", - "assert_fs", - "comrak", - "criterion", - "env_logger", - "html-escape", - "lazy_static", - "log", - "predicates", - "regex", - "serde", - "serde_json", - "serde_yml", - "syntect", - "tempfile", - "thiserror", - "toml", - "version_check", -] - [[package]] name = "ssg-metadata" version = "0.0.1" @@ -3240,7 +3222,7 @@ dependencies = [ "tokio", "toml", "version_check", - "yaml-rust2 0.8.1", + "yaml-rust2", ] [[package]] @@ -4217,17 +4199,6 @@ dependencies = [ "linked-hash-map", ] -[[package]] -name = "yaml-rust2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink 0.8.4", -] - [[package]] name = "yaml-rust2" version = "0.9.0" @@ -4236,7 +4207,7 @@ checksum = "2a1a1c0bc9823338a3bdf8c61f994f23ac004c6fa32c08cd152984499b445e8d" dependencies = [ "arraydeque", "encoding_rs", - "hashlink 0.9.1", + "hashlink", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a92f5673..59864dbf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["ssg", "ssg-cli", "ssg-core", "ssg-frontmatter", "ssg-html", "ssg-i18n", "ssg-markdown", "ssg-metadata" ,"ssg-server", "ssg-sitemap", "ssg-template"] +members = ["ssg", "ssg-cli", "ssg-core", "ssg-frontmatter", "ssg-html", "ssg-i18n", "ssg-metadata" ,"ssg-server", "ssg-sitemap", "ssg-template"] resolver = "2" [workspace.package] diff --git a/ssg-html/Cargo.toml b/ssg-html/Cargo.toml index f83ea4be..c7a0c979 100644 --- a/ssg-html/Cargo.toml +++ b/ssg-html/Cargo.toml @@ -15,6 +15,7 @@ categories = ["web-programming", "text-processing"] comrak = "0.28" html-escape = "0.2.13" htmlescape = "0.3" +mdx-gen = "0.0.1" minify-html = "0.15" once_cell = "1.19" regex = "1.10" diff --git a/ssg-html/src/generator.rs b/ssg-html/src/generator.rs index a67edadc..82c28db3 100644 --- a/ssg-html/src/generator.rs +++ b/ssg-html/src/generator.rs @@ -1,8 +1,9 @@ use crate::extract_front_matter; +use crate::HtmlError; use crate::Result; -use comrak::{markdown_to_html, ComrakOptions}; +use mdx_gen::{process_markdown, ComrakOptions, MarkdownOptions}; -/// Generate HTML from Markdown content. +/// Generate HTML from Markdown content using `mdx-gen`. /// /// This function takes Markdown content and a configuration object, /// converts the Markdown into HTML, and returns the resulting HTML string. @@ -33,11 +34,11 @@ pub fn generate_html( markdown_to_html_with_extensions(markdown) } -/// Convert Markdown to HTML with specified extensions. +/// Convert Markdown to HTML with specified extensions using `mdx-gen`. /// /// This function applies a set of extensions to enhance the conversion -/// process. These extensions include strikethrough, table support, autolinks, -/// tasklists, and superscripts. +/// process, such as syntax highlighting, enhanced table formatting, +/// custom blocks, and more. /// /// # Arguments /// @@ -58,32 +59,26 @@ pub fn generate_html( pub fn markdown_to_html_with_extensions( markdown: &str, ) -> Result { - // Extract front matter from the Markdown content let content_without_front_matter = extract_front_matter(markdown).unwrap_or(markdown.to_string()); - let mut options = ComrakOptions::default(); - options.extension.strikethrough = true; - options.extension.table = true; - options.extension.autolink = true; - options.extension.tasklist = true; - options.extension.superscript = true; - - // Set render options to avoid wrapping everything in

-    options.render.github_pre_lang = false; // Ensures Comrak doesn't assume everything is code
-    options.render.unsafe_ = true; // Allow unsafe HTML rendering for better debugging
-
-    // Debug print to ensure options are correctly set
-    // println!("{:?}", options);
-
-    // Render the Markdown to HTML
-    let html_output =
-        markdown_to_html(&content_without_front_matter, &options);
-
-    // Print the generated HTML to debug the issue
-    // println!("{}", html_output);
-
-    Ok(html_output)
+    let mut comrak_options = ComrakOptions::default();
+    comrak_options.extension.strikethrough = true;
+    comrak_options.extension.table = true;
+    comrak_options.extension.autolink = true;
+    comrak_options.extension.tasklist = true;
+    comrak_options.extension.superscript = true;
+
+    let options =
+        MarkdownOptions::default().with_comrak_options(comrak_options);
+
+    // Process the Markdown to HTML using `mdx-gen`
+    match process_markdown(&content_without_front_matter, &options) {
+        Ok(html_output) => Ok(html_output),
+        Err(err) => {
+            Err(HtmlError::MarkdownConversionError(err.to_string()))
+        }
+    }
 }
 
 #[cfg(test)]
@@ -107,11 +102,10 @@ mod tests {
 
     /// Test conversion with Markdown extensions.
     ///
-    /// This test ensures that the Markdown extensions (e.g., strikethrough, tables, autolinks)
+    /// This test ensures that the Markdown extensions (e.g., custom blocks, enhanced tables, etc.)
     /// are correctly applied when converting Markdown to HTML.
     #[test]
     fn test_markdown_to_html_with_extensions() {
-        // Simplified input to focus on table rendering
         let markdown = r#"
 | Header 1 | Header 2 |
 | -------- | -------- |
@@ -121,16 +115,18 @@ mod tests {
         assert!(result.is_ok());
         let html = result.unwrap();
 
-        // Debug output
         println!("{}", html);
 
-        // Check if the table is rendered
-        assert!(html.contains(""), "Table element not found");
+        // Update the test to look for the div wrapper and table classes
+        assert!(html.contains("
"), "Table element not found"); assert!( html.contains(""), "Table header not found" ); - assert!(html.contains(""), "Table row not found"); + assert!( + html.contains(""), + "Table row not found" + ); } /// Test conversion of empty Markdown. @@ -158,15 +154,12 @@ mod tests { assert!(result.is_ok()); let html = result.unwrap(); - // Debug output println!("{}", html); - // Modify the assertion to reflect Comrak's strict handling of unclosed tags assert!( html.contains("

Unclosed header

"), "Header not found" ); - // Comrak does not automatically close bold tags; this is expected behaviour assert!( html.contains("

Some **unclosed bold

"), "Unclosed bold tag not properly handled" @@ -179,7 +172,6 @@ mod tests { /// elements like lists, headers, code blocks, and links. #[test] fn test_generate_html_complex() { - // Ensure no leading indentation in the Markdown input. let markdown = r#" # Header @@ -201,10 +193,9 @@ fn main() { assert!(result.is_ok()); let html = result.unwrap(); - // Print the generated HTML for debugging - println!("{}", html); + println!("{}", html); // Print the HTML for inspection - // Verify if Comrak processed the Markdown as expected + // Verify the header and subheader assert!( html.contains("

Header

"), "H1 Header not found" @@ -213,6 +204,8 @@ fn main() { html.contains("

Subheader

"), "H2 Header not found" ); + + // Verify the inline code and link assert!( html.contains("inline code"), "Inline code not found" @@ -222,10 +215,29 @@ fn main() { "Link not found" ); - // Check for encoded special characters in code blocks + // Verify that the code block starts correctly + assert!( + html.contains(r#""#), + "Rust code block not found" + ); + + // Match each part of the highlighted syntax separately + // Check for `fn` keyword in a span with the correct style assert!( - html.contains(""Hello, world!""), - "Special characters not encoded in code block" + html.contains(r#"fn "#), + "`fn` keyword with syntax highlighting not found" ); + + // Check for `main` in a span with the correct style + assert!( + html.contains( + r#"main"# + ), + "`main` function name with syntax highlighting not found" + ); + + // Check for `First item` and `Second item` in the ordered list + assert!(html.contains("First item"), "First item not found"); + assert!(html.contains("Second item"), "Second item not found"); } } diff --git a/ssg-markdown/Cargo.toml b/ssg-markdown/Cargo.toml deleted file mode 100644 index c11587c0..00000000 --- a/ssg-markdown/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -# Cargo.toml - Configuration file for the SSG Markdown (Shokunin Static Site Generator Markdown) -# Copyright © 2024 Shokunin Static Site Generator. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 OR MIT - -[package] -# Metadata about the package. -authors.workspace = true -build = "build.rs" -edition.workspace = true -exclude.workspace = true -homepage.workspace = true -license.workspace = true -repository.workspace = true -rust-version.workspace = true - -# The name of the crate, used by Cargo to identify the project. -name = "ssg-markdown" -version = "0.0.1" - -# A short description of the project. This is displayed on package registries like crates.io. -description = "A Markdown processing and conversion library for the Shokunin Static Site Generator" - -# The path to the README file for the project. Cargo will include this file in the package. -readme = "README.md" - -[dependencies] -# Dependencies required for building and running the project. -anyhow = "1.0" -comrak = "0.28.0" -env_logger = "0.11" -html-escape = "0.2.13" -lazy_static = "1.5" -log = "0.4" -regex = "1.10" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yml = "0.0.12" -syntect = "5.2" -thiserror = "1.0" -toml = "0.8" - -[build-dependencies] -# Dependencies for build scripts. -version_check = "0.9" # Check the Rust version used to compile the package. - -[lib] -name = "ssg_markdown" -path = "src/lib.rs" - -[dev-dependencies] -tempfile = "3.12" -assert_fs = "1.1" -predicates = "3.1" -criterion = "0.5" - -[[bench]] -name = "markdown_benchmark" -harness = false diff --git a/ssg-markdown/README.md b/ssg-markdown/README.md deleted file mode 100644 index d50efadc..00000000 --- a/ssg-markdown/README.md +++ /dev/null @@ -1,113 +0,0 @@ - - - - -# Shokunin Static Site Generator Markdown (ssg-markdown) - -A Rust-based library for processing and enhancing Markdown content in static site generators. The library provides tools for converting Markdown to HTML with support for custom blocks, syntax highlighting, and enhanced table formatting. - -[![Made With Love][made-with-rust]][14] [![Crates.io][crates-badge]][8] [![Lib.rs][libs-badge]][10] [![Docs.rs][docs-badge]][9] [![License][license-badge]][2] - -## Overview - -`ssg-markdown` is designed for developers working on static site generators (SSG) who need robust tools to process Markdown content and convert it to HTML with additional features. It helps ensure that your static sites have rich, well-formatted content with support for custom elements and syntax highlighting. - -## Features - -- **Markdown to HTML Conversion**: Convert Markdown content to HTML using the `comrak` library. -- **Custom Block Extensions**: Support for custom blocks such as notes, warnings, and tips. -- **Syntax Highlighting**: Apply syntax highlighting to code blocks in various programming languages. -- **Enhanced Table Formatting**: Improve the formatting and responsiveness of HTML tables. -- **Flexible Configuration**: Easily customize the Markdown processing behavior through `MarkdownOptions`. -- **Error Handling**: Robust error handling with detailed error types and context. - -## Installation - -Add this to your `Cargo.toml`: - -```toml -[dependencies] -ssg-markdown = "0.0.1" -``` - -## Usage - -Here are some examples of how to use the library: - -### Basic Usage - -```rust -use ssg_markdown::{process_markdown, MarkdownOptions}; - -let markdown_content = "# Hello, world!\n\nThis is a paragraph."; -let options = MarkdownOptions::new(); -let html = process_markdown(markdown_content, &options).unwrap(); -println!("HTML output: {}", html); -``` - -### Custom Blocks and Syntax Highlighting - -```rust -use ssg_markdown::{process_markdown, MarkdownOptions}; - -let markdown_content = r#" -# Example - -
This is a note.
- -```rust -fn main() { - println!("Hello, world!"); -} - -"#; - -let options = MarkdownOptions::new() - .with_custom_blocks(true) - .with_syntax_highlighting(true); - -let html = process_markdown(markdown_content, &options).unwrap(); -println!("HTML output: {}", html); - -``` - -## Modules - -- **lib.rs**: The main library module that ties everything together. -- **markdown.rs**: Core functionality for Markdown processing and conversion. -- **extensions.rs**: Handles custom block extensions, syntax highlighting, and table processing. -- **error.rs**: Defines error types and implements error handling for the library. - -## Documentation - -For full API documentation, please visit [docs.rs/ssg-markdown][9]. - -## Contributing - -Contributions are welcome! Please feel free to submit a Pull Request. - -## License - -This project is licensed under either of - -- [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) -- [MIT license](https://opensource.org/licenses/MIT) - -at your option. - -## Acknowledgements - -Special thanks to all contributors who have helped build the `ssg-markdown` library. - -[9]: https://docs.rs/ssg-markdown -[2]: https://opensource.org/licenses/MIT -[8]: https://crates.io/crates/ssg-markdown -[10]: https://lib.rs/crates/ssg-markdown -[14]: https://www.rust-lang.org - -[crates-badge]: https://img.shields.io/crates/v/ssg-html.svg?style=for-the-badge 'Crates.io badge' -[docs-badge]: https://img.shields.io/docsrs/ssg-html.svg?style=for-the-badge 'Docs.rs badge' -[libs-badge]: https://img.shields.io/badge/lib.rs-v0.1.0-orange.svg?style=for-the-badge 'Lib.rs badge' -[license-badge]: https://img.shields.io/crates/l/ssg-html.svg?style=for-the-badge 'License badge' -[made-with-rust]: https://img.shields.io/badge/rust-f04041?style=for-the-badge&labelColor=c0282d&logo=rust 'Made With Rust badge' diff --git a/ssg-markdown/benches/markdown_benchmark.rs b/ssg-markdown/benches/markdown_benchmark.rs deleted file mode 100644 index 68492f1b..00000000 --- a/ssg-markdown/benches/markdown_benchmark.rs +++ /dev/null @@ -1,107 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 OR MIT -// See LICENSE-APACHE.md and LICENSE-MIT.md in the repository root for full license information. - -#![allow(missing_docs)] - -use comrak::ComrakOptions; -use criterion::{ - black_box, criterion_group, criterion_main, BenchmarkId, Criterion, - Throughput, -}; -use ssg_markdown::{process_markdown, MarkdownOptions}; - -/// Create a valid MarkdownOptions configuration -fn create_valid_options( - syntax_highlighting: bool, - custom_blocks: bool, - enhanced_tables: bool, - enable_comrak_tables: bool, -) -> MarkdownOptions<'static> { - let mut comrak_options = ComrakOptions::default(); - comrak_options.extension.table = enable_comrak_tables; - - MarkdownOptions::new() - .with_syntax_highlighting(syntax_highlighting) - .with_custom_blocks(custom_blocks) - .with_enhanced_tables(enhanced_tables) - .with_comrak_options(comrak_options) -} - -/// Benchmark the Markdown to HTML conversion process with various configurations. -fn markdown_benchmark(c: &mut Criterion) { - let small_markdown = r#" -# Welcome to SSG Markdown -This is a **bold** statement and this is *italic*. -## Features -- Easy to use -- Extensible -- Fast -Check out [our website](https://example.com) for more information. - "#; - - let large_markdown = include_str!("../README.md"); - - let markdown_sizes = - [("small", small_markdown), ("large", large_markdown)]; - - let mut group = c.benchmark_group("Markdown to HTML Conversion"); - - for (size, markdown) in markdown_sizes.iter() { - group.throughput(Throughput::Bytes(markdown.len() as u64)); - - // Basic conversion (no enhanced tables) - let basic_options = - create_valid_options(false, false, false, false); - group.bench_with_input( - BenchmarkId::new("basic", size), - markdown, - |b, markdown| { - b.iter(|| { - let _ = process_markdown( - black_box(markdown), - black_box(&basic_options), - ) - .expect("Basic conversion should not fail"); - }); - }, - ); - - // Full-featured conversion - let full_options = create_valid_options(true, true, true, true); - group.bench_with_input( - BenchmarkId::new("full", size), - markdown, - |b, markdown| { - b.iter(|| { - let _ = process_markdown( - black_box(markdown), - black_box(&full_options), - ) - .expect("Full-featured conversion should not fail"); - }); - }, - ); - - // Custom configuration - let custom_options = - create_valid_options(true, false, true, true); - group.bench_with_input( - BenchmarkId::new("custom", size), - markdown, - |b, markdown| { - b.iter(|| { - let _ = process_markdown( - black_box(markdown), - black_box(&custom_options), - ) - .expect("Custom conversion should not fail"); - }); - }, - ); - } - - group.finish(); -} - -criterion_group!(benches, markdown_benchmark); -criterion_main!(benches); diff --git a/ssg-markdown/build.rs b/ssg-markdown/build.rs deleted file mode 100644 index db9e681c..00000000 --- a/ssg-markdown/build.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! This build script checks if the current Rustc version is at least the -//! minimum required version. -//! If the current Rustc version is less than the minimum required version, -//! the build script will exit the build process with a non-zero exit code. -//! -//! The minimum required version is specified in the `min_version` variable. - -use std::process; - -/// Checks if the current Rustc version is at least the minimum required version -/// -/// # Arguments -/// -/// * `min_version` - The minimum required Rustc version as a string. -/// -/// # Returns -/// -/// * `Some(true)` - If the current Rustc version is at least the minimum -/// required version. -/// * `Some(false)` - If the current Rustc version is less than the minimum -/// required version. -/// * `None` - If the current Rustc version cannot be determined. -/// -/// # Errors -/// -/// This function will exit the build process with a non-zero exit code if the -/// current Rustc version is less than the minimum required version. -/// -/// # Examples -/// -/// ```rust -/// let min_version = "1.56"; -/// -/// match version_check::is_min_version(min_version) { -/// Some(true) => println!("Rustc version is at least {}", min_version), -/// Some(false) => { -/// eprintln!("Rustc version is less than {}", min_version); -/// process::exit(1); -/// } -/// None => { -/// eprintln!("Unable to determine Rustc version"); -/// process::exit(1); -/// } -/// } -/// ``` -fn main() { - let min_version = "1.56"; - - match version_check::is_min_version(min_version) { - Some(true) => {} - _ => { - eprintln!("'fd' requires Rustc version >= {}", min_version); - process::exit(1); - } - } -} diff --git a/ssg-markdown/examples/basic_conversion.rs b/ssg-markdown/examples/basic_conversion.rs deleted file mode 100644 index 561aeb08..00000000 --- a/ssg-markdown/examples/basic_conversion.rs +++ /dev/null @@ -1,40 +0,0 @@ -//! # Basic Markdown to HTML Conversion Example -//! -//! This example demonstrates how to use the `ssg-markdown` crate to convert Markdown content -//! into HTML using the `comrak` library. It shows how to configure various Markdown extensions -//! (e.g., strikethrough, tables, and autolinks) and then process the Markdown content to generate HTML. -//! -//! ## Usage -//! -//! Simply run the example, and it will print the converted HTML to the console. You can customize -//! the Markdown content and options to see how different configurations affect the output. - -use ssg_markdown::{process_markdown, MarkdownOptions}; - -fn main() -> Result<(), Box> { - let markdown = r#" -# Welcome to SSG Markdown - -This is a **bold** statement and this is *italic*. - -## Features - -- Easy to use -- Extensible -- Fast - -Check out [our website](https://example.com) for more information. - "#; - - // Initialize MarkdownOptions with default Comrak options - let mut options = MarkdownOptions::default(); - options.comrak_options.extension.strikethrough = true; // Enable strikethrough - options.comrak_options.extension.table = true; // Enable tables - options.comrak_options.extension.autolink = true; // Enable automatic links - - // Process the markdown content to HTML - let html = process_markdown(markdown, &options)?; - println!("Converted HTML:\n\n{}", html); - - Ok(()) -} diff --git a/ssg-markdown/src/error.rs b/ssg-markdown/src/error.rs deleted file mode 100644 index c16e6ff3..00000000 --- a/ssg-markdown/src/error.rs +++ /dev/null @@ -1,50 +0,0 @@ -//! Error handling for the SSG Markdown library. - -use anyhow::{Context, Result}; -use thiserror::Error; - -/// Errors that can occur during Markdown processing. -#[derive(Error, Debug)] -pub enum MarkdownError { - /// An error occurred while parsing the Markdown content. - #[error("Failed to parse Markdown: {0}")] - ParseError(String), - - /// An error occurred while converting Markdown to HTML. - #[error("Failed to convert Markdown to HTML: {0}")] - ConversionError(String), - - /// An error occurred while processing a custom block. - #[error("Failed to process custom block: {0}")] - CustomBlockError(String), - - /// An error occurred while applying syntax highlighting. - #[error("Syntax highlighting error: {0}")] - SyntaxHighlightError(String), - - /// An error occurred due to invalid options. - #[error("Invalid Markdown options: {0}")] - InvalidOptionsError(String), - - /// An error occurred while loading a syntax set. - #[error("Failed to load syntax set: {0}")] - SyntaxHighlightingError(String), -} - -/// A helper function that adds context to errors occurring during Markdown processing. -pub fn parse_markdown_with_context(input: &str) -> Result { - // Example of adding context using anyhow - let parsed_content = some_markdown_parsing_function(input) - .context("Failed while parsing markdown content")?; - - Ok(parsed_content) -} - -// Placeholder for the actual markdown parsing function -fn some_markdown_parsing_function(input: &str) -> Result { - // Simulate success or failure - if input.is_empty() { - Err(MarkdownError::ParseError("Input is empty".to_string()))?; - } - Ok("Parsed markdown content".to_string()) -} diff --git a/ssg-markdown/src/extensions.rs b/ssg-markdown/src/extensions.rs deleted file mode 100644 index b7961281..00000000 --- a/ssg-markdown/src/extensions.rs +++ /dev/null @@ -1,222 +0,0 @@ -//! Extension functionality for the SSG Markdown library. -//! -//! This module provides utilities for enhancing Markdown processing, -//! including syntax highlighting, table formatting, and custom block handling. - -use crate::error::MarkdownError; -use lazy_static::lazy_static; -use regex::Regex; -use syntect::{ - highlighting::ThemeSet, html::highlighted_html_for_string, - parsing::SyntaxSet, -}; - -lazy_static! { - /// Cached `SyntaxSet` to avoid reloading on every function call. - static ref SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines(); - /// Cached `ThemeSet` to avoid reloading on every function call. - static ref THEME_SET: ThemeSet = ThemeSet::load_defaults(); -} - -/// Alignment options for table columns. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum ColumnAlignment { - /// Align the column to the left. - Left, - /// Align the column to the center. - Center, - /// Align the column to the right. - Right, -} - -/// Represents different types of custom blocks. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum CustomBlockType { - /// A note block. - Note, - /// A warning block. - Warning, - /// A tip block. - Tip, - /// An info block. - Info, - /// An important block. - Important, - /// A caution block. - Caution, -} - -impl CustomBlockType { - /// Returns the appropriate Bootstrap alert class for the custom block type. - fn get_alert_class(&self) -> &'static str { - match self { - CustomBlockType::Note => "alert-info", - CustomBlockType::Warning => "alert-warning", - CustomBlockType::Tip => "alert-success", - CustomBlockType::Info => "alert-primary", - CustomBlockType::Important => "alert-danger", - CustomBlockType::Caution => "alert-secondary", - } - } - - /// Returns the title for the custom block type. - fn get_title(&self) -> &'static str { - match self { - CustomBlockType::Note => "Note", - CustomBlockType::Warning => "Warning", - CustomBlockType::Tip => "Tip", - CustomBlockType::Info => "Info", - CustomBlockType::Important => "Important", - CustomBlockType::Caution => "Caution", - } - } -} - -lazy_static! { - static ref CUSTOM_BLOCK_REGEX: Regex = Regex::new( - r#"(?i)(.*?)"# - ).unwrap(); -} - -/// Applies syntax highlighting to code blocks in the Markdown. -/// -/// # Arguments -/// -/// * `code` - The code block string to be highlighted. -/// * `lang` - The programming language of the code block. -/// -/// # Returns -/// -/// A `Result` containing the HTML for the highlighted code or a `MarkdownError`. -pub fn apply_syntax_highlighting( - code: &str, - lang: &str, -) -> Result { - // Use cached SyntaxSet and ThemeSet - let theme = &THEME_SET.themes["base16-ocean.dark"]; - - let syntax = SYNTAX_SET - .find_syntax_by_token(lang) - .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text()); - - highlighted_html_for_string(code, &SYNTAX_SET, syntax, theme) - .map_err(|e| MarkdownError::SyntaxHighlightError(e.to_string())) -} - -/// Processes tables, enhancing them with responsive design and alignment classes. -/// -/// # Arguments -/// -/// * `table_html` - The HTML string representing the table. -/// -/// # Returns -/// -/// The enhanced HTML string. -pub fn process_tables(table_html: &str) -> String { - let table_regex = Regex::new(r"
Header 1Row 1Row 1
").unwrap(); - let table_html = table_regex.replace( - table_html, - r#"
"#, - ); - - let table_end_regex = Regex::new(r"
").unwrap(); - let table_html = - table_end_regex.replace(&table_html, ""); - - // Add alignment classes to table cells - let cell_regex = Regex::new(r"]*)>").unwrap(); - let table_html = cell_regex.replace_all( - &table_html, - |caps: ®ex::Captures| { - let attrs = &caps[1]; - if attrs.contains("align=\"center\"") { - format!(r#""#, attrs) - } else if attrs.contains("align=\"right\"") { - format!(r#""#, attrs) - } else { - format!(r#""#, attrs) - } - }, - ); - - table_html.to_string() -} - -/// Processes custom blocks in the Markdown content, such as note, warning, tip, info, important, and caution blocks. -/// These custom blocks are represented by div elements with specific class names. -/// The function replaces these div elements with corresponding Bootstrap alert elements. -/// -/// # Arguments -/// -/// * `content` - A string containing the Markdown content. -/// -/// # Returns -/// -/// A string containing the processed Markdown content with custom blocks replaced by Bootstrap alert elements. -pub fn process_custom_blocks(content: &str) -> String { - CUSTOM_BLOCK_REGEX.replace_all(content, |caps: ®ex::Captures| { - let block_type = match caps.get(1).unwrap().as_str().to_lowercase().as_str() { - "note" => CustomBlockType::Note, - "warning" => CustomBlockType::Warning, - "tip" => CustomBlockType::Tip, - "info" => CustomBlockType::Info, - "important" => CustomBlockType::Important, - "caution" => CustomBlockType::Caution, - _ => unreachable!(), - }; - let block_content = &caps[2]; - format!( - r#""#, - block_type.get_alert_class(), - block_type.get_title(), - block_content - ) - }).to_string() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_process_custom_blocks() { - let input = r#" -
This is a note.
-
This is a warning.
-
This is a tip.
-
This is an info block.
-
This is important.
-
This is a caution.
- "#; - - let processed = process_custom_blocks(input); - - assert!(processed.contains(r#""#)); - assert!(processed.contains(r#""#)); - assert!(processed.contains(r#""#)); - assert!(processed.contains(r#""#)); - assert!(processed.contains(r#""#)); - assert!(processed.contains(r#""#)); - } - - #[test] - fn test_process_tables() { - let input = r#"
CenterRightLeft
"#; - - let processed = process_tables(input); - - assert!(processed.contains( - r#"
"# - )); - assert!(processed.contains( - r#""# - )); - assert!(processed.contains( - r#""# - )); - assert!( - processed.contains(r#""#) - ); - assert!(processed.contains("
CenterRightLeft
")); - } -} diff --git a/ssg-markdown/src/lib.rs b/ssg-markdown/src/lib.rs deleted file mode 100644 index 440d4714..00000000 --- a/ssg-markdown/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! # SSG Markdown Processor -//! -//! The `ssg-markdown` crate provides utilities for parsing, converting, and rendering Markdown -//! into HTML. It integrates with the `comrak` library for Markdown processing and offers a -//! flexible interface for customizing the conversion process. -//! -//! ## Features -//! -//! - Markdown to HTML conversion -//! - Custom block extensions (e.g., notes, warnings, tips) -//! - Syntax highlighting for code blocks -//! - Enhanced table processing -//! - Error handling - -/// The `error` module contains error types for Markdown processing. -pub mod error; - -/// The `extensions` module contains custom block extensions for Markdown processing. -pub mod extensions; - -/// The `markdown` module contains functions for parsing, converting, and rendering Markdown. -pub mod markdown; - -pub use error::MarkdownError; -pub use extensions::{ - apply_syntax_highlighting, ColumnAlignment, CustomBlockType, -}; -pub use markdown::{process_markdown, MarkdownOptions}; - -/// Re-export comrak options for convenience -pub use comrak::ComrakOptions; diff --git a/ssg-markdown/src/markdown.rs b/ssg-markdown/src/markdown.rs deleted file mode 100644 index 74e59c8e..00000000 --- a/ssg-markdown/src/markdown.rs +++ /dev/null @@ -1,423 +0,0 @@ -//! Core Markdown processing functionality. -//! -//! This module handles the conversion of Markdown content into HTML, -//! with support for custom blocks, enhanced tables, and syntax highlighting. -//! - -use crate::error::MarkdownError; -use crate::extensions::{ - apply_syntax_highlighting, process_custom_blocks, process_tables, -}; -use comrak::{markdown_to_html, ComrakOptions}; -use log::{debug, info, warn}; - -/// Options for configuring Markdown processing behavior. -#[derive(Debug, Clone)] -pub struct MarkdownOptions<'a> { - /// Options for the underlying Comrak Markdown parser. - pub comrak_options: ComrakOptions<'a>, - /// Enable or disable processing of custom blocks (e.g., note, warning, tip). - pub enable_custom_blocks: bool, - /// Enable or disable syntax highlighting for code blocks. - pub enable_syntax_highlighting: bool, - /// Enable or disable enhanced table formatting. - pub enable_enhanced_tables: bool, -} - -impl<'a> Default for MarkdownOptions<'a> { - /// Provides default options where custom blocks, syntax highlighting, - /// and enhanced tables are all enabled. - fn default() -> Self { - Self { - comrak_options: ComrakOptions::default(), - enable_custom_blocks: true, - enable_syntax_highlighting: true, - enable_enhanced_tables: true, - } - } -} - -impl<'a> MarkdownOptions<'a> { - /// Creates a new instance of `MarkdownOptions` with default values. - pub fn new() -> Self { - Self::default() - } - - /// Enables or disables custom blocks. - /// - /// # Example - /// ``` - /// use ssg_markdown::MarkdownOptions; - /// let options = MarkdownOptions::new().with_custom_blocks(true); - /// ``` - pub fn with_custom_blocks(mut self, enable: bool) -> Self { - self.enable_custom_blocks = enable; - self - } - - /// Enables or disables syntax highlighting for code blocks. - /// - /// # Example - /// ``` - /// use ssg_markdown::MarkdownOptions; - /// let options = MarkdownOptions::new().with_syntax_highlighting(false); - /// ``` - pub fn with_syntax_highlighting(mut self, enable: bool) -> Self { - self.enable_syntax_highlighting = enable; - self - } - - /// Enables or disables enhanced table formatting. - /// - /// # Example - /// ``` - /// use ssg_markdown::MarkdownOptions; - /// let options = MarkdownOptions::new().with_enhanced_tables(true); - /// ``` - pub fn with_enhanced_tables(mut self, enable: bool) -> Self { - self.enable_enhanced_tables = enable; - self - } - - /// Sets custom Comrak options. - /// - /// # Example - /// ``` - /// use comrak::ComrakOptions; - /// use ssg_markdown::MarkdownOptions; - /// let custom_comrak_options = ComrakOptions::default(); - /// let options = MarkdownOptions::new().with_comrak_options(custom_comrak_options); - /// ``` - pub fn with_comrak_options( - mut self, - options: ComrakOptions<'a>, - ) -> Self { - self.comrak_options = options; - self - } - - /// Validates the `MarkdownOptions` to ensure they are consistent and compatible. - /// - /// # Returns - /// A `Result` indicating whether the options are valid, with an error message if not. - pub fn validate(&self) -> Result<(), String> { - if self.enable_enhanced_tables - && !self.comrak_options.extension.table - { - return Err("Enhanced tables are enabled, but Comrak table extension is disabled.".to_string()); - } - Ok(()) - } -} - -/// Processes the input Markdown content and converts it into HTML. -/// Applies custom blocks, syntax highlighting, and enhanced tables based on the provided options. -/// -/// # Arguments -/// * `content` - The input Markdown content as a string slice. -/// * `options` - Configuration options to enable or disable specific features. -/// -/// # Returns -/// A `Result` containing the processed HTML string, or a `MarkdownError` if processing fails. -/// -pub fn process_markdown( - content: &str, - options: &MarkdownOptions, -) -> Result { - info!("Starting markdown processing"); - debug!("Markdown options: {:?}", options); - - // Validate options - if let Err(msg) = options.validate() { - warn!("Invalid MarkdownOptions: {}", msg); - return Err(MarkdownError::ConversionError(msg)); - } - - // Clone Comrak options and enable unsafe rendering - let mut comrak_opts = options.comrak_options.clone(); - comrak_opts.render.unsafe_ = true; - - // Convert Markdown to initial HTML - debug!("Converting markdown to HTML using Comrak"); - let mut html = markdown_to_html(content, &comrak_opts); - - // Apply syntax highlighting if enabled - if options.enable_syntax_highlighting { - debug!("Applying syntax highlighting"); - html = highlight_code_blocks(&html)?; - } - - // Process enhanced tables if enabled - if options.enable_enhanced_tables { - debug!("Processing enhanced tables"); - html = process_tables(&html); - } - - // Process custom blocks (e.g., note, warning, tip) if enabled - if options.enable_custom_blocks { - debug!("Processing custom blocks"); - html = process_custom_blocks(&html); - } - - info!("Markdown processing completed successfully"); - Ok(html) -} - -/// Highlights code blocks in the generated HTML using the specified syntax highlighter. -/// This function searches for code blocks marked with a language and applies the appropriate -/// syntax highlighting. -/// -/// # Arguments -/// * `html` - The input HTML containing code blocks. -/// -/// # Returns -/// A `Result` containing the HTML with highlighted code blocks, or a `MarkdownError` if highlighting fails. -fn highlight_code_blocks(html: &str) -> Result { - debug!("Highlighting code blocks"); - let re = regex::Regex::new( - r#"(?s)
(.*?)
"#, - ) - .unwrap(); - - let mut highlighted_html = String::new(); - let mut last_end = 0; - - // Iterate over captured code blocks and apply syntax highlighting - for cap in re.captures_iter(html) { - let before = &html[last_end..cap.get(0).unwrap().start()]; - highlighted_html.push_str(before); - - let lang = &cap[1]; - let code = html_escape::decode_html_entities(&cap[2]); - - debug!("Highlighting code block with language: {}", lang); - let highlighted_code = apply_syntax_highlighting(&code, lang)?; - - highlighted_html.push_str(&format!( - "
{}
", - lang, highlighted_code - )); - last_end = cap.get(0).unwrap().end(); - } - - // Append the remaining portion of the HTML - highlighted_html.push_str(&html[last_end..]); - Ok(highlighted_html) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_process_markdown_with_all_features() { - let markdown = r#" -# Test Markdown - -Here's a table: - -| Left | Center | Right | -|:-----|:------:|------:| -| 1 | 2 | 3 | - -```rust -fn main() { - println!("Hello, world!"); -} -``` - -
This is a note.
- -
This is a warning.
- -
This is a tip.
-"#; - - let options = MarkdownOptions::new() - .with_syntax_highlighting(true) - .with_custom_blocks(true) - .with_enhanced_tables(true) - .with_comrak_options({ - let mut opts = ComrakOptions::default(); - opts.extension.table = true; - opts - }); - - let result = process_markdown(markdown, &options); - assert!(result.is_ok(), "Markdown processing failed"); - - let html = result.unwrap(); - - assert!( - html.contains( - r#"
"# - ), - "Table not processed correctly" - ); - assert!( - html.contains(r#"
"#),
-            "Syntax highlighting not applied"
-        );
-        assert!(html.contains(r#"
"# - ), - "Enhanced table processing applied when disabled" - ); - assert!( - html.contains("
"), - "Basic table should still be present" - ); - } - - #[test] - fn test_markdown_options_validation() { - let options = MarkdownOptions::new() - .with_enhanced_tables(true) - .with_comrak_options({ - let mut opts = ComrakOptions::default(); - opts.extension.table = false; - opts - }); - - assert!(options.validate().is_err(), "Validation should fail when enhanced tables are enabled but Comrak table extension is disabled"); - - let options = MarkdownOptions::new() - .with_enhanced_tables(true) - .with_comrak_options({ - let mut opts = ComrakOptions::default(); - opts.extension.table = true; - opts - }); - - assert!( - options.validate().is_ok(), - "Validation should pass when options are consistent" - ); - } - - #[test] - fn test_markdown_options_builder() { - let options = MarkdownOptions::new() - .with_custom_blocks(false) - .with_syntax_highlighting(true) - .with_enhanced_tables(false); - - assert!(!options.enable_custom_blocks); - assert!(options.enable_syntax_highlighting); - assert!(!options.enable_enhanced_tables); - } - - #[test] - fn test_process_markdown_with_invalid_options() { - let markdown = "# Test\n\n| Column 1 | Column 2 |\n| -------- | -------- |\n| Value 1 | Value 2 |"; - - let options = MarkdownOptions::new() - .with_enhanced_tables(true) - .with_comrak_options({ - let mut opts = ComrakOptions::default(); - opts.extension.table = false; - opts - }); - - let result = process_markdown(markdown, &options); - assert!(result.is_err()); - assert!(matches!( - result, - Err(MarkdownError::ConversionError(_)) - )); - } - - #[test] - fn test_process_markdown_with_empty_content() { - let markdown = ""; - let options = MarkdownOptions::new() - .with_enhanced_tables(false) // No need for enhanced tables in an empty document - .with_comrak_options({ - let mut opts = ComrakOptions::default(); - opts.extension.table = false; // Disable table extension - opts - }); - - let result = process_markdown(markdown, &options); - assert!( - result.is_ok(), - "Markdown processing failed for empty content: {:?}", - result - ); - assert_eq!(result.unwrap().trim(), ""); - } - - #[test] - fn test_process_markdown_with_only_custom_blocks() { - let markdown = "
This is a note.
"; - let options = MarkdownOptions::new() - .with_custom_blocks(true) - .with_enhanced_tables(false) // Disable enhanced tables since they're not used here - .with_comrak_options({ - let mut opts = ComrakOptions::default(); - opts.extension.table = false; // Ensure table extension is disabled - opts - }); - - let result = process_markdown(markdown, &options); - assert!( - result.is_ok(), - "Markdown processing failed for custom blocks: {:?}", - result - ); - let html = result.unwrap(); - assert!(html.contains(r#"