From dbe4be79f6dfcdc1c56d94da1c96d149d70cb9af Mon Sep 17 00:00:00 2001 From: Sebastien Rousseau Date: Mon, 7 Oct 2024 20:13:16 +0100 Subject: [PATCH] v0.0.1 --- .github/workflows/document.yml | 8 +- .github/workflows/release.yml | 127 ++++++++++++++ Cargo.toml | 40 ++++- benches/html_benchmark.rs | 64 +++++++ examples/accessibility_example.rs | 143 +++++++++++++++ examples/error_example.rs | 277 ++++++++++++++++++++++++++++++ examples/generator_example.rs | 185 ++++++++++++++++++++ examples/lib_example.rs | 92 ++++++++++ examples/performance_example.rs | 70 ++++++++ examples/seo_example.rs | 58 +++++++ examples/utils_example.rs | 76 ++++++++ src/error.rs | 4 + src/seo.rs | 7 - src/utils.rs | 3 - 14 files changed, 1137 insertions(+), 17 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 examples/accessibility_example.rs create mode 100644 examples/error_example.rs create mode 100644 examples/generator_example.rs create mode 100644 examples/lib_example.rs create mode 100644 examples/performance_example.rs create mode 100644 examples/seo_example.rs create mode 100644 examples/utils_example.rs diff --git a/.github/workflows/document.yml b/.github/workflows/document.yml index 34e5085..742359c 100644 --- a/.github/workflows/document.yml +++ b/.github/workflows/document.yml @@ -19,11 +19,11 @@ jobs: concurrency: group: ${{ github.workflow }}-${{ github.ref }} steps: - - uses: hecrj/setup-rust-action@v2.0.1 + - uses: hecrj/setup-rust-action@v2 with: rust-version: nightly - - uses: actions/checkout@v4.1.7 + - uses: actions/checkout@v4 - name: Update libssl run: | @@ -38,7 +38,7 @@ jobs: echo '' > ./target/doc/index.html - name: Deploy - uses: actions/upload-artifact@v4.4.0 + uses: actions/upload-artifact@v4 with: name: documentation path: target/doc @@ -49,7 +49,7 @@ jobs: run: echo 'doc.html-generator.co' > ./target/doc/CNAME - name: Deploy to GitHub Pages - uses: peaceiris/actions-gh-pages@v4.0.0 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1a31e68 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,127 @@ +name: ๐Ÿงช Release + +on: + push: + branches: [main, feat/html-gen] + pull_request: + branches: [feat/html-gen] + release: + types: [created] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }} + +jobs: + build: + name: Build ๐Ÿ›  + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + strategy: + fail-fast: false + matrix: + include: + - os: windows-latest + target: x86_64-pc-windows-msvc + - os: windows-latest + target: aarch64-pc-windows-msvc + - os: macos-latest + target: x86_64-apple-darwin + - os: macos-latest + target: aarch64-apple-darwin + - os: ubuntu-latest + target: x86_64-unknown-linux-gnu + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + target: ${{ matrix.target }} + override: true + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Build + run: cargo build --verbose --release --target ${{ matrix.target }} + - name: Package + run: | + if [ ! -d "target/package" ]; then + mkdir -p target/package + fi + cd target/${{ matrix.target }}/release + tar czf ../../package/${{ matrix.target }}.tar.gz * + shell: bash + + - name: Package (Windows) + if: matrix.os == 'windows-latest' + run: | + if (!(Test-Path "target/package")) { + mkdir target/package + } + cd target/${{ matrix.target }}/release + tar -czf ../../package/${{ matrix.target }}.tar.gz * + shell: pwsh + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.target }} + path: target/package/${{ matrix.target }}.tar.gz + + release: + name: Release ๐Ÿš€ + needs: build + if: github.ref == 'refs/heads/main' && github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set version + run: echo "VERSION=$(grep -m 1 '^version =' Cargo.toml | cut -d '"' -f 2)" >> $GITHUB_ENV + - uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Generate Changelog + run: | + echo "## Release v${VERSION} - $(date +'%Y-%m-%d')" > CHANGELOG.md + cat TEMPLATE.md >> CHANGELOG.md + git log --pretty=format:'%s' --reverse HEAD >> CHANGELOG.md + echo "" >> CHANGELOG.md + - uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ env.VERSION }} + release_name: HTML Generator ๐Ÿฆ€ v${{ env.VERSION }} + body_path: CHANGELOG.md + draft: true + prerelease: false + - name: Upload Release Assets + run: | + for asset in artifacts/*/*; do + gh release upload v${{ env.VERSION }} "$asset" --clobber + done + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + crate: + name: Publish to Crates.io ๐Ÿฆ€ + needs: release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + toolchain: stable + override: true + - name: Publish + run: cargo publish --token ${CARGO_REGISTRY_TOKEN} + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_API_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index a261c87..243ce3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,10 +74,44 @@ criterion = "0.5" # Examples # ----------------------------------------------------------------------------- -# [[example]] -# name = "error_example" -# path = "examples/error_example.rs" +[[example]] +name = "accessibility_example" +path = "examples/accessibility_example.rs" +[[example]] +name = "error_example" +path = "examples/error_example.rs" + +[[example]] +name = "generator_example" +path = "examples/generator_example.rs" + +[[example]] +name = "lib_example" +path = "examples/lib_example.rs" + +[[example]] +name = "performance_example" +path = "examples/performance_example.rs" + +[[example]] +name = "seo_example" +path = "examples/seo_example.rs" + +[[example]] +name = "utils_example" +path = "examples/utils_example.rs" + +# ----------------------------------------------------------------------------- +# Criterion Benchmark +# ----------------------------------------------------------------------------- + +[[bench]] +name = "html_benchmark" +harness = false + +[profile.bench] +debug = true # ----------------------------------------------------------------------------- # Features diff --git a/benches/html_benchmark.rs b/benches/html_benchmark.rs index 8b13789..d8c1f98 100644 --- a/benches/html_benchmark.rs +++ b/benches/html_benchmark.rs @@ -1 +1,65 @@ +// benches/html_benchmark.rs +#![allow(missing_docs)] +use criterion::{ + black_box, criterion_group, criterion_main, Criterion, +}; +use html_generator::{ + accessibility::add_aria_attributes, generate_html, + performance::minify_html, seo::generate_meta_tags, + utils::extract_front_matter, +}; + +fn benchmark_generate_html(c: &mut Criterion) { + let markdown_input = r#"# Benchmark Heading +This is a test content for benchmarking HTML generation."#; + let config = html_generator::HtmlConfig::default(); + c.bench_function("generate_html", |b| { + b.iter(|| generate_html(black_box(markdown_input), &config)) + }); +} + +fn benchmark_minify_html(c: &mut Criterion) { + let html_input = + r#"

