Skip to content

Commit 4ae65cc

Browse files
authored
Merge pull request #2 from ae-utbm/nom
Use `nom` to parse image dimensions
2 parents f4959f9 + cce3de0 commit 4ae65cc

5 files changed

Lines changed: 155 additions & 87 deletions

File tree

Cargo.lock

Lines changed: 10 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ mark = { path = "mark" }
1717

1818
[profile.release]
1919
strip = true
20-
lto = true
20+
panic = "abort"
21+
lto = "fat"
2122
codegen-units = 1
2223

mark/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@ edition = "2024"
66

77
[dependencies]
88
comrak = "0.52.0"
9-
regex = "1.12.4"
9+
nom = "8"

mark/src/image.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use comrak::html::{ChildRendering, Context, dangerous_url};
2+
use comrak::nodes::NodeLink;
3+
use nom::branch::alt;
4+
use nom::bytes::tag;
5+
6+
use nom::Parser;
7+
use nom::character::complete::digit1;
8+
use nom::combinator::{eof, map_res, opt};
9+
use std::fmt;
10+
use std::fmt::{Display, Write};
11+
12+
enum ImgDimension {
13+
Px(u32),
14+
Percent(u32),
15+
}
16+
17+
impl Display for ImgDimension {
18+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
19+
match self {
20+
Self::Px(n) => write!(f, "{n}px"),
21+
Self::Percent(n) => write!(f, "{n}%"),
22+
}
23+
}
24+
}
25+
26+
struct SithImg<'a> {
27+
url: &'a str,
28+
width: Option<ImgDimension>,
29+
height: Option<ImgDimension>,
30+
}
31+
32+
struct ParseDimensionError;
33+
34+
/// Given a querystring that may contain image dimensions,
35+
/// parse it and return the result
36+
///
37+
/// ## Note
38+
///
39+
/// If the dimension string is valid, it will always contain the image width.
40+
/// However, the height is optional.
41+
fn parse_dimensions(s: &str) -> Result<(ImgDimension, Option<ImgDimension>), ParseDimensionError> {
42+
fn parse_dim(dim: &str) -> Result<(&str, ImgDimension), ParseDimensionError> {
43+
let parsed = (
44+
map_res(digit1::<_, (_, _)>, str::parse),
45+
opt(alt((eof, tag("%"), tag("px")))),
46+
)
47+
.parse(dim);
48+
let Ok((remaining, (val, unit))) = parsed else {
49+
return Err(ParseDimensionError);
50+
};
51+
match unit {
52+
Some("%") => Ok((remaining, ImgDimension::Percent(val))),
53+
None | Some("px") | Some("") => Ok((remaining, ImgDimension::Px(val))),
54+
_ => Err(ParseDimensionError),
55+
}
56+
}
57+
let (remaining, width) = parse_dim(s)?;
58+
let height = if let Some(stripped) = remaining.strip_prefix("x") {
59+
Some(parse_dim(stripped)?.1)
60+
} else {
61+
None
62+
};
63+
64+
Ok((width, height))
65+
}
66+
67+
impl<'a> From<&'a str> for SithImg<'a> {
68+
fn from(s: &'a str) -> Self {
69+
// if the url contained dimension instructions, remove the query part
70+
// else, leave the url untouched
71+
if let Some((url, query)) = s.rsplit_once('?')
72+
&& let Ok((width, height)) = parse_dimensions(query)
73+
{
74+
Self {
75+
url,
76+
width: Some(width),
77+
height,
78+
}
79+
} else {
80+
Self {
81+
url: s,
82+
width: None,
83+
height: None,
84+
}
85+
}
86+
}
87+
}
88+
89+
/// Render an image, with eventual size modifiers.
90+
///
91+
/// This is basically a copy-paste of the comrack `render_image` function,
92+
/// with some code added
93+
pub(crate) fn render_image<T>(
94+
context: &mut Context<T>,
95+
entering: bool,
96+
nl: &NodeLink,
97+
) -> Result<ChildRendering, fmt::Error> {
98+
if entering {
99+
let img_data = SithImg::from(nl.url.as_str());
100+
if context.options.render.figure_with_caption {
101+
context.write_str("<figure>")?;
102+
}
103+
context.write_str("<img src=\"")?;
104+
if !dangerous_url(img_data.url) {
105+
context.escape_href(img_data.url)?;
106+
}
107+
if img_data.width.is_some() || img_data.height.is_some() {
108+
context.write_str("\" style=\"")?;
109+
if let Some(width) = img_data.width {
110+
write!(context, "width:{}", width)?;
111+
}
112+
if let Some(height) = img_data.height {
113+
write!(context, ";height:{}", height)?;
114+
}
115+
}
116+
context.write_str("\" alt=\"")?;
117+
return Ok(ChildRendering::Plain);
118+
} else {
119+
if !nl.title.is_empty() {
120+
context.write_str("\" title=\"")?;
121+
context.escape(&nl.title)?;
122+
}
123+
context.write_str("\" />")?;
124+
if context.options.render.figure_with_caption {
125+
if !nl.title.is_empty() {
126+
context.write_str("<figcaption>")?;
127+
context.escape(&nl.title)?;
128+
context.write_str("</figcaption>")?;
129+
}
130+
context.write_str("</figure>")?;
131+
}
132+
}
133+
134+
Ok(ChildRendering::HTML)
135+
}

mark/src/lib.rs

Lines changed: 7 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,9 @@
1-
use comrak::html::{ChildRendering, Context, dangerous_url};
2-
use comrak::nodes::{NodeLink, NodeValue};
1+
mod image;
2+
3+
use crate::image::render_image;
4+
use comrak::nodes::NodeValue;
35
use comrak::options::{Extension, Parse, Render};
46
use comrak::{Arena, Options, create_formatter, parse_document};
5-
use regex::Regex;
6-
use std::fmt;
7-
use std::fmt::Write;
8-
use std::sync::LazyLock;
9-
10-
const DIMENSION_PATTERN: &str = r"^(?P<width>\d+(%|px)?)(x(?P<height>\d+(%|px)?))?$";
11-
static DIMENSION_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(DIMENSION_PATTERN).unwrap());
127

138
#[inline(always)]
149
fn options<'a>() -> Options<'a> {
@@ -22,8 +17,6 @@ fn options<'a>() -> Options<'a> {
2217
footnotes: true,
2318
description_lists: true,
2419
multiline_block_quotes: true,
25-
math_dollars: true,
26-
math_code: true,
2720
shortcodes: true,
2821
underline: true,
2922
subscript: true,
@@ -37,71 +30,12 @@ fn options<'a>() -> Options<'a> {
3730
..Default::default()
3831
},
3932
render: Render {
33+
escape: true,
4034
..Default::default()
4135
},
4236
}
4337
}
4438

45-
/// Render an image, with eventual size modifiers.
46-
///
47-
/// This is basically a copy-paste of the comrack `render_image` function,
48-
/// with some code added
49-
fn render_image<T>(
50-
context: &mut Context<T>,
51-
entering: bool,
52-
nl: &NodeLink,
53-
) -> Result<ChildRendering, fmt::Error> {
54-
if entering {
55-
if context.options.render.figure_with_caption {
56-
context.write_str("<figure>")?;
57-
}
58-
context.write_str("<img src=\"")?;
59-
let url = nl.url.split('?').next().unwrap();
60-
if context.options.render.r#unsafe || !dangerous_url(url) {
61-
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
62-
context.escape_href(&rewriter.to_html(&nl.url))?;
63-
} else {
64-
context.escape_href(url)?;
65-
}
66-
}
67-
context.write_str("\" alt=\"")?;
68-
return Ok(ChildRendering::Plain);
69-
} else {
70-
if !nl.title.is_empty() {
71-
context.write_str("\" title=\"")?;
72-
context.escape(&nl.title)?;
73-
}
74-
if let Some((_url, query)) = nl.url.rsplit_once('?')
75-
&& let Some(caps) = DIMENSION_RE.captures(query)
76-
{
77-
context.write_str("\" style=\"")?;
78-
79-
for dim in ["width", "height"] {
80-
if let Some(val) = caps.name(dim) {
81-
context.write_str(dim)?;
82-
context.write_char(':')?;
83-
context.write_str(val.as_str())?;
84-
if !val.as_str().ends_with('%') && !val.as_str().ends_with("px") {
85-
context.write_str("px")?;
86-
}
87-
context.write_char(';')?;
88-
}
89-
}
90-
}
91-
context.write_str("\" />")?;
92-
if context.options.render.figure_with_caption {
93-
if !nl.title.is_empty() {
94-
context.write_str("<figcaption>")?;
95-
context.escape(&nl.title)?;
96-
context.write_str("</figcaption>")?;
97-
}
98-
context.write_str("</figure>")?;
99-
}
100-
}
101-
102-
Ok(ChildRendering::HTML)
103-
}
104-
10539
create_formatter!(CustomFormatter, {
10640
NodeValue::Image(ref nl) => |context, entering| {
10741
return render_image(context, entering, nl);
@@ -122,7 +56,8 @@ pub fn markdown(s: &str) -> String {
12256
}
12357
}
12458

125-
let mut html = String::new();
59+
// html result will be at least as long as md input, so let's allocate that
60+
let mut html = String::with_capacity(s.len());
12661
CustomFormatter::format_document(root, &options, &mut html).unwrap();
12762
html
12863
}

0 commit comments

Comments
 (0)