mirror of
https://github.com/Nutomic/ibis.git
synced 2025-01-11 15:45:49 +00:00
Split markdown files
This commit is contained in:
parent
7f2730470d
commit
d4c4202281
4 changed files with 160 additions and 143 deletions
76
src/frontend/markdown/article_link.rs
Normal file
76
src/frontend/markdown/article_link.rs
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
use markdown_it::{
|
||||||
|
parser::inline::{InlineRule, InlineState},
|
||||||
|
Node,
|
||||||
|
NodeValue,
|
||||||
|
Renderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ArticleLink {
|
||||||
|
label: String,
|
||||||
|
title: String,
|
||||||
|
domain: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// This defines how your custom node should be rendered.
|
||||||
|
impl NodeValue for ArticleLink {
|
||||||
|
fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
|
||||||
|
let mut attrs = node.attrs.clone();
|
||||||
|
|
||||||
|
let link = format!("/article/{}@{}", self.title, self.domain);
|
||||||
|
attrs.push(("href", link));
|
||||||
|
|
||||||
|
fmt.open("a", &attrs);
|
||||||
|
fmt.text(&self.label);
|
||||||
|
fmt.close("a");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ArticleLinkScanner;
|
||||||
|
|
||||||
|
impl InlineRule for ArticleLinkScanner {
|
||||||
|
const MARKER: char = '[';
|
||||||
|
|
||||||
|
/// Find `[[Title@example.com]], return the position and split title/domain.
|
||||||
|
fn run(state: &mut InlineState) -> Option<(Node, usize)> {
|
||||||
|
let input = &state.src[state.pos..state.pos_max];
|
||||||
|
if !input.starts_with("[[") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
const SEPARATOR_LENGTH: usize = 2;
|
||||||
|
|
||||||
|
input.find("]]").and_then(|length| {
|
||||||
|
let start = state.pos + SEPARATOR_LENGTH;
|
||||||
|
let i = start + length - SEPARATOR_LENGTH;
|
||||||
|
let content = &state.src[start..i];
|
||||||
|
content.split_once('@').map(|(title, domain)| {
|
||||||
|
// Handle custom link label if provided, otherwise use title as label
|
||||||
|
let (domain, label) = domain.split_once('|').unwrap_or((domain, title));
|
||||||
|
let node = Node::new(ArticleLink {
|
||||||
|
label: label.to_string(),
|
||||||
|
title: title.to_string(),
|
||||||
|
domain: domain.to_string(),
|
||||||
|
});
|
||||||
|
(node, length + SEPARATOR_LENGTH)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_markdown_article_link() {
|
||||||
|
let parser = super::markdown_parser();
|
||||||
|
let plain = parser.parse("[[Title@example.com]]").render();
|
||||||
|
assert_eq!(
|
||||||
|
"<p><a href=\"/article/Title@example.com\">Title</a></p>\n",
|
||||||
|
plain
|
||||||
|
);
|
||||||
|
|
||||||
|
let with_label = parser
|
||||||
|
.parse("[[Title@example.com|Example Article]]")
|
||||||
|
.render();
|
||||||
|
assert_eq!(
|
||||||
|
"<p><a href=\"/article/Title@example.com\">Example Article</a></p>\n",
|
||||||
|
with_label
|
||||||
|
);
|
||||||
|
}
|
72
src/frontend/markdown/math_equation.rs
Normal file
72
src/frontend/markdown/math_equation.rs
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
use katex;
|
||||||
|
use markdown_it::{
|
||||||
|
parser::inline::{InlineRule, InlineState},
|
||||||
|
Node,
|
||||||
|
NodeValue,
|
||||||
|
Renderer,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct MathEquation {
|
||||||
|
equation: String,
|
||||||
|
display_mode: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NodeValue for MathEquation {
|
||||||
|
fn render(&self, _node: &Node, fmt: &mut dyn Renderer) {
|
||||||
|
let opts = katex::Opts::builder()
|
||||||
|
.throw_on_error(false)
|
||||||
|
.display_mode(self.display_mode)
|
||||||
|
.build()
|
||||||
|
.ok();
|
||||||
|
let katex_equation = opts.and_then(|o| katex::render_with_opts(&self.equation, o).ok());
|
||||||
|
fmt.text_raw(katex_equation.as_ref().unwrap_or(&self.equation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MathEquationScanner;
|
||||||
|
|
||||||
|
impl InlineRule for MathEquationScanner {
|
||||||
|
const MARKER: char = '$';
|
||||||
|
|
||||||
|
fn run(state: &mut InlineState) -> Option<(Node, usize)> {
|
||||||
|
let input = &state.src[state.pos..state.pos_max];
|
||||||
|
if !input.starts_with("$$") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut display_mode = false;
|
||||||
|
if input.starts_with("$$\n") || input.starts_with("$$ ") {
|
||||||
|
display_mode = true;
|
||||||
|
}
|
||||||
|
const SEPARATOR_LENGTH: usize = 2;
|
||||||
|
|
||||||
|
input[SEPARATOR_LENGTH - 1..].find("$$").map(|length| {
|
||||||
|
let start = state.pos + SEPARATOR_LENGTH;
|
||||||
|
let i = start + length - SEPARATOR_LENGTH + 1;
|
||||||
|
if start > i {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let content = &state.src[start..i];
|
||||||
|
let node = Node::new(MathEquation {
|
||||||
|
equation: content.to_string(),
|
||||||
|
display_mode,
|
||||||
|
});
|
||||||
|
Some((node, length + SEPARATOR_LENGTH + 1))
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
fn test_markdown_equation_katex() {
|
||||||
|
let parser = super::markdown_parser();
|
||||||
|
let rendered = parser
|
||||||
|
.parse("here is a math equation: $$E=mc^2$$. Pretty cool, right?")
|
||||||
|
.render();
|
||||||
|
assert_eq!(
|
||||||
|
"<p>here is a math equation: ".to_owned()
|
||||||
|
+ &katex::render("E=mc^2").unwrap()
|
||||||
|
+ ". Pretty cool, right?</p>\n",
|
||||||
|
rendered
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,13 +1,15 @@
|
||||||
use katex;
|
#![deny(clippy::unwrap_used)]
|
||||||
|
|
||||||
|
use article_link::ArticleLinkScanner;
|
||||||
use markdown_it::{
|
use markdown_it::{
|
||||||
parser::inline::{InlineRule, InlineState},
|
|
||||||
plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader},
|
plugins::cmark::block::{heading::ATXHeading, lheading::SetextHeader},
|
||||||
MarkdownIt,
|
MarkdownIt,
|
||||||
Node,
|
|
||||||
NodeValue,
|
|
||||||
Renderer,
|
|
||||||
};
|
};
|
||||||
|
use math_equation::MathEquationScanner;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
pub mod article_link;
|
||||||
|
pub mod math_equation;
|
||||||
pub mod toc;
|
pub mod toc;
|
||||||
|
|
||||||
pub fn render_markdown(text: &str) -> String {
|
pub fn render_markdown(text: &str) -> String {
|
||||||
|
@ -81,138 +83,3 @@ fn markdown_parser() -> MarkdownIt {
|
||||||
|
|
||||||
parser
|
parser
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct ArticleLink {
|
|
||||||
label: String,
|
|
||||||
title: String,
|
|
||||||
domain: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
// This defines how your custom node should be rendered.
|
|
||||||
impl NodeValue for ArticleLink {
|
|
||||||
fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
|
|
||||||
let mut attrs = node.attrs.clone();
|
|
||||||
|
|
||||||
let link = format!("/article/{}@{}", self.title, self.domain);
|
|
||||||
attrs.push(("href", link));
|
|
||||||
|
|
||||||
fmt.open("a", &attrs);
|
|
||||||
fmt.text(&self.label);
|
|
||||||
fmt.close("a");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ArticleLinkScanner;
|
|
||||||
|
|
||||||
impl InlineRule for ArticleLinkScanner {
|
|
||||||
const MARKER: char = '[';
|
|
||||||
|
|
||||||
/// Find `[[Title@example.com]], return the position and split title/domain.
|
|
||||||
fn run(state: &mut InlineState) -> Option<(Node, usize)> {
|
|
||||||
let input = &state.src[state.pos..state.pos_max];
|
|
||||||
if !input.starts_with("[[") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
const SEPARATOR_LENGTH: usize = 2;
|
|
||||||
|
|
||||||
input.find("]]").and_then(|length| {
|
|
||||||
let start = state.pos + SEPARATOR_LENGTH;
|
|
||||||
let i = start + length - SEPARATOR_LENGTH;
|
|
||||||
let content = &state.src[start..i];
|
|
||||||
content.split_once('@').map(|(title, domain)| {
|
|
||||||
// Handle custom link label if provided, otherwise use title as label
|
|
||||||
let (domain, label) = domain.split_once('|').unwrap_or((domain, title));
|
|
||||||
let node = Node::new(ArticleLink {
|
|
||||||
label: label.to_string(),
|
|
||||||
title: title.to_string(),
|
|
||||||
domain: domain.to_string(),
|
|
||||||
});
|
|
||||||
(node, length + SEPARATOR_LENGTH)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct MathEquation {
|
|
||||||
equation: String,
|
|
||||||
display_mode: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NodeValue for MathEquation {
|
|
||||||
fn render(&self, _node: &Node, fmt: &mut dyn Renderer) {
|
|
||||||
let opts = katex::Opts::builder()
|
|
||||||
.throw_on_error(false)
|
|
||||||
.display_mode(self.display_mode)
|
|
||||||
.build()
|
|
||||||
.ok();
|
|
||||||
let katex_equation = opts.and_then(|o| katex::render_with_opts(&self.equation, o).ok());
|
|
||||||
fmt.text_raw(katex_equation.as_ref().unwrap_or(&self.equation))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct MathEquationScanner;
|
|
||||||
|
|
||||||
impl InlineRule for MathEquationScanner {
|
|
||||||
const MARKER: char = '$';
|
|
||||||
|
|
||||||
fn run(state: &mut InlineState) -> Option<(Node, usize)> {
|
|
||||||
let input = &state.src[state.pos..state.pos_max];
|
|
||||||
if !input.starts_with("$$") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut display_mode = false;
|
|
||||||
if input.starts_with("$$\n") || input.starts_with("$$ ") {
|
|
||||||
display_mode = true;
|
|
||||||
}
|
|
||||||
const SEPARATOR_LENGTH: usize = 2;
|
|
||||||
|
|
||||||
input[SEPARATOR_LENGTH - 1..].find("$$").map(|length| {
|
|
||||||
let start = state.pos + SEPARATOR_LENGTH;
|
|
||||||
let i = start + length - SEPARATOR_LENGTH + 1;
|
|
||||||
if start > i {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let content = &state.src[start..i];
|
|
||||||
let node = Node::new(MathEquation {
|
|
||||||
equation: content.to_string(),
|
|
||||||
display_mode,
|
|
||||||
});
|
|
||||||
Some((node, length + SEPARATOR_LENGTH + 1))
|
|
||||||
})?
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_markdown_article_link() {
|
|
||||||
let parser = markdown_parser();
|
|
||||||
let plain = parser.parse("[[Title@example.com]]").render();
|
|
||||||
assert_eq!(
|
|
||||||
"<p><a href=\"/article/Title@example.com\">Title</a></p>\n",
|
|
||||||
plain
|
|
||||||
);
|
|
||||||
|
|
||||||
let with_label = parser
|
|
||||||
.parse("[[Title@example.com|Example Article]]")
|
|
||||||
.render();
|
|
||||||
assert_eq!(
|
|
||||||
"<p><a href=\"/article/Title@example.com\">Example Article</a></p>\n",
|
|
||||||
with_label
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[expect(clippy::unwrap_used)]
|
|
||||||
fn test_markdown_equation_katex() {
|
|
||||||
let parser = markdown_parser();
|
|
||||||
let rendered = parser
|
|
||||||
.parse("here is a math equation: $$E=mc^2$$. Pretty cool, right?")
|
|
||||||
.render();
|
|
||||||
assert_eq!(
|
|
||||||
"<p>here is a math equation: ".to_owned()
|
|
||||||
+ &katex::render("E=mc^2").unwrap()
|
|
||||||
+ ". Pretty cool, right?</p>\n",
|
|
||||||
rendered
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
@ -100,7 +100,7 @@ impl TocBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push(&mut self, level: u8, name: String, id: String) -> &str {
|
fn push(&mut self, level: u8, name: String, id: String) -> String {
|
||||||
debug_assert!(level >= 1);
|
debug_assert!(level >= 1);
|
||||||
|
|
||||||
self.fold_until(level);
|
self.fold_until(level);
|
||||||
|
@ -132,8 +132,10 @@ impl TocBuilder {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
let just_inserted = self.chain.last_mut().unwrap();
|
self.chain
|
||||||
&just_inserted.sec_number
|
.last()
|
||||||
|
.map(|l| l.sec_number.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue