From e40018b9b4b815e25c03ce66555fa572c78e3ef0 Mon Sep 17 00:00:00 2001 From: Thomas Girod Date: Tue, 16 Jun 2026 10:55:18 +0200 Subject: [PATCH 1/4] move `render_image` to its own file --- mark/src/image.rs | 76 +++++++++++++++++++++++++++++++++++++++++++++++ mark/src/lib.rs | 74 ++++----------------------------------------- 2 files changed, 81 insertions(+), 69 deletions(-) create mode 100644 mark/src/image.rs diff --git a/mark/src/image.rs b/mark/src/image.rs new file mode 100644 index 0000000..1fe4fdb --- /dev/null +++ b/mark/src/image.rs @@ -0,0 +1,76 @@ +use comrak::html::{ChildRendering, Context, dangerous_url}; +use comrak::nodes::NodeLink; +use std::fmt; +use std::fmt::Write; +use std::sync::LazyLock; +use std::time::Instant; +use regex::Regex; + +const DIMENSION_PATTERN: &str = r"^(?P\d+(%|px)?)(x(?P\d+(%|px)?))?$"; +static DIMENSION_RE: LazyLock = LazyLock::new(|| Regex::new(DIMENSION_PATTERN).unwrap()); + +/// Render an image, with eventual size modifiers. +/// +/// This is basically a copy-paste of the comrack `render_image` function, +/// with some code added +pub(crate) fn render_image( + context: &mut Context, + entering: bool, + nl: &NodeLink, +) -> Result { + if entering { + let start = Instant::now(); + let split = nl.url.rsplit_once('?'); + let dimensions = match split { + Some((_, query)) => DIMENSION_RE.captures(query), + None => None, + }; + let dur = start.elapsed(); + println!("{dur:?} {}", nl.url); + if context.options.render.figure_with_caption { + context.write_str("
")?; + } + context.write_str(" split.unwrap().0, + None => nl.url.as_str(), // else, leave the url untouched + }; + if !dangerous_url(url) { + context.escape_href(url)?; + } + if let Some(dimensions) = dimensions { + context.write_str("\" style=\"")?; + + for dim in ["width", "height"] { + if let Some(val) = dimensions.name(dim) { + context.write_str(dim)?; + context.write_char(':')?; + context.write_str(val.as_str())?; + if !val.as_str().ends_with('%') && !val.as_str().ends_with("px") { + context.write_str("px")?; + } + context.write_char(';')?; + } + } + } + context.write_str("\" alt=\"")?; + return Ok(ChildRendering::Plain); + } else { + if !nl.title.is_empty() { + context.write_str("\" title=\"")?; + context.escape(&nl.title)?; + } + context.write_str("\" />")?; + if context.options.render.figure_with_caption { + if !nl.title.is_empty() { + context.write_str("
")?; + context.escape(&nl.title)?; + context.write_str("
")?; + } + context.write_str("
")?; + } + } + + Ok(ChildRendering::HTML) +} diff --git a/mark/src/lib.rs b/mark/src/lib.rs index ec052fc..8bbf5bd 100644 --- a/mark/src/lib.rs +++ b/mark/src/lib.rs @@ -1,14 +1,9 @@ -use comrak::html::{ChildRendering, Context, dangerous_url}; -use comrak::nodes::{NodeLink, NodeValue}; +mod image; + +use crate::image::render_image; +use comrak::nodes::NodeValue; use comrak::options::{Extension, Parse, Render}; use comrak::{Arena, Options, create_formatter, parse_document}; -use regex::Regex; -use std::fmt; -use std::fmt::Write; -use std::sync::LazyLock; - -const DIMENSION_PATTERN: &str = r"^(?P\d+(%|px)?)(x(?P\d+(%|px)?))?$"; -static DIMENSION_RE: LazyLock = LazyLock::new(|| Regex::new(DIMENSION_PATTERN).unwrap()); #[inline(always)] fn options<'a>() -> Options<'a> { @@ -37,71 +32,12 @@ fn options<'a>() -> Options<'a> { ..Default::default() }, render: Render { + escape: true, ..Default::default() }, } } -/// Render an image, with eventual size modifiers. -/// -/// This is basically a copy-paste of the comrack `render_image` function, -/// with some code added -fn render_image( - context: &mut Context, - entering: bool, - nl: &NodeLink, -) -> Result { - if entering { - if context.options.render.figure_with_caption { - context.write_str("
")?; - } - context.write_str("\"")?;")?; - if context.options.render.figure_with_caption { - if !nl.title.is_empty() { - context.write_str("
")?; - context.escape(&nl.title)?; - context.write_str("
")?; - } - context.write_str("
")?; - } - } - - Ok(ChildRendering::HTML) -} - create_formatter!(CustomFormatter, { NodeValue::Image(ref nl) => |context, entering| { return render_image(context, entering, nl); From 076438f85bd8cbaa999ff639724111280268a52f Mon Sep 17 00:00:00 2001 From: Thomas Girod Date: Wed, 17 Jun 2026 19:59:33 +0200 Subject: [PATCH 2/4] extract img dimension logic --- mark/src/image.rs | 104 +++++++++++++++++++++++++++++++++------------- 1 file changed, 74 insertions(+), 30 deletions(-) diff --git a/mark/src/image.rs b/mark/src/image.rs index 1fe4fdb..f9c6a63 100644 --- a/mark/src/image.rs +++ b/mark/src/image.rs @@ -1,14 +1,76 @@ +use crate::image::ImgDimension::{Percent, Px}; use comrak::html::{ChildRendering, Context, dangerous_url}; use comrak::nodes::NodeLink; +use regex::Regex; use std::fmt; -use std::fmt::Write; +use std::fmt::{Display, Write}; use std::sync::LazyLock; -use std::time::Instant; -use regex::Regex; const DIMENSION_PATTERN: &str = r"^(?P\d+(%|px)?)(x(?P\d+(%|px)?))?$"; static DIMENSION_RE: LazyLock = LazyLock::new(|| Regex::new(DIMENSION_PATTERN).unwrap()); +enum ImgDimension { + Px(T), + Percent(T), +} + +impl<'a> TryFrom<&'a str> for ImgDimension<&'a str> { + type Error = (); + + fn try_from(s: &'a str) -> Result { + if s.ends_with('%') { + Ok(Percent(s.strip_suffix('%').unwrap())) + } else if s.ends_with("px") { + Ok(Px(s.strip_suffix("px").unwrap())) + } else if s.chars().all(|c| c.is_ascii_digit()) { + Ok(Px(s)) + } else { + Err(()) + } + } +} + +impl Display for ImgDimension { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Px(n) => write!(f, "{n}px"), + Percent(n) => write!(f, "{n}%"), + } + } +} + +struct SithImg<'a> { + url: &'a str, + width: Option>, + height: Option>, +} + +impl<'a> From<&'a str> for SithImg<'a> { + fn from(s: &'a str) -> Self { + // if the url contained dimension instructions, remove the query part + // else, leave the url untouched + if let Some((url, query)) = s.rsplit_once('?') + && let Some(dimensions) = DIMENSION_RE.captures(query) + { + Self { + url, + width: dimensions + .name("width") + .and_then(|i| i.as_str().try_into().ok()), + height: dimensions + .name("height") + .and_then(|i| i.as_str().try_into().ok()), + } + } else { + Self { + url: s, + width: None, + height: None, + } + } + } +} + /// Render an image, with eventual size modifiers. /// /// This is basically a copy-paste of the comrack `render_image` function, @@ -19,39 +81,21 @@ pub(crate) fn render_image( nl: &NodeLink, ) -> Result { if entering { - let start = Instant::now(); - let split = nl.url.rsplit_once('?'); - let dimensions = match split { - Some((_, query)) => DIMENSION_RE.captures(query), - None => None, - }; - let dur = start.elapsed(); - println!("{dur:?} {}", nl.url); + let img_data = SithImg::from(nl.url.as_str()); if context.options.render.figure_with_caption { context.write_str("
")?; } context.write_str(" split.unwrap().0, - None => nl.url.as_str(), // else, leave the url untouched - }; - if !dangerous_url(url) { - context.escape_href(url)?; + if !dangerous_url(img_data.url) { + context.escape_href(img_data.url)?; } - if let Some(dimensions) = dimensions { + if img_data.width.is_some() || img_data.height.is_some() { context.write_str("\" style=\"")?; - - for dim in ["width", "height"] { - if let Some(val) = dimensions.name(dim) { - context.write_str(dim)?; - context.write_char(':')?; - context.write_str(val.as_str())?; - if !val.as_str().ends_with('%') && !val.as_str().ends_with("px") { - context.write_str("px")?; - } - context.write_char(';')?; - } + if let Some(width) = img_data.width { + write!(context, "width:{}", width)?; + } + if let Some(height) = img_data.height { + write!(context, ";height:{}", height)?; } } context.write_str("\" alt=\"")?; From 9ba1a9ca9b8b39c20bfe82c3c0e267729f8298a6 Mon Sep 17 00:00:00 2001 From: Thomas Girod Date: Fri, 19 Jun 2026 11:40:37 +0200 Subject: [PATCH 3/4] use nom instead of regex to parse dimensions Execution is faster at first call (because no more regex compilation) and the wheel size is split in half (1Mo => 500ko). --- Cargo.lock | 23 ++++++------ mark/Cargo.toml | 2 +- mark/src/image.rs | 89 +++++++++++++++++++++++++++-------------------- 3 files changed, 63 insertions(+), 51 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b12e0a6..bbfcbb1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -435,7 +435,7 @@ name = "mark" version = "0.1.0" dependencies = [ "comrak", - "regex", + "nom", ] [[package]] @@ -454,6 +454,15 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -658,18 +667,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "regex" -version = "1.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.14" diff --git a/mark/Cargo.toml b/mark/Cargo.toml index e3d4c12..c571713 100644 --- a/mark/Cargo.toml +++ b/mark/Cargo.toml @@ -6,4 +6,4 @@ edition = "2024" [dependencies] comrak = "0.52.0" -regex = "1.12.4" +nom = "8" diff --git a/mark/src/image.rs b/mark/src/image.rs index f9c6a63..e2c804a 100644 --- a/mark/src/image.rs +++ b/mark/src/image.rs @@ -1,48 +1,67 @@ -use crate::image::ImgDimension::{Percent, Px}; use comrak::html::{ChildRendering, Context, dangerous_url}; use comrak::nodes::NodeLink; -use regex::Regex; +use nom::branch::alt; +use nom::bytes::tag; + +use nom::Parser; +use nom::character::complete::digit1; +use nom::combinator::{eof, map_res, opt}; use std::fmt; use std::fmt::{Display, Write}; -use std::sync::LazyLock; - -const DIMENSION_PATTERN: &str = r"^(?P\d+(%|px)?)(x(?P\d+(%|px)?))?$"; -static DIMENSION_RE: LazyLock = LazyLock::new(|| Regex::new(DIMENSION_PATTERN).unwrap()); -enum ImgDimension { - Px(T), - Percent(T), +enum ImgDimension { + Px(u32), + Percent(u32), } -impl<'a> TryFrom<&'a str> for ImgDimension<&'a str> { - type Error = (); - - fn try_from(s: &'a str) -> Result { - if s.ends_with('%') { - Ok(Percent(s.strip_suffix('%').unwrap())) - } else if s.ends_with("px") { - Ok(Px(s.strip_suffix("px").unwrap())) - } else if s.chars().all(|c| c.is_ascii_digit()) { - Ok(Px(s)) - } else { - Err(()) - } - } -} - -impl Display for ImgDimension { +impl Display for ImgDimension { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Px(n) => write!(f, "{n}px"), - Percent(n) => write!(f, "{n}%"), + Self::Px(n) => write!(f, "{n}px"), + Self::Percent(n) => write!(f, "{n}%"), } } } struct SithImg<'a> { url: &'a str, - width: Option>, - height: Option>, + width: Option, + height: Option, +} + +struct ParseDimensionError; + +/// Given a querystring that may contain image dimensions, +/// parse it and return the result +/// +/// ## Note +/// +/// If the dimension string is valid, it will always contain the image width. +/// However, the height is optional. +fn parse_dimensions(s: &str) -> Result<(ImgDimension, Option), ParseDimensionError> { + fn parse_dim(dim: &str) -> Result<(&str, ImgDimension), ParseDimensionError> { + let parsed = ( + map_res(digit1::<_, (_, _)>, str::parse), + opt(alt((eof, tag("%"), tag("px")))), + ) + .parse(dim); + let Ok((remaining, (val, unit))) = parsed else { + return Err(ParseDimensionError); + }; + match unit { + Some("%") => Ok((remaining, ImgDimension::Percent(val))), + None | Some("px") | Some("") => Ok((remaining, ImgDimension::Px(val))), + _ => Err(ParseDimensionError), + } + } + let (remaining, width) = parse_dim(s)?; + let height = if let Some(stripped) = remaining.strip_prefix("x") { + Some(parse_dim(stripped)?.1) + } else { + None + }; + + Ok((width, height)) } impl<'a> From<&'a str> for SithImg<'a> { @@ -50,16 +69,12 @@ impl<'a> From<&'a str> for SithImg<'a> { // if the url contained dimension instructions, remove the query part // else, leave the url untouched if let Some((url, query)) = s.rsplit_once('?') - && let Some(dimensions) = DIMENSION_RE.captures(query) + && let Ok((width, height)) = parse_dimensions(query) { Self { url, - width: dimensions - .name("width") - .and_then(|i| i.as_str().try_into().ok()), - height: dimensions - .name("height") - .and_then(|i| i.as_str().try_into().ok()), + width: Some(width), + height, } } else { Self { From cce3de0cebad0512224f83f2c5b776dbd0560047 Mon Sep 17 00:00:00 2001 From: Thomas Girod Date: Fri, 19 Jun 2026 11:40:54 +0200 Subject: [PATCH 4/4] minor optimisations --- Cargo.toml | 3 ++- mark/src/lib.rs | 5 ++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1b0a471..e9d9103 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ mark = { path = "mark" } [profile.release] strip = true -lto = true +panic = "abort" +lto = "fat" codegen-units = 1 diff --git a/mark/src/lib.rs b/mark/src/lib.rs index 8bbf5bd..ad52d08 100644 --- a/mark/src/lib.rs +++ b/mark/src/lib.rs @@ -17,8 +17,6 @@ fn options<'a>() -> Options<'a> { footnotes: true, description_lists: true, multiline_block_quotes: true, - math_dollars: true, - math_code: true, shortcodes: true, underline: true, subscript: true, @@ -58,7 +56,8 @@ pub fn markdown(s: &str) -> String { } } - let mut html = String::new(); + // html result will be at least as long as md input, so let's allocate that + let mut html = String::with_capacity(s.len()); CustomFormatter::format_document(root, &options, &mut html).unwrap(); html }