render.rs (4999B)
1 use crate::post::{File, Post}; 2 3 /// Renders a single post to an HTML fragment string. 4 /// If download_files or download_thumbnails is true, the links will be converted to local paths 5 pub fn render_post(post: &Post, download_files: bool, download_thumbnails: bool) -> String { 6 let mut html = format!("<div class=\"post\" id=\"post{}\">\n", post.id); 7 8 html.push_str(" <div class=\"post-head\">\n"); 9 10 // Subject 11 if let Some(ref subject) = post.subject { 12 html.push_str(&format!( 13 " <span class=\"post-subject\">{}</span>\n", 14 html_escape(subject) 15 )); 16 } 17 18 // Name /w mailto/sage 19 let name = post.name.as_deref().unwrap_or("Аноним"); 20 let name_display = if let Some(ref mailto) = post.mailto { 21 format!("[{}] {}", mailto, name) 22 } else { 23 name.to_string() 24 }; 25 html.push_str(&format!( 26 " <span class=\"post-name\">{}</span>\n", 27 html_escape(&name_display) 28 )); 29 30 // Time, num, id 31 html.push_str(&format!(" <span class=\"post-time\">{}</span>\n", html_escape(&post.time))); 32 html.push_str(&format!(" <span class=\"post-num\">{}</span>\n", html_escape(&post.num))); 33 html.push_str(&format!( 34 " <span class=\"post-id\"><a href=\"#post{0}\">№{0}</a></span>\n", 35 post.id 36 )); 37 38 html.push_str(" </div>\n"); 39 40 // Images 41 html.push_str(&render_images(&post.files, download_files, download_thumbnails)); 42 43 // Body 44 html.push_str(" <div class=\"post-body\">\n"); 45 if !post.text.is_empty() { 46 html.push_str(" "); 47 html.push_str(&render_text_to_html(&post.text)); 48 html.push('\n'); 49 } 50 html.push_str(" </div>\n"); 51 52 html.push_str("</div>\n"); 53 html 54 } 55 56 fn html_escape(s: &str) -> String { 57 s.replace('&', "&") 58 .replace('<', "<") 59 .replace('>', ">") 60 .replace('"', """) 61 } 62 63 /// Converts plain post text to HTML. 64 /// - `>>id` → reply link anchor 65 /// - Lines starting with `>` (not `>>digit`) → greentext span 66 /// - `\n` → `<br>` 67 fn render_text_to_html(text: &str) -> String { 68 let needle = ">>"; 69 70 let lines: Vec<String> = text.split('\n').map(|line| { 71 let escaped = html_escape(line); 72 73 // Replace >>id with reply link anchors 74 let mut processed = String::with_capacity(escaped.len()); 75 let mut rest = escaped.as_str(); 76 while let Some(pos) = rest.find(needle) { 77 processed.push_str(&rest[..pos]); 78 let after = &rest[pos + needle.len()..]; 79 let digit_end = after.find(|c: char| !c.is_ascii_digit()).unwrap_or(after.len()); 80 if digit_end > 0 { 81 let id = &after[..digit_end]; 82 processed.push_str(&format!("<a href=\"#post{id}\" class=\"reply-link\">>>{id}</a>")); 83 rest = &after[digit_end..]; 84 } else { 85 processed.push_str(needle); 86 rest = after; 87 } 88 } 89 processed.push_str(rest); 90 91 // Wrap in greentext span if line starts with > but not >>digit 92 let is_greentext = escaped.starts_with(">") 93 && !escaped.strip_prefix(needle).is_some_and(|s| s.starts_with(|c: char| c.is_ascii_digit())); 94 if is_greentext { 95 format!("<span class=\"quote\">{processed}</span>") 96 } else { 97 processed 98 } 99 }).collect(); 100 101 lines.join("<br>\n") 102 } 103 104 fn render_images( 105 files: &[File], 106 download_files: bool, 107 download_thumbnails: bool, 108 ) -> String { 109 if files.is_empty() { 110 return String::new(); 111 } 112 113 let mut html = String::from(" <div class=\"post-images\">\n"); 114 for file in files { 115 let href = if download_files && !file.url.is_empty() { 116 format!("files/{}", file.url.split('/').last().unwrap_or("")) 117 } else { 118 file.url.clone() 119 }; 120 121 let thumb_filename = file.url_thumb.split('/').last().unwrap_or("").to_string(); 122 let img_src = if download_thumbnails && !file.url_thumb.is_empty() { 123 format!("thumb/{}", thumb_filename) 124 } else { 125 file.url_thumb.clone() 126 }; 127 128 html.push_str(&format!( 129 " <div class=\"post-image\">\n <a href=\"{}\" target=\"_blank\" title=\"{}\">\n <img src=\"{}\" alt=\"\" loading=\"lazy\">\n </a>\n <div class=\"post-image-info\">{} (<a href=\"{}\" target=\"_blank\" class=\"post-image-link\">o</a>, <a href=\"{}\" target=\"_blank\" class=\"post-image-link\">t</a>)</div>\n </div>\n", 130 html_escape(&href), 131 html_escape(&file.name_orig), 132 html_escape(&img_src), 133 html_escape(&file.name_orig), 134 html_escape(&file.url), 135 html_escape(&file.url_thumb), 136 )); 137 } 138 html.push_str(" </div>\n"); 139 html 140 }