diff --git a/src/utils/markdown.rs b/src/utils/markdown.rs index 531b6f4..958639e 100644 --- a/src/utils/markdown.rs +++ b/src/utils/markdown.rs @@ -4,6 +4,7 @@ use comrak::nodes::{AstNode, NodeValue}; use comrak::{format_html, parse_document, Arena, ComrakOptions, ListStyleType, Options}; use lol_html::html_content::ContentType; use lol_html::{element, rewrite_str, HtmlRewriter, RewriteStrSettings, Settings}; +use mime_guess::mime; use ramhorns::Content; use std::fmt::Debug; use std::fs; @@ -162,10 +163,15 @@ fn custom_img_size(html: &str) -> String { rewrite_str( html, RewriteStrSettings { - element_content_handlers: vec![element!("img[alt]", |el| { - let alt = el.get_attribute("alt").unwrap(); + element_content_handlers: vec![element!("img[alt], svg", |el| { + let alt = el.get_attribute("alt").unwrap_or_default(); let possible_piece = alt.split('|').collect::>(); + if el.tag_name() == *"svg" { + el.remove_attribute("width"); + el.remove_attribute("height"); + } + if possible_piece.len() > 1 { let data = possible_piece.last().unwrap().trim(); @@ -255,7 +261,7 @@ fn fix_images_and_integration( .unwrap() .to_string(); - if let Ok(file_contents) = fs::read(&img_path) { + if let Ok(mut file_contents) = fs::read(&img_path) { let mime = mime_guess::from_path(&img_path).first_or_octet_stream(); if recursive && mime == "text/markdown" { let file_str = String::from_utf8_lossy(&file_contents).into_owned(); @@ -286,6 +292,16 @@ fn fix_images_and_integration( // Store the metadata for later merging additional_metadata.push(data.metadata); + } else if mime == mime::IMAGE_SVG { + let alt = el.get_attribute("alt").unwrap_or_default(); + file_contents.drain(0..3); + el.replace( + &format!( + " * { page-break-inside: avoid; diff --git a/static/css/style.css b/static/css/style.css index e7882c0..f0058b6 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -83,7 +83,7 @@ header nav a:hover { } /* Add links */ - a:not(:where([href^="#"], [href^="/"])):not(:has(img))::after { + a:not(:where([href^="#"], [href^="/"])):not(:has(img)):not(:has(svg))::after { content: " (" attr(href) ")"; display: inline-block; white-space: pre; @@ -96,7 +96,8 @@ header nav a:hover { } @media (prefers-color-scheme: dark) { - img { + img, + svg { filter: brightness(0.8) contrast(1.2); } } diff --git a/static/js/markdown.js b/static/js/markdown.js index fa192eb..78d4656 100644 --- a/static/js/markdown.js +++ b/static/js/markdown.js @@ -4,94 +4,117 @@ const Mode = { }; /** - * Change the svg color theme based on the mode + * Convert to grayscale + * @param {{r: number, g: number, b: number}} color + * @returns Number between 0 and 255 + */ +const colorToGrayscale = (color) => { + return 0.3 * color.r + 0.59 * color.g + 0.11 * color.b; +}; + +/** + * Extract color using canvas2d + * @param {HTMLImageElement} image Image source + * @returns Colors represeting the image + */ +const extractColors = (image) => { + const canvas = document.createElement("canvas"); + canvas.width = image.naturalWidth || image.width; + canvas.height = image.naturalHeight || image.height; + const ctx = canvas.getContext("2d"); + ctx.drawImage(image, 0, 0); + + const imageData = ctx.getImageData( + 0, + 0, + Math.max(1, canvas.width), + Math.max(1, canvas.height) + ); + const pixelData = imageData.data; + + const colors = []; + for (let i = 0; i < pixelData.length; i += 4) { + if (pixelData[i + 3] > 0) { + colors.push({ + r: pixelData[i], + g: pixelData[i + 1], + b: pixelData[i + 2], + }); + } + } + + return colors; +}; + +/** + * Change the color theme based on the mode * @param {Mode} mode */ -const svgChangeTheme = (mode) => { +const changeTheme = (mode) => { + for (const item of document.getElementsByTagName("svg")) { + const image = document.createElement("img"); + image.src = `data:image/svg+xml;base64,${btoa( + new XMLSerializer().serializeToString(item) + )}`; + + image.width = item.viewBox.baseVal.width; + image.height = item.viewBox.baseVal.height; + + image.onload = () => { + applyFilter(mode, item, extractColors(image)); + }; + } + + // SVG embedded in images as base64 for (const item of document.getElementsByTagName("img")) { if (!item.src.startsWith("data:image/svg+xml;base64,")) { // Exclude image who aren't SVG and base64 encoded continue; } - /** - * Convert to grayscale - * @param {{r: number, g: number, b: number}} color - * @returns Number between 0 and 255 - */ - const colorToGrayscale = (color) => { - return 0.3 * color.r + 0.59 * color.g + 0.11 * color.b; - }; - - /** - * Extract color using canvas2d - * @param {HTMLImageElement} image Image source - * @returns Colors represeting the image - */ - const extractColors = (image) => { - const canvas = document.createElement("canvas"); - canvas.width = image.naturalWidth; - canvas.height = image.naturalHeight; - const ctx = canvas.getContext("2d"); - ctx.drawImage(image, 0, 0); - - const imageData = ctx.getImageData( - 0, - 0, - Math.max(1, canvas.width), - Math.max(1, canvas.height) - ); - const pixelData = imageData.data; - - const colors = []; - for (let i = 0; i < pixelData.length; i += 4) { - if (pixelData[i + 3] > 0) { - colors.push({ - r: pixelData[i], - g: pixelData[i + 1], - b: pixelData[i + 2], - }); - } - } - - return colors; - }; - - // Extract colors - const colors = extractColors(item); - - // Calculate the average grayscale value - const grayscaleValues = colors.map(colorToGrayscale); - const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0); - const averageGrayscale = totalGrayscale / grayscaleValues.length; - - const treshold = 128; - - const style = "filter: "; - const dim = "brightness(0.8) contrast(1.2)"; - - if (averageGrayscale < treshold && mode === Mode.Dark) { - item.style = style + dim + " invert(1);"; - continue; - } - - if (averageGrayscale > treshold && mode === Mode.Light) { - item.style = style + "invert(1);"; - continue; - } - - if (mode === Mode.Dark) { - item.style = style + `${dim};`; - continue; - } - - item.style = ""; + applyFilter(mode, item, extractColors(item)); } }; +/** + * Apply the filter based on the selected mode + * @param {Mode} mode Current theme + * @param {HTMLImageElement | SVGSVGElement} item Element + * @param {{r: number, g: number, b: number}} colors Array of the colors of the element + * @returns + */ +const applyFilter = (mode, item, colors) => { + // Calculate the average grayscale value + const grayscaleValues = colors.map(colorToGrayscale); + const totalGrayscale = grayscaleValues.reduce((acc, val) => acc + val, 0); + const averageGrayscale = totalGrayscale / grayscaleValues.length; + + const treshold = 128; + + const style = "filter: "; + const dim = "brightness(0.8) contrast(1.2)"; + + if (averageGrayscale < treshold && mode === Mode.Dark) { + item.style = style + dim + " invert(1);"; + return; + } + + if (averageGrayscale > treshold && mode === Mode.Light) { + item.style = style + "invert(1);"; + return; + } + + if (mode === Mode.Dark) { + item.style = style + `${dim};`; + return; + } + + item.style = ""; +}; + window.addEventListener("load", () => { // Fix SVG images - svgChangeTheme( + changeTheme( window.matchMedia("(prefers-color-scheme: dark)").matches ? Mode.Dark : Mode.Light @@ -101,5 +124,5 @@ window.addEventListener("load", () => { window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", (event) => - svgChangeTheme(event.matches ? Mode.Dark : Mode.Light) + changeTheme(event.matches ? Mode.Dark : Mode.Light) );