arhivach-downloader

Download arhivach.vc threads
git clone https://git.ea.contact/arhivach-downloader
Log | Files | Refs | README

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('&', "&amp;")
     58      .replace('<', "&lt;")
     59      .replace('>', "&gt;")
     60      .replace('"', "&quot;")
     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 = "&gt;&gt;";
     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\">&gt;&gt;{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("&gt;")
     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 }