Upgrade mdx parser

This commit is contained in:
2025-11-05 21:46:49 -08:00
parent 726bf3cd36
commit 399fccb73b
3 changed files with 203 additions and 85 deletions

View File

@@ -9,7 +9,7 @@ use std::str::FromStr;
use crate::components::fa::FAIcon; use crate::components::fa::FAIcon;
use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail}; use crate::components::mangle::{MangledBetaEmail, MangledGoogleEmail};
use crate::components::misc::Backlinks; use crate::components::misc::{Backlinks, FarLink};
lazy_static! { lazy_static! {
static ref MdParser: MarkdownIt = { static ref MdParser: MarkdownIt = {
@@ -159,6 +159,10 @@ impl NodeValue for InlineMdx {
return; return;
} }
if mdx_external(&self.0, node, fmt) {
return;
}
fmt.open("code", &[]); fmt.open("code", &[]);
fmt.text(&self.0); fmt.text(&self.0);
fmt.close("code"); fmt.close("code");
@@ -200,78 +204,83 @@ impl InlineRule for InlineMdx {
} }
fn mdx_style(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool { fn mdx_style(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
// Parse inside of mdx: `style(<style>) <content>` // Parse inside of mdx: `color(value, "text")`
let (style, content) = { let mdx = mdx
let mdx = mdx.trim(); .trim()
if !mdx.starts_with("style(") { .trim_start_matches('{')
return false; .trim_end_matches('}')
.trim();
// Find the function name (everything before the opening parenthesis)
let paren_pos = match mdx.find('(') {
Some(x) => x,
None => return false,
};
if mdx[..paren_pos].trim() != "color" {
return false;
};
// Find matching closing parenthesis
let skip = paren_pos + 1;
let mut balance = 1;
let mut end = skip;
for i in mdx[skip..].bytes() {
match i {
b')' => balance -= 1,
b'(' => balance += 1,
_ => {}
} }
let skip = 6; if balance == 0 {
let mut balance = 1; break;
let mut end = skip;
for i in mdx[skip..].bytes() {
match i {
b')' => balance -= 1,
b'(' => balance += 1,
_ => {}
}
if balance == 0 {
break;
}
end += 1;
} }
if balance != 0 { end += 1;
return false; }
}
let style = mdx[skip..end].trim(); if balance != 0 {
let content = mdx[end + 1..].trim(); return false;
}
(style, content) let args = mdx[skip..end].trim();
// Parse arguments: should be "value, text" or "value, \"text\""
let comma_pos = match args.find(',') {
Some(x) => x,
None => return false,
};
let value = args[..comma_pos].trim();
let text = args[comma_pos + 1..].trim();
// Strip quotes from text if present
let text = if (text.starts_with('"') && text.ends_with('"'))
|| (text.starts_with('\'') && text.ends_with('\''))
{
&text[1..text.len() - 1]
} else {
text
}; };
let mut style_str = String::new(); let mut style_str = String::new();
for kv in style.split(";") { if value.starts_with("#") {
let mut s = kv.split(":"); style_str.push_str("color:");
let k = s.next(); style_str.push_str(value);
let v = s.next(); style_str.push(';');
} else if value.starts_with("--") {
if k.is_none() || v.is_none() || s.next().is_some() { style_str.push_str("color:var(");
return false; style_str.push_str(value);
} style_str.push_str(");");
} else {
#[expect(clippy::unwrap_used)] // Checked previously style_str.push_str("color:");
let k = k.unwrap().trim(); style_str.push_str(value);
style_str.push(';');
#[expect(clippy::unwrap_used)] // Checked previously
let v = v.unwrap().trim();
match k {
"color" => {
style_str.push_str("color:");
style_str.push_str(v);
style_str.push(';');
}
"color_var" => {
style_str.push_str("color:var(--");
style_str.push_str(v);
style_str.push_str(");");
}
_ => continue,
}
} }
// Only works with text, could be reworked to do basic md styling
// (italics, bold, tab, code)
fmt.open("span", &[("style", style_str)]); fmt.open("span", &[("style", style_str)]);
fmt.text(content); fmt.text(text);
fmt.close("span"); fmt.close("span");
return true; return true;
@@ -280,7 +289,12 @@ fn mdx_style(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool { fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
// Parse inside of mdx: `include(<args>)` // Parse inside of mdx: `include(<args>)`
let args = { let args = {
let mdx = mdx.trim(); let mdx = mdx
.trim()
.trim_start_matches('{')
.trim_end_matches('}')
.trim();
if !mdx.starts_with("include(") { if !mdx.starts_with("include(") {
return false; return false;
} }
@@ -326,6 +340,105 @@ fn mdx_include(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
return true; return true;
} }
fn mdx_external(mdx: &str, _node: &Node, fmt: &mut dyn Renderer) -> bool {
// Parse inside of mdx: `external("text", "link")`
let args = {
let mdx = mdx
.trim()
.trim_start_matches('{')
.trim_end_matches('}')
.trim();
if !mdx.starts_with("external(") {
return false;
}
let skip = 9;
let mut balance = 1;
let mut end = skip;
for i in mdx[skip..].bytes() {
match i {
b')' => balance -= 1,
b'(' => balance += 1,
_ => {}
}
if balance == 0 {
break;
}
end += 1;
}
if balance != 0 {
return false;
}
let args = mdx[skip..end].trim();
let trail = mdx[end + 1..].trim();
if !trail.is_empty() {
return false;
}
args
};
// TODO: better parsing, handle , in string
let mut split = args.split(",");
let title = match split.next() {
Some(mut x) => {
x = x.trim();
if &x[0..1] == "\"" {
x = &x[1..]
} else {
return false;
}
if &x[x.len() - 1..] == "\"" {
x = &x[0..x.len() - 1]
} else {
return false;
}
x
}
None => return false,
};
let link = match split.next() {
Some(mut x) => {
x = x.trim();
if &x[0..1] == "\"" {
x = &x[1..]
} else {
return false;
}
if &x[x.len() - 1..] == "\"" {
x = &x[0..x.len() - 1]
} else {
return false;
}
x
}
None => return false,
};
if split.next().is_some() {
return false;
}
fmt.text_raw(&FarLink(link, title).render().0);
return true;
}
// //
// MARK: yaml frontmatter // MARK: yaml frontmatter
// //

View File

@@ -6,23 +6,26 @@ slug = "handouts"
# Mark's Handouts # Mark's Handouts
[ORMC]: https://circles.math.ucla.edu/circles
This page lists all the handouts I've written for my classes at the [ORMC], This page lists all the handouts I've written for my classes at the
{{external("ORMC", "https://circles.math.ucla.edu/circles")}},
arguably the best math circle in the western world. We teach students mathematics arguably the best math circle in the western world. We teach students mathematics
far beyond the regular school curriculum, much like [AOPS](https://artofproblemsolving.com) far beyond the regular school curriculum, much like
and the [BMC](https://mathcircle.berkeley.edu). {{external("AOPS", "https://artofproblemsolving.com")}}
and the
{{external("BMC", "https://mathcircle.berkeley.edu")}}
<br></br> <br></br>
{style(color_var:pink) For my students: } \ {{color(--pink, "For my students:")}} \
Don't look at solutions we haven't discussed, Don't look at solutions we haven't discussed,
and don't start any handouts before class. That spoils all the fun! and don't start any handouts before class. That spoils all the fun!
{style(color_var:green) For everyone else:} \ {{color(--green, "For everyone else:")}} \
If you're using any of these, please let me know---especially \ If you're using any of these, please let me know---especially \
if you find errors, mistakes, or a poorly designed section. \ if you find errors, mistakes, or a poorly designed section. \
Such things must be fixed! { include(email_beta) } Such things must be fixed! {{ include(email_beta) }}
<br></br> <br></br>
@@ -33,8 +36,10 @@ are written with this in mind.\
I do not expect the average student to finish all problems during this two-hour session. I do not expect the average student to finish all problems during this two-hour session.
If the class finishes early, the lesson is either too short or too easy. If the class finishes early, the lesson is either too short or too easy.
The sources for all these handouts are available [here](https://git.betalupi.com/mark/handouts).\ The sources for all these handouts are available
Some are written in LaTeX, some are in [Typst](https://typst.app). \ {{external("here", "https://git.betalupi.com/mark/handouts")}} \
Some are written in LaTeX, some are in
{{external("Typst", "https://typst.app")}}. \
The latter is vastly superior. The latter is vastly superior.
<br></br> <br></br>

View File

@@ -9,37 +9,37 @@ Also see [what's a "betalupi?"](/whats-a-betalupi)
## Projects ## Projects
- **RedoxOS**, a general-purpose, microkernel-based operating system written in Rust. _{ style(color_var:grey) [enthusiast] }_ - **RedoxOS**, a general-purpose, microkernel-based operating system written in Rust. _{{color(--grey, "[enthusiast]")}}
- { style(color_var:grey) Status: } { style(color_var:yellow) Passive. } - {{color(--grey, "Status: ")}} {{color(--yellow, "Passive.")}}
- { style(color_var:grey) Website: } [:fa-link: redox-os.org](https://www.redox-os.org/) - {{color(--grey, "Website: ")}} [:fa-link: redox-os.org](https://www.redox-os.org/)
<br/> <br/>
- **Tectonic**, the LaTeX engine that is pleasant to use. - **Tectonic**, the LaTeX engine that is pleasant to use.
Experimental, but fully functional. _{ style(color_var:grey) [co-maintainer] }_ Experimental, but fully functional. _{{color(--grey, "[co-maintainer]")}}_
- { style(color_var:grey) Status: } { style(color_var:yellow) Passive. } [Typst](https://github.com/typst/typst) is better. - {{color(--grey, "Status: ")}} {{color(--yellow, "Abandoned. ")}} [Typst](https://github.com/typst/typst) is better.
- { style(color_var:grey) Main repo: } [:fa-github: Tectonic](https://github.com/tectonic-typesetting/tectonic) - {{color(--grey, "Main repo: ")}} [:fa-github: Tectonic](https://github.com/tectonic-typesetting/tectonic)
- { style(color_var:grey) Bundle tools: } [:fa-github: tectonic-texlive-bundles](https://github.com/tectonic-typesetting/tectonic-texlive-bundles) - {{color(--grey, "Bundle tools: ")}} [:fa-github: tectonic-texlive-bundles](https://github.com/tectonic-typesetting/tectonic-texlive-bundles)
<br/> <br/>
- **Daisy**, a pretty TUI scientific calculator. _{style(color_var:grey) [author] }_ - **Daisy**, a pretty TUI scientific calculator. _{{color(--grey, "[author]")}}_
- {style(color_var:grey) Status: } {style(color_var:orange) Done. } Used this to learn Rust. [Numbat](https://numbat.dev) is better. - {{color(--grey, "Status: ")}} {{color(--orange, "Done. ")}} Used this to learn Rust. [Numbat](https://numbat.dev) is better.
- {style(color_var:grey) Repository: } [:fa-github: rm-dr/daisy](https://github.com/rm-dr/daisy) - {{color(--grey, "Repository: ")}} [:fa-github: rm-dr/daisy](https://github.com/rm-dr/daisy)
- {style(color_var:grey) Website: } [:fa-link: daisy.betalupi.com](https://daisy.betalupi.com) (WASM demo) - {{color(--grey, "Website: ")}} [:fa-link: daisy.betalupi.com](https://daisy.betalupi.com) (WASM demo)
<br/> <br/>
- **Lamb**, a lambda calculus engine. _{style(color_var:grey) [author] }_ - **Lamb**, a lambda calculus engine. _{{color(--grey, "[author] ")}}_
- {style(color_var:grey) Status: } {style(color_var:orange) Done. } Fun little project. - {{color(--grey, "Status: ")}} {{color(--orange, "Done. ")}} Fun little project.
- {style(color_var:grey) Repository: } [:fa-github: rm-dr/lamb](https://github.com/rm-dr/lamb) - {{color(--grey, "Repository: ")}} [:fa-github: rm-dr/lamb](https://github.com/rm-dr/lamb)
- {style(color_var:grey) PyPi: } [:fa-python: lamb-engine](https://pypi.org/project/lamb-engine) - {{color(--grey, "PyPi: ")}} [:fa-python: lamb-engine](https://pypi.org/project/lamb-engine)
<br/> <br/>