Test

"#; + let temp_file = tempfile::NamedTempFile::new().unwrap(); + std::fs::write(temp_file.path(), html_input).unwrap(); + c.bench_function("minify_html", |b| { + b.iter(|| minify_html(black_box(temp_file.path()))) + }); +} + +fn benchmark_add_aria_attributes(c: &mut Criterion) { + let html_input = r#""#; + c.bench_function("add_aria_attributes", |b| { + b.iter(|| add_aria_attributes(black_box(html_input))) + }); +} + +fn benchmark_generate_meta_tags(c: &mut Criterion) { + let html_input = r#"Page Title

Content

"#; + c.bench_function("generate_meta_tags", |b| { + b.iter(|| generate_meta_tags(black_box(html_input))) + }); +} + +fn benchmark_extract_front_matter(c: &mut Criterion) { + let input = r#"--- +title: Test +--- +# Content +This is the main content."#; + c.bench_function("extract_front_matter", |b| { + b.iter(|| extract_front_matter(black_box(input))) + }); +} + +criterion_group!( + benches, + benchmark_generate_html, + benchmark_minify_html, + benchmark_add_aria_attributes, + benchmark_generate_meta_tags, + benchmark_extract_front_matter +); +criterion_main!(benches); diff --git a/examples/accessibility_example.rs b/examples/accessibility_example.rs new file mode 100644 index 0000000..80329e0 --- /dev/null +++ b/examples/accessibility_example.rs @@ -0,0 +1,143 @@ +// src/examples/accessibility_example.rs +#![allow(missing_docs)] + +use html_generator::{ + accessibility::AccessibilityError, + accessibility::{add_aria_attributes, validate_wcag}, +}; + +/// Entry point for the html-generator accessibility handling examples. +/// +/// This function runs various examples demonstrating error creation, conversion, +/// and handling for different scenarios in the html-generator library. +/// +/// # Errors +/// +/// Returns an error if any of the example functions fail. +fn main() -> Result<(), Box> { + println!("\n๐Ÿงช html-generator Accessibility Examples\n"); + + aria_attribute_error_example()?; + wcag_validation_error_example()?; + html_processing_error_example()?; + html_too_large_error_example()?; + malformed_html_error_example()?; + + println!("\n๐ŸŽ‰ All accessibility examples completed successfully!"); + + Ok(()) +} + +/// Demonstrates handling of invalid ARIA attribute errors. +fn aria_attribute_error_example() -> Result<(), AccessibilityError> { + println!("๐Ÿฆ€ Invalid ARIA Attribute Example"); + println!("---------------------------------------------"); + + let invalid_html = + r#"
Content
"#; + let result = add_aria_attributes(invalid_html); + + match result { + Ok(_) => { + println!( + " โŒ Unexpected success in adding ARIA attributes" + ) + } + Err(e) => { + println!( + " โœ… Successfully caught Invalid ARIA Attribute Error: {}", + e + ); + } + } + + Ok(()) +} + +/// Demonstrates handling of WCAG validation errors. +fn wcag_validation_error_example() -> Result<(), AccessibilityError> { + println!("\n๐Ÿฆ€ WCAG Validation Error Example"); + println!("---------------------------------------------"); + + let invalid_html = r#""#; // Missing alt text + match validate_wcag(invalid_html) { + Ok(_) => { + println!( + " โŒ Unexpected success in passing WCAG validation" + ) + } + Err(e) => { + println!( + " โœ… Successfully caught WCAG Validation Error: {}", + e + ); + } + } + + Ok(()) +} + +/// Demonstrates handling of HTML processing errors. +fn html_processing_error_example() -> Result<(), AccessibilityError> { + println!("\n๐Ÿฆ€ HTML Processing Error Example"); + println!("---------------------------------------------"); + + let malformed_html = "
"; + + // Map the error from `add_aria_attributes` to `HtmlError::AccessibilityError` + let updated_html = add_aria_attributes(html) + .map_err(|e| HtmlError::AccessibilityError(e.to_string()))?; + + println!("Updated HTML with ARIA attributes: \n{}", updated_html); + + Ok(()) +} + +/// Demonstrates performance optimization by minifying HTML and asynchronous generation. +async fn performance_optimization_example() -> Result<()> { + println!("\n๐Ÿฆ€ Performance Optimization Example"); + println!("---------------------------------------------"); + + let markdown = "# Performance matters!"; + let html = async_generate_html(markdown).await?; + println!("Generated HTML: \n{}", html); + + Ok(()) +} + +/// Demonstrates SEO optimization by generating meta tags and structured data. +fn seo_optimization_example() -> Result<()> { + println!("\n๐Ÿฆ€ SEO Optimization Example"); + println!("---------------------------------------------"); + + let html = "

