Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 10 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mark = { path = "mark" }

[profile.release]
strip = true
lto = true
panic = "abort"
lto = "fat"
codegen-units = 1

2 changes: 1 addition & 1 deletion mark/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ edition = "2024"

[dependencies]
comrak = "0.52.0"
regex = "1.12.4"
nom = "8"
135 changes: 135 additions & 0 deletions mark/src/image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
use comrak::html::{ChildRendering, Context, dangerous_url};
use comrak::nodes::NodeLink;
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};

enum ImgDimension {
Px(u32),
Percent(u32),
}

impl Display for ImgDimension {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Px(n) => write!(f, "{n}px"),
Self::Percent(n) => write!(f, "{n}%"),
}
}
}

struct SithImg<'a> {
url: &'a str,
width: Option<ImgDimension>,
height: Option<ImgDimension>,
}

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<ImgDimension>), 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> {
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 Ok((width, height)) = parse_dimensions(query)
{
Self {
url,
width: Some(width),
height,
}
} 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,
/// with some code added
pub(crate) fn render_image<T>(
context: &mut Context<T>,
entering: bool,
nl: &NodeLink,
) -> Result<ChildRendering, fmt::Error> {
if entering {
let img_data = SithImg::from(nl.url.as_str());
if context.options.render.figure_with_caption {
context.write_str("<figure>")?;
}
context.write_str("<img src=\"")?;
if !dangerous_url(img_data.url) {
context.escape_href(img_data.url)?;
}
if img_data.width.is_some() || img_data.height.is_some() {
context.write_str("\" style=\"")?;
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=\"")?;
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("<figcaption>")?;
context.escape(&nl.title)?;
context.write_str("</figcaption>")?;
}
context.write_str("</figure>")?;
}
}

Ok(ChildRendering::HTML)
}
79 changes: 7 additions & 72 deletions mark/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<width>\d+(%|px)?)(x(?P<height>\d+(%|px)?))?$";
static DIMENSION_RE: LazyLock<Regex> = LazyLock::new(|| Regex::new(DIMENSION_PATTERN).unwrap());

#[inline(always)]
fn options<'a>() -> Options<'a> {
Expand All @@ -22,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,
Expand All @@ -37,71 +30,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<T>(
context: &mut Context<T>,
entering: bool,
nl: &NodeLink,
) -> Result<ChildRendering, fmt::Error> {
if entering {
if context.options.render.figure_with_caption {
context.write_str("<figure>")?;
}
context.write_str("<img src=\"")?;
let url = nl.url.split('?').next().unwrap();
if context.options.render.r#unsafe || !dangerous_url(url) {
if let Some(rewriter) = &context.options.extension.image_url_rewriter {
context.escape_href(&rewriter.to_html(&nl.url))?;
} else {
context.escape_href(url)?;
}
}
context.write_str("\" alt=\"")?;
return Ok(ChildRendering::Plain);
} else {
if !nl.title.is_empty() {
context.write_str("\" title=\"")?;
context.escape(&nl.title)?;
}
if let Some((_url, query)) = nl.url.rsplit_once('?')
&& let Some(caps) = DIMENSION_RE.captures(query)
{
context.write_str("\" style=\"")?;

for dim in ["width", "height"] {
if let Some(val) = caps.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("\" />")?;
if context.options.render.figure_with_caption {
if !nl.title.is_empty() {
context.write_str("<figcaption>")?;
context.escape(&nl.title)?;
context.write_str("</figcaption>")?;
}
context.write_str("</figure>")?;
}
}

Ok(ChildRendering::HTML)
}

create_formatter!(CustomFormatter, {
NodeValue::Image(ref nl) => |context, entering| {
return render_image(context, entering, nl);
Expand All @@ -122,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
}