refactor: consolidate escape functions and extract weight constants
- Create escape.rs with shared html_escape, html_escape_into, xml_escape - Remove duplicate implementations from render.rs, highlight.rs, feed.rs, sitemap.rs - Add DEFAULT_WEIGHT (50) and DEFAULT_WEIGHT_HIGH (99) constants to content.rs - Replace all magic number weight defaults with named constants No functional changes; all 67 tests pass.
This commit is contained in:
@@ -6,6 +6,12 @@ use serde::Serialize;
|
|||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Default weight for content items in navigation and listings.
|
||||||
|
pub const DEFAULT_WEIGHT: i64 = 50;
|
||||||
|
|
||||||
|
/// High default weight for content that should appear last (e.g., projects).
|
||||||
|
pub const DEFAULT_WEIGHT_HIGH: i64 = 99;
|
||||||
|
|
||||||
/// The type of content being processed.
|
/// The type of content being processed.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum ContentKind {
|
pub enum ContentKind {
|
||||||
@@ -212,7 +218,7 @@ pub fn discover_nav(content_dir: &Path) -> Result<Vec<NavItem>> {
|
|||||||
.nav_label
|
.nav_label
|
||||||
.unwrap_or(content.frontmatter.title),
|
.unwrap_or(content.frontmatter.title),
|
||||||
path: format!("/{}.html", slug),
|
path: format!("/{}.html", slug),
|
||||||
weight: content.frontmatter.weight.unwrap_or(50),
|
weight: content.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -245,7 +251,7 @@ pub fn discover_nav(content_dir: &Path) -> Result<Vec<NavItem>> {
|
|||||||
.map(|item| NavItem {
|
.map(|item| NavItem {
|
||||||
label: item.frontmatter.nav_label.unwrap_or(item.frontmatter.title),
|
label: item.frontmatter.nav_label.unwrap_or(item.frontmatter.title),
|
||||||
path: format!("/{}/{}.html", dir_name, item.slug),
|
path: format!("/{}/{}.html", dir_name, item.slug),
|
||||||
weight: item.frontmatter.weight.unwrap_or(50),
|
weight: item.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT),
|
||||||
children: Vec::new(),
|
children: Vec::new(),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@@ -260,7 +266,7 @@ pub fn discover_nav(content_dir: &Path) -> Result<Vec<NavItem>> {
|
|||||||
.nav_label
|
.nav_label
|
||||||
.unwrap_or(content.frontmatter.title),
|
.unwrap_or(content.frontmatter.title),
|
||||||
path: format!("/{}/index.html", dir_name),
|
path: format!("/{}/index.html", dir_name),
|
||||||
weight: content.frontmatter.weight.unwrap_or(50),
|
weight: content.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT),
|
||||||
children,
|
children,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -358,8 +364,8 @@ pub fn discover_sections(content_dir: &Path) -> Result<Vec<Section>> {
|
|||||||
|
|
||||||
// Sort by weight
|
// Sort by weight
|
||||||
sections.sort_by(|a, b| {
|
sections.sort_by(|a, b| {
|
||||||
let wa = a.index.frontmatter.weight.unwrap_or(50);
|
let wa = a.index.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT);
|
||||||
let wb = b.index.frontmatter.weight.unwrap_or(50);
|
let wb = b.index.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT);
|
||||||
wa.cmp(&wb)
|
wa.cmp(&wb)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
63
src/escape.rs
Normal file
63
src/escape.rs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
//! Text escaping utilities for HTML and XML output.
|
||||||
|
|
||||||
|
/// Escape HTML special characters for safe embedding in HTML content.
|
||||||
|
///
|
||||||
|
/// Escapes: `&`, `<`, `>`, `"`
|
||||||
|
pub fn html_escape(s: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(s.len());
|
||||||
|
html_escape_into(&mut result, s);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Escape HTML characters into an existing string.
|
||||||
|
///
|
||||||
|
/// This is more efficient when building output incrementally.
|
||||||
|
pub fn html_escape_into(out: &mut String, s: &str) {
|
||||||
|
for c in s.chars() {
|
||||||
|
match c {
|
||||||
|
'&' => out.push_str("&"),
|
||||||
|
'<' => out.push_str("<"),
|
||||||
|
'>' => out.push_str(">"),
|
||||||
|
'"' => out.push_str("""),
|
||||||
|
_ => out.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Escape XML special characters for safe embedding in XML documents.
|
||||||
|
///
|
||||||
|
/// Escapes: `&`, `<`, `>`, `"`, `'`
|
||||||
|
pub fn xml_escape(s: &str) -> String {
|
||||||
|
s.replace('&', "&")
|
||||||
|
.replace('<', "<")
|
||||||
|
.replace('>', ">")
|
||||||
|
.replace('"', """)
|
||||||
|
.replace('\'', "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_html_escape() {
|
||||||
|
assert_eq!(html_escape("Hello & World"), "Hello & World");
|
||||||
|
assert_eq!(html_escape("<tag>"), "<tag>");
|
||||||
|
assert_eq!(html_escape("\"quoted\""), ""quoted"");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_html_escape_into() {
|
||||||
|
let mut buf = String::new();
|
||||||
|
html_escape_into(&mut buf, "a < b");
|
||||||
|
assert_eq!(buf, "a < b");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_xml_escape() {
|
||||||
|
assert_eq!(xml_escape("Hello & World"), "Hello & World");
|
||||||
|
assert_eq!(xml_escape("<tag>"), "<tag>");
|
||||||
|
assert_eq!(xml_escape("\"quoted\""), ""quoted"");
|
||||||
|
assert_eq!(xml_escape("it's"), "it's");
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/feed.rs
10
src/feed.rs
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::config::SiteConfig;
|
use crate::config::SiteConfig;
|
||||||
use crate::content::SiteManifest;
|
use crate::content::SiteManifest;
|
||||||
|
use crate::escape::xml_escape;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// Generate an Atom 1.0 feed from blog posts in the manifest.
|
/// Generate an Atom 1.0 feed from blog posts in the manifest.
|
||||||
@@ -80,15 +81,6 @@ pub fn generate_atom_feed(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Escape XML special characters.
|
|
||||||
fn xml_escape(s: &str) -> String {
|
|
||||||
s.replace('&', "&")
|
|
||||||
.replace('<', "<")
|
|
||||||
.replace('>', ">")
|
|
||||||
.replace('"', """)
|
|
||||||
.replace('\'', "'")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use std::collections::HashMap;
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use crate::escape::{html_escape, html_escape_into};
|
||||||
use ropey::RopeSlice;
|
use ropey::RopeSlice;
|
||||||
use tree_house::highlighter::{Highlight, HighlightEvent, Highlighter};
|
use tree_house::highlighter::{Highlight, HighlightEvent, Highlighter};
|
||||||
use tree_house::{
|
use tree_house::{
|
||||||
@@ -624,26 +625,6 @@ fn render_html<'a>(source: &str, mut highlighter: Highlighter<'a, 'a, SukrLoader
|
|||||||
html
|
html
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Simple HTML escape for fallback.
|
|
||||||
fn html_escape(s: &str) -> String {
|
|
||||||
let mut result = String::with_capacity(s.len());
|
|
||||||
html_escape_into(&mut result, s);
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Escape HTML characters into an existing string.
|
|
||||||
fn html_escape_into(out: &mut String, s: &str) {
|
|
||||||
for c in s.chars() {
|
|
||||||
match c {
|
|
||||||
'&' => out.push_str("&"),
|
|
||||||
'<' => out.push_str("<"),
|
|
||||||
'>' => out.push_str(">"),
|
|
||||||
'"' => out.push_str("""),
|
|
||||||
_ => out.push(c),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
11
src/main.rs
11
src/main.rs
@@ -6,6 +6,7 @@ mod config;
|
|||||||
mod content;
|
mod content;
|
||||||
mod css;
|
mod css;
|
||||||
mod error;
|
mod error;
|
||||||
|
mod escape;
|
||||||
mod feed;
|
mod feed;
|
||||||
mod highlight;
|
mod highlight;
|
||||||
mod math;
|
mod math;
|
||||||
@@ -14,7 +15,7 @@ mod render;
|
|||||||
mod sitemap;
|
mod sitemap;
|
||||||
mod template_engine;
|
mod template_engine;
|
||||||
|
|
||||||
use crate::content::{Content, ContentKind, NavItem};
|
use crate::content::{Content, ContentKind, DEFAULT_WEIGHT, DEFAULT_WEIGHT_HIGH, NavItem};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
use crate::template_engine::{ContentContext, TemplateEngine};
|
use crate::template_engine::{ContentContext, TemplateEngine};
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@@ -123,8 +124,8 @@ fn run(config_path: &Path) -> Result<()> {
|
|||||||
items.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
a.frontmatter
|
a.frontmatter
|
||||||
.weight
|
.weight
|
||||||
.unwrap_or(99)
|
.unwrap_or(DEFAULT_WEIGHT_HIGH)
|
||||||
.cmp(&b.frontmatter.weight.unwrap_or(99))
|
.cmp(&b.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT_HIGH))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
@@ -132,8 +133,8 @@ fn run(config_path: &Path) -> Result<()> {
|
|||||||
items.sort_by(|a, b| {
|
items.sort_by(|a, b| {
|
||||||
a.frontmatter
|
a.frontmatter
|
||||||
.weight
|
.weight
|
||||||
.unwrap_or(50)
|
.unwrap_or(DEFAULT_WEIGHT)
|
||||||
.cmp(&b.frontmatter.weight.unwrap_or(50))
|
.cmp(&b.frontmatter.weight.unwrap_or(DEFAULT_WEIGHT))
|
||||||
.then_with(|| a.frontmatter.title.cmp(&b.frontmatter.title))
|
.then_with(|| a.frontmatter.title.cmp(&b.frontmatter.title))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
//! Markdown to HTML rendering via pulldown-cmark with syntax highlighting.
|
//! Markdown to HTML rendering via pulldown-cmark with syntax highlighting.
|
||||||
|
|
||||||
|
use crate::escape::html_escape;
|
||||||
use crate::highlight::{highlight_code, Language};
|
use crate::highlight::{highlight_code, Language};
|
||||||
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
@@ -247,13 +248,6 @@ pub fn markdown_to_html(markdown: &str) -> (String, Vec<Anchor>) {
|
|||||||
(html_output, anchors)
|
(html_output, anchors)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn html_escape(s: &str) -> String {
|
|
||||||
s.replace('&', "&")
|
|
||||||
.replace('<', "<")
|
|
||||||
.replace('>', ">")
|
|
||||||
.replace('"', """)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert heading text to a URL-friendly slug ID.
|
/// Convert heading text to a URL-friendly slug ID.
|
||||||
fn slugify(text: &str) -> String {
|
fn slugify(text: &str) -> String {
|
||||||
text.to_lowercase()
|
text.to_lowercase()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use crate::config::SiteConfig;
|
use crate::config::SiteConfig;
|
||||||
use crate::content::SiteManifest;
|
use crate::content::SiteManifest;
|
||||||
|
use crate::escape::xml_escape;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
/// A URL entry for the sitemap.
|
/// A URL entry for the sitemap.
|
||||||
@@ -88,15 +89,6 @@ fn build_sitemap_xml(entries: &[SitemapEntry]) -> String {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Escape XML special characters.
|
|
||||||
fn xml_escape(s: &str) -> String {
|
|
||||||
s.replace('&', "&")
|
|
||||||
.replace('<', "<")
|
|
||||||
.replace('>', ">")
|
|
||||||
.replace('"', """)
|
|
||||||
.replace('\'', "'")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user