Example Article

This is an example article for SEO optimization.

"; + + // Use a closure to convert the error type to HtmlError::SeoError, which expects a String + let meta_tags = generate_meta_tags(html) + .map_err(|e| HtmlError::SeoError(e.to_string()))?; + let structured_data = generate_structured_data(html) + .map_err(|e| HtmlError::SeoError(e.to_string()))?; + + println!("Generated Meta Tags: \n{}", meta_tags); + println!("Generated Structured Data: \n{}", structured_data); + + Ok(()) +} diff --git a/examples/performance_example.rs b/examples/performance_example.rs new file mode 100644 index 0000000..ed61758 --- /dev/null +++ b/examples/performance_example.rs @@ -0,0 +1,70 @@ +// src/examples/performance_example.rs + +#![allow(missing_docs)] + +use html_generator::performance::{ + async_generate_html, generate_html, minify_html, +}; +use html_generator::HtmlError; +use std::path::Path; + +#[tokio::main] +async fn main() -> Result<(), HtmlError> { + println!("\n๐Ÿงช html-generator Performance Examples\n"); + + minify_html_example()?; + async_generate_html_example().await?; + generate_html_example()?; + + println!("\n๐ŸŽ‰ All performance examples completed successfully!"); + + Ok(()) +} + +/// Demonstrates the use of the `minify_html` function. +fn minify_html_example() -> Result<(), HtmlError> { + println!("๐Ÿฆ€ Minify HTML Example"); + println!("---------------------------------------------"); + + let path = Path::new("index.html"); + + // Attempt to minify the HTML file + match minify_html(path) { + Ok(minified_html) => { + println!("Minified HTML: \n{}", minified_html); + } + Err(e) => { + println!("Failed to minify HTML: {}", e); + } + } + + Ok(()) +} + +/// Demonstrates the asynchronous generation of HTML from Markdown. +async fn async_generate_html_example() -> Result<(), HtmlError> { + println!("\n๐Ÿฆ€ Async Generate HTML Example"); + println!("---------------------------------------------"); + + let markdown = "# Hello\n\nThis is an async test."; + match async_generate_html(markdown).await { + Ok(html) => println!("Generated HTML: \n{}", html), + Err(e) => eprintln!("Error: {}", e), + } + + Ok(()) +} + +/// Demonstrates the synchronous generation of HTML from Markdown. +fn generate_html_example() -> Result<(), HtmlError> { + println!("\n๐Ÿฆ€ Generate HTML Example"); + println!("---------------------------------------------"); + + let markdown = "# Hello\n\nThis is a test."; + match generate_html(markdown) { + Ok(html) => println!("Generated HTML: \n{}", html), + Err(e) => eprintln!("Error: {}", e), + } + + Ok(()) +} diff --git a/examples/seo_example.rs b/examples/seo_example.rs new file mode 100644 index 0000000..abf8062 --- /dev/null +++ b/examples/seo_example.rs @@ -0,0 +1,58 @@ +// src/examples/seo_example.rs + +#![allow(missing_docs)] + +use html_generator::seo::{ + generate_meta_tags, generate_structured_data, +}; +use html_generator::HtmlError; + +fn main() -> Result<(), HtmlError> { + println!("\n๐Ÿงช html-generator SEO Examples\n"); + + generate_meta_tags_example()?; + generate_structured_data_example()?; + + println!("\n๐ŸŽ‰ All SEO examples completed successfully!"); + + Ok(()) +} + +/// Demonstrates the generation of meta tags for SEO purposes. +fn generate_meta_tags_example() -> Result<(), HtmlError> { + println!("๐Ÿฆ€ Generate Meta Tags Example"); + println!("---------------------------------------------"); + + let html = r#"Test Page

