diff --git a/my-blog/static/css/style.css b/my-blog/static/css/style.css index 45598a7..ceb791c 100644 --- a/my-blog/static/css/style.css +++ b/my-blog/static/css/style.css @@ -1185,3 +1185,158 @@ article.article-content { } } +/* Image Comparison Slider Styles */ +.img-comparison-container { + position: relative; + width: 100%; + max-width: 800px; + margin: 20px auto; + overflow: hidden; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1); +} + +.img-comparison-slider { + position: relative; + width: 100%; + height: 400px; + overflow: hidden; + cursor: pointer; +} + +.img-before, +.img-after { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + overflow: hidden; +} + +.img-before { + z-index: 2; + clip-path: inset(0 50% 0 0); +} + +.img-after { + z-index: 1; +} + +.img-before img, +.img-after img { + width: 100%; + height: 100%; + object-fit: cover; + user-select: none; + pointer-events: none; + position: absolute; + top: 0; + left: 0; +} + +.slider { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: transparent; + outline: none; + cursor: pointer; + z-index: 4; + opacity: 0; + -webkit-appearance: none; + appearance: none; +} + +.slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 0; + height: 0; +} + +.slider::-moz-range-thumb { + width: 0; + height: 0; + border: none; + background: transparent; +} + +.slider-thumb { + position: absolute; + top: 0; + left: 50%; + width: 4px; + height: 100%; + background: #ffffff; + z-index: 3; + pointer-events: none; + box-shadow: 0 0 8px rgba(0, 0, 0, 0.3); + transform: translateX(-50%); +} + +.slider-thumb::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 40px; + height: 40px; + background: #ffffff; + border: 2px solid var(--theme-color); + border-radius: 50%; + transform: translate(-50%, -50%); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.slider-thumb::after { + content: '↔'; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + color: var(--theme-color); + font-size: 16px; + font-weight: bold; + z-index: 1; +} + + +/* Responsive design */ +@media (max-width: 768px) { + .img-comparison-container { + margin: 15px auto; + border-radius: 6px; + } + + .img-comparison-slider { + height: 250px; + } + + .slider-thumb::before { + width: 32px; + height: 32px; + } + + .slider-thumb::after { + font-size: 14px; + } +} + +@media (max-width: 480px) { + .img-comparison-slider { + height: 200px; + } + + .slider-thumb::before { + width: 28px; + height: 28px; + } + + .slider-thumb::after { + font-size: 12px; + } +} + diff --git a/my-blog/static/img/ue_blender_model_ai_v0401.png b/my-blog/static/img/ue_blender_model_ai_v0401.png new file mode 100644 index 0000000..351e4e2 Binary files /dev/null and b/my-blog/static/img/ue_blender_model_ai_v0401.png differ diff --git a/my-blog/static/img/ue_blender_model_ai_v0402.png b/my-blog/static/img/ue_blender_model_ai_v0402.png new file mode 100644 index 0000000..4b2d211 Binary files /dev/null and b/my-blog/static/img/ue_blender_model_ai_v0402.png differ diff --git a/my-blog/static/img/ue_blender_model_ai_v0501.png b/my-blog/static/img/ue_blender_model_ai_v0501.png new file mode 100644 index 0000000..ce61e48 Binary files /dev/null and b/my-blog/static/img/ue_blender_model_ai_v0501.png differ diff --git a/my-blog/static/img/ue_blender_model_ai_v0502.png b/my-blog/static/img/ue_blender_model_ai_v0502.png new file mode 100644 index 0000000..19f01e3 Binary files /dev/null and b/my-blog/static/img/ue_blender_model_ai_v0502.png differ diff --git a/my-blog/static/js/image-comparison.js b/my-blog/static/js/image-comparison.js new file mode 100644 index 0000000..f625153 --- /dev/null +++ b/my-blog/static/js/image-comparison.js @@ -0,0 +1,123 @@ +/** + * Image Comparison Slider + * UE5-style before/after image comparison component + */ + +class ImageComparison { + constructor(container) { + this.container = container; + this.slider = container.querySelector('.slider'); + this.beforeImg = container.querySelector('.img-before'); + this.afterImg = container.querySelector('.img-after'); + this.sliderThumb = container.querySelector('.slider-thumb'); + + this.isDragging = false; + this.containerRect = null; + + this.init(); + } + + init() { + this.bindEvents(); + this.updatePosition(50); // Start at 50% + } + + bindEvents() { + // Mouse events + this.slider.addEventListener('input', (e) => { + this.updatePosition(e.target.value); + }); + + this.slider.addEventListener('mousedown', () => { + this.isDragging = true; + document.body.style.userSelect = 'none'; + }); + + document.addEventListener('mouseup', () => { + if (this.isDragging) { + this.isDragging = false; + document.body.style.userSelect = ''; + } + }); + + // Touch events for mobile + this.slider.addEventListener('touchstart', (e) => { + this.isDragging = true; + e.preventDefault(); + }); + + this.slider.addEventListener('touchmove', (e) => { + if (this.isDragging) { + const touch = e.touches[0]; + this.containerRect = this.container.getBoundingClientRect(); + const x = touch.clientX - this.containerRect.left; + const percentage = Math.max(0, Math.min(100, (x / this.containerRect.width) * 100)); + this.slider.value = percentage; + this.updatePosition(percentage); + e.preventDefault(); + } + }); + + this.slider.addEventListener('touchend', () => { + this.isDragging = false; + }); + + // Direct click on container + this.container.addEventListener('click', (e) => { + if (e.target === this.container || e.target.classList.contains('img-comparison-slider')) { + this.containerRect = this.container.getBoundingClientRect(); + const x = e.clientX - this.containerRect.left; + const percentage = Math.max(0, Math.min(100, (x / this.containerRect.width) * 100)); + this.slider.value = percentage; + this.updatePosition(percentage); + } + }); + + // Keyboard support + this.slider.addEventListener('keydown', (e) => { + let value = parseFloat(this.slider.value); + switch (e.key) { + case 'ArrowLeft': + value = Math.max(0, value - 1); + break; + case 'ArrowRight': + value = Math.min(100, value + 1); + break; + case 'Home': + value = 0; + break; + case 'End': + value = 100; + break; + default: + return; + } + e.preventDefault(); + this.slider.value = value; + this.updatePosition(value); + }); + } + + updatePosition(percentage) { + const position = parseFloat(percentage); + + // Update clip-path for before image to show only the left portion + this.beforeImg.style.clipPath = `inset(0 ${100 - position}% 0 0)`; + + // Update slider thumb position + this.sliderThumb.style.left = `${position}%`; + this.sliderThumb.style.transform = `translateX(-50%)`; + + } +} + +// Auto-initialize all image comparison components +document.addEventListener('DOMContentLoaded', function() { + const comparisons = document.querySelectorAll('.img-comparison-container'); + comparisons.forEach(container => { + new ImageComparison(container); + }); +}); + +// Export for manual initialization +window.ImageComparison = ImageComparison; \ No newline at end of file diff --git a/my-blog/templates/base.html b/my-blog/templates/base.html index d666b2b..d4c0030 100644 --- a/my-blog/templates/base.html +++ b/my-blog/templates/base.html @@ -114,6 +114,7 @@ + {% include "oauth-assets.html" %} diff --git a/src/lib.rs b/src/lib.rs index a32bd75..6d908c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,6 +7,7 @@ pub mod config; pub mod doc_generator; pub mod generator; pub mod markdown; +pub mod shortcode; pub mod mcp; pub mod oauth; // pub mod ollama_proxy; // Temporarily disabled - uses actix-web instead of axum diff --git a/src/main.rs b/src/main.rs index d202a4c..db19035 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,7 @@ mod commands; mod doc_generator; mod generator; mod markdown; +mod shortcode; mod template; mod oauth; mod translator; diff --git a/src/markdown.rs b/src/markdown.rs index e6e211b..87db536 100644 --- a/src/markdown.rs +++ b/src/markdown.rs @@ -6,12 +6,14 @@ use syntect::html::{styled_line_to_highlighted_html, IncludeBackground}; use gray_matter::Matter; use gray_matter::engine::YAML; use serde_json::Value; +use crate::shortcode::ShortcodeProcessor; pub struct MarkdownProcessor { highlight_code: bool, highlight_theme: String, syntax_set: SyntaxSet, theme_set: ThemeSet, + shortcode_processor: ShortcodeProcessor, } impl MarkdownProcessor { @@ -21,6 +23,7 @@ impl MarkdownProcessor { highlight_theme: highlight_theme.unwrap_or_else(|| "Monokai".to_string()), syntax_set: SyntaxSet::load_defaults_newlines(), theme_set: ThemeSet::load_defaults(), + shortcode_processor: ShortcodeProcessor::new(), } } @@ -68,6 +71,9 @@ impl MarkdownProcessor { pub fn render(&self, content: &str) -> Result { + // Process shortcodes first + let processed_content = self.shortcode_processor.process(content); + let mut options = Options::empty(); options.insert(Options::ENABLE_STRIKETHROUGH); options.insert(Options::ENABLE_TABLES); @@ -75,15 +81,21 @@ impl MarkdownProcessor { options.insert(Options::ENABLE_TASKLISTS); if self.highlight_code { - self.render_with_syntax_highlighting(content, options) + self.render_with_syntax_highlighting(&processed_content, options) } else { - let parser = Parser::new_ext(content, options); + let parser = Parser::new_ext(&processed_content, options); let mut html_output = String::new(); html::push_html(&mut html_output, parser); Ok(html_output) } } + /// Provide access to the shortcode processor for custom shortcode registration + #[allow(dead_code)] + pub fn shortcode_processor_mut(&mut self) -> &mut ShortcodeProcessor { + &mut self.shortcode_processor + } + fn render_with_syntax_highlighting(&self, content: &str, options: Options) -> Result { let parser = Parser::new_ext(content, options); let mut html_output = String::new(); diff --git a/src/shortcode.rs b/src/shortcode.rs new file mode 100644 index 0000000..7844eb2 --- /dev/null +++ b/src/shortcode.rs @@ -0,0 +1,192 @@ +use regex::Regex; +use std::collections::HashMap; + +pub struct ShortcodeProcessor { + shortcodes: HashMap String + Send + Sync>>, +} + +impl ShortcodeProcessor { + pub fn new() -> Self { + let mut processor = Self { + shortcodes: HashMap::new(), + }; + + // Register built-in shortcodes + processor.register_img_compare(); + + processor + } + + fn register_img_compare(&mut self) { + self.shortcodes.insert( + "img-compare".to_string(), + Box::new(|attrs| Self::parse_img_compare_shortcode(attrs)), + ); + } + + pub fn process(&self, content: &str) -> String { + let mut processed = content.to_string(); + + // Process {{< shortcode >}} format (Hugo-style) + let hugo_regex = Regex::new(r#"\{\{\<\s*(\w+(?:-\w+)*)\s+([^>]*)\s*\>\}\}"#).unwrap(); + processed = hugo_regex.replace_all(&processed, |caps: ®ex::Captures| { + let shortcode_name = &caps[1]; + let attrs = &caps[2]; + + if let Some(handler) = self.shortcodes.get(shortcode_name) { + handler(attrs) + } else { + caps[0].to_string() // Return original if shortcode not found + } + }).to_string(); + + // Process [shortcode] format (Bracket-style) + let bracket_regex = Regex::new(r#"\[(\w+(?:-\w+)*)\s+([^\]]*)\]"#).unwrap(); + processed = bracket_regex.replace_all(&processed, |caps: ®ex::Captures| { + let shortcode_name = &caps[1]; + let attrs = &caps[2]; + + if let Some(handler) = self.shortcodes.get(shortcode_name) { + handler(attrs) + } else { + caps[0].to_string() // Return original if shortcode not found + } + }).to_string(); + + processed + } + + fn parse_attributes(attrs: &str) -> HashMap { + let attr_regex = Regex::new(r#"(\w+(?:-\w+)*)=(?:"([^"]*)"|'([^']*)'|([^\s]+))"#).unwrap(); + let mut attributes = HashMap::new(); + + for caps in attr_regex.captures_iter(attrs) { + let key = caps.get(1).unwrap().as_str().to_string(); + let value = caps.get(2).or(caps.get(3)).or(caps.get(4)).unwrap().as_str().to_string(); + attributes.insert(key, value); + } + + attributes + } + + fn parse_img_compare_shortcode(attrs: &str) -> String { + let attributes = Self::parse_attributes(attrs); + + let before = attributes.get("before").map(|s| s.as_str()).unwrap_or(""); + let after = attributes.get("after").map(|s| s.as_str()).unwrap_or(""); + let before_caption = attributes.get("before-caption") + .or(attributes.get("before-alt")) + .map(|s| s.as_str()) + .unwrap_or("Before"); + let after_caption = attributes.get("after-caption") + .or(attributes.get("after-alt")) + .map(|s| s.as_str()) + .unwrap_or("After"); + let width = attributes.get("width").map(|s| s.as_str()).unwrap_or("1000"); + let height = attributes.get("height").map(|s| s.as_str()).unwrap_or("400"); + let alt = attributes.get("alt").map(|s| s.as_str()).unwrap_or(""); + + let alt_suffix = if !alt.is_empty() { + format!(" | {}", alt) + } else { + String::new() + }; + + format!(r#" +
+
+
+ {}{} +
+
+ {}{} +
+ +
+
+
+
+
"#, + height, + before, before_caption, alt_suffix, width, + after, after_caption, alt_suffix, width + ) + } + + /// Register a custom shortcode handler + #[allow(dead_code)] + pub fn register_shortcode(&mut self, name: &str, handler: F) + where + F: Fn(&str) -> String + Send + Sync + 'static, + { + self.shortcodes.insert(name.to_string(), Box::new(handler)); + } + + /// Get list of registered shortcodes + #[allow(dead_code)] + pub fn get_shortcode_names(&self) -> Vec<&String> { + self.shortcodes.keys().collect() + } +} + +impl Default for ShortcodeProcessor { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_img_compare_hugo_style() { + let processor = ShortcodeProcessor::new(); + let input = r#"{{< img-compare before="/before.jpg" after="/after.jpg" >}}"#; + let result = processor.process(input); + + assert!(result.contains("img-comparison-container")); + assert!(result.contains("/before.jpg")); + assert!(result.contains("/after.jpg")); + } + + #[test] + fn test_img_compare_bracket_style() { + let processor = ShortcodeProcessor::new(); + let input = r#"[img-compare before="/before.jpg" after="/after.jpg"]"#; + let result = processor.process(input); + + assert!(result.contains("img-comparison-container")); + assert!(result.contains("/before.jpg")); + assert!(result.contains("/after.jpg")); + } + + #[test] + fn test_custom_shortcode() { + let mut processor = ShortcodeProcessor::new(); + processor.register_shortcode("test", |_| "
test
".to_string()); + + let input = "{{< test >}}"; + let result = processor.process(input); + + assert_eq!(result, "
test
"); + } + + #[test] + fn test_unknown_shortcode() { + let processor = ShortcodeProcessor::new(); + let input = "{{< unknown attr=\"value\" >}}"; + let result = processor.process(input); + + assert_eq!(result, input); // Should return original + } + + #[test] + fn test_attribute_parsing() { + let attributes = ShortcodeProcessor::parse_attributes(r#"before="/test.jpg" after='test2.jpg' width=800"#); + + assert_eq!(attributes.get("before").unwrap(), "/test.jpg"); + assert_eq!(attributes.get("after").unwrap(), "test2.jpg"); + assert_eq!(attributes.get("width").unwrap(), "800"); + } +} \ No newline at end of file