This is a test page.

"#; + match generate_meta_tags(html) { + Ok(meta_tags) => { + println!("Generated Meta Tags: \n{}", meta_tags); + } + Err(e) => { + println!("Failed to generate meta tags: {}", e); + } + } + + Ok(()) +} + +/// Demonstrates the generation of structured data (JSON-LD) for SEO purposes. +fn generate_structured_data_example() -> Result<(), HtmlError> { + println!("\n๐Ÿฆ€ Generate Structured Data Example"); + println!("---------------------------------------------"); + + let html = r#"Test Page

This is a test page.

"#; + match generate_structured_data(html) { + Ok(structured_data) => { + println!( + "Generated Structured Data: \n{}", + structured_data + ); + } + Err(e) => { + println!("Failed to generate structured data: {}", e); + } + } + + Ok(()) +} diff --git a/examples/utils_example.rs b/examples/utils_example.rs new file mode 100644 index 0000000..71f8089 --- /dev/null +++ b/examples/utils_example.rs @@ -0,0 +1,76 @@ +// src/examples/utils_example.rs + +#![allow(missing_docs)] + +use html_generator::utils::{ + extract_front_matter, format_header_with_id_class, + generate_table_of_contents, +}; +use html_generator::HtmlError; + +fn main() -> Result<(), HtmlError> { + println!("\n๐Ÿงช html-generator Utils Examples\n"); + + extract_front_matter_example()?; + format_header_with_id_class_example()?; + generate_table_of_contents_example()?; + + println!("\n๐ŸŽ‰ All utils examples completed successfully!"); + + Ok(()) +} + +/// Demonstrates extracting front matter from Markdown content. +fn extract_front_matter_example() -> Result<(), HtmlError> { + println!("๐Ÿฆ€ Extract Front Matter Example"); + println!("---------------------------------------------"); + + let content = + "---\ntitle: My Page\n---\n# Hello, world!\n\nThis is a test."; + match extract_front_matter(content) { + Ok(remaining_content) => { + println!("Remaining Content: \n{}", remaining_content); + } + Err(e) => { + println!("Failed to extract front matter: {}", e); + } + } + + Ok(()) +} + +/// Demonstrates formatting a header with an ID and class. +fn format_header_with_id_class_example() -> Result<(), HtmlError> { + println!("\n๐Ÿฆ€ Format Header with ID and Class Example"); + println!("---------------------------------------------"); + + let header = "

Hello, World!

"; + match format_header_with_id_class(header, None, None) { + Ok(formatted_header) => { + println!("Formatted Header: \n{}", formatted_header); + } + Err(e) => { + println!("Failed to format header: {}", e); + } + } + + Ok(()) +} + +/// Demonstrates generating a table of contents from HTML content. +fn generate_table_of_contents_example() -> Result<(), HtmlError> { + println!("\n๐Ÿฆ€ Generate Table of Contents Example"); + println!("---------------------------------------------"); + + let html = "

Title

Some content

Subtitle

More content

Sub-subtitle

"; + match generate_table_of_contents(html) { + Ok(toc) => { + println!("Generated Table of Contents: \n{}", toc); + } + Err(e) => { + println!("Failed to generate table of contents: {}", e); + } + } + + Ok(()) +} diff --git a/src/error.rs b/src/error.rs index f68d256..ba1545d 100644 --- a/src/error.rs +++ b/src/error.rs @@ -127,6 +127,10 @@ pub enum HtmlError { /// This variant is used for errors that do not fit into other categories. #[error("Unexpected error: {0}")] UnexpectedError(String), + + /// An SEO-related error. + #[error("SEO error: {0}")] + SeoError(String), } /// Type alias for a result using the `HtmlError` error type. diff --git a/src/seo.rs b/src/seo.rs index 10c6ee9..6ed90d8 100644 --- a/src/seo.rs +++ b/src/seo.rs @@ -90,15 +90,8 @@ pub fn generate_meta_tags(html: &str) -> Result { let title = extract_title(&document)?; let description = extract_description(&document)?; - - // Ensure that escape_html is applied only once - println!("Original title: {}", title); let escaped_title = escape_html(&title); - println!("Escaped title: {}", escaped_title); - - println!("Original description: {}", description); let escaped_description = escape_html(&description); - println!("Escaped description: {}", escaped_description); meta_tags.push_str(&format!( r#""#, diff --git a/src/utils.rs b/src/utils.rs index bbf14f4..9c13841 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -62,7 +62,6 @@ const MAX_INPUT_SIZE: usize = 1_000_000; // 1 MB /// let result = extract_front_matter(content).unwrap(); /// assert_eq!(result, "# Hello, world!\n\nThis is a test."); /// ``` -#[must_use] pub fn extract_front_matter(content: &str) -> Result { if content.is_empty() { return Err(HtmlError::InvalidInput("Empty input".to_string())); @@ -114,7 +113,6 @@ pub fn extract_front_matter(content: &str) -> Result { /// let result = format_header_with_id_class(header, None, None).unwrap(); /// assert_eq!(result, "

Hello, World!

"); /// ``` -#[must_use] pub fn format_header_with_id_class( header: &str, id_generator: Option String>, @@ -167,7 +165,6 @@ pub fn format_header_with_id_class( /// let result = generate_table_of_contents(html).unwrap(); /// assert_eq!(result, r#""#); /// ``` -#[must_use] pub fn generate_table_of_contents(html: &str) -> Result { if html.is_empty() { return Err(HtmlError::InvalidInput("Empty input".to_string()));