Skip to content
Open
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
1 change: 1 addition & 0 deletions Cargo.lock

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

3 changes: 3 additions & 0 deletions components/config/src/config/markup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ pub struct Markdown {
pub insert_anchor_links: InsertAnchor,
/// Whether to enable GitHub-style alerts
pub github_alerts: bool,
/// Whether to enable Wikilinks style links, eg [[title]] like Obsidian
pub wikilinks: bool,
}

impl Markdown {
Expand Down Expand Up @@ -237,6 +239,7 @@ impl Default for Markdown {
lazy_async_image: false,
insert_anchor_links: InsertAnchor::None,
github_alerts: false,
wikilinks: false,
}
}
}
10 changes: 6 additions & 4 deletions components/markdown/benches/all.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::HashMap;

use ahash::AHashMap;
use config::{Config, Highlighting};
use criterion::{Criterion, criterion_group, criterion_main};
Expand Down Expand Up @@ -73,7 +71,8 @@ The end.
fn bench_without_highlighting(c: &mut Criterion) {
let mut tera = ZOLA_TERA.clone();
tera.set_fallback_prefixes(vec!["__zola_builtins/".to_string()]);
let permalinks = HashMap::new();
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let config = Config::default_for_test();

let colocated_assets = AHashMap::new();
Expand All @@ -82,6 +81,7 @@ fn bench_without_highlighting(c: &mut Criterion) {
config: &config,
permalinks: &permalinks,
colocated_assets: &colocated_assets,
wikilinks: &wikilinks,
lang: &config.default_language,
current_permalink: "https://www.example.com/bench/",
current_path: "bench.md",
Expand All @@ -96,7 +96,8 @@ fn bench_without_highlighting(c: &mut Criterion) {
fn bench_with_highlighting(c: &mut Criterion) {
let mut tera = ZOLA_TERA.clone();
tera.set_fallback_prefixes(vec!["__zola_builtins/".to_string()]);
let permalinks = HashMap::new();
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let mut config = Config::default_for_test();

let mut highlighting = Highlighting {
Expand All @@ -120,6 +121,7 @@ fn bench_with_highlighting(c: &mut Criterion) {
config: &config,
permalinks: &permalinks,
colocated_assets: &colocated_assets,
wikilinks: &wikilinks,
lang: &config.default_language,
current_permalink: "https://www.example.com/bench/",
current_path: "bench.md",
Expand Down
10 changes: 5 additions & 5 deletions components/markdown/src/context.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::HashMap;

use ahash::AHashMap;
use config::Config;
use pulldown_cmark::Options;
Expand All @@ -10,8 +8,9 @@ use utils::types::InsertAnchor;
pub struct MarkdownContext<'a> {
pub tera: &'a Tera,
pub config: &'a Config,
pub permalinks: &'a HashMap<String, String>,
pub permalinks: &'a AHashMap<String, String>,
pub colocated_assets: &'a AHashMap<String, (String, String)>,
pub wikilinks: &'a AHashMap<String, String>,
pub lang: &'a str,
pub current_permalink: &'a str,
pub current_path: &'a str,
Expand All @@ -26,8 +25,6 @@ impl<'a> MarkdownContext<'a> {
opts.insert(Options::ENABLE_STRIKETHROUGH);
opts.insert(Options::ENABLE_TASKLISTS);
opts.insert(Options::ENABLE_HEADING_ATTRIBUTES);
// TODO: enable it later
// opts.insert(Options::ENABLE_WIKILINKS);

if self.config.markdown.smart_punctuation {
opts.insert(Options::ENABLE_SMART_PUNCTUATION);
Expand All @@ -38,6 +35,9 @@ impl<'a> MarkdownContext<'a> {
if self.config.markdown.github_alerts {
opts.insert(Options::ENABLE_GFM);
}
if self.config.markdown.wikilinks {
opts.insert(Options::ENABLE_WIKILINKS);
}
opts
}
}
74 changes: 53 additions & 21 deletions components/markdown/src/markdown.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use std::collections::{HashMap, HashSet};
use std::sync::LazyLock;

use ahash::{AHashMap, AHashSet};
use gh_emoji::Replacer as EmojiReplacer;
use giallo::{ExtraHtmlContent, HtmlRenderer, ParsedFence, parse_markdown_fence};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, LinkType, Parser, Tag, TagEnd};
Expand Down Expand Up @@ -269,11 +269,11 @@ pub struct State<'a> {
footnote: Option<FootnoteDef>,
complete_footnotes: Vec<FootnoteDef>,
/// name -> (number, count)
footnote_numbers: HashMap<String, (usize, usize)>,
footnote_numbers: AHashMap<String, (usize, usize)>,
/// All heading IDs we've generated so far, for collision detection.
anchors: Vec<String>,
/// Explicit heading IDs we've already processed (for collision detection)
seen_explicit_ids: HashSet<String>,
seen_explicit_ids: AHashSet<String>,
toc: Vec<Heading>,
/// At which event we've seen <!-- summary -->
summary_index: Option<usize>,
Expand Down Expand Up @@ -416,7 +416,29 @@ impl<'a> State<'a> {
return Ok(link.to_string());
}

let result = if link.starts_with("@/") {
let result = if matches!(link_type, LinkType::WikiLink { .. }) {
let (key, anchor) = match link.split_once('#') {
Some((k, a)) => (k, Some(a.to_string())),
None => (link, None),
};
if let Some(md_path) = ctx.wikilinks.get(key) {
let permalink = &ctx.permalinks[md_path];
self.internal_links.push((md_path.clone(), anchor.clone()));
match anchor {
Some(a) => format!("{}#{}", permalink, a),
None => permalink.clone(),
}
} else {
let msg = format!("Broken wikilink `[[{}]]` in {}", link, ctx.current_path);
match ctx.config.link_checker.internal_level {
config::LinkCheckerLevel::Error => bail!(msg),
config::LinkCheckerLevel::Warn => {
log::warn!("{msg}");
link.to_string()
}
}
}
} else if link.starts_with("@/") {
if let Some(url) = resolve_colocated_asset(link, ctx) {
url
} else {
Expand Down Expand Up @@ -728,13 +750,15 @@ mod tests {
fn make_context<'a>(
config: &'a Config,
tera: &'a tera::Tera,
permalinks: &'a HashMap<String, String>,
permalinks: &'a AHashMap<String, String>,
wikilinks: &'a AHashMap<String, String>,
) -> MarkdownContext<'a> {
MarkdownContext {
tera,
config,
permalinks,
colocated_assets: &EMPTY_ASSETS,
wikilinks,
lang: &config.default_language,
current_permalink: "",
current_path: "",
Expand Down Expand Up @@ -762,7 +786,7 @@ mod tests {
fn colocated_asset_falls_back_to_default_language() {
let config = Config::default();
let tera = ZOLA_TERA.clone();
let mut permalinks = HashMap::new();
let mut permalinks = AHashMap::new();
permalinks.insert(
"blog/english-only/index.md".to_string(),
"https://example.com/blog/english-only/".to_string(),
Expand All @@ -772,7 +796,8 @@ mod tests {
"blog/english-only/img.png".to_string(),
("blog/english-only/index.md".to_string(), "img.png".to_string()),
);
let mut context = make_context(&config, &tera, &permalinks);
let wikilinks = AHashMap::new();
let mut context = make_context(&config, &tera, &permalinks, &wikilinks);
context.colocated_assets = &colocated_assets;
context.lang = "fr";

Expand All @@ -792,8 +817,9 @@ mod tests {
["<!-- more -->", "<!--more-->", "<!-- MORE -->", "<!--MORE-->", "<!--\t MoRe \t-->"];
let config = Config::default();
let tera = ZOLA_TERA.clone();
let permalinks = HashMap::new();
let context = make_context(&config, &tera, &permalinks);
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let context = make_context(&config, &tera, &permalinks, &wikilinks);
for more in mores {
let content = format!("{top}\n\n{more}\n\n{bottom}");
let rendered = State::default().render(&content, &context).unwrap();
Expand All @@ -814,8 +840,9 @@ mod tests {
let mut config = Config::default();
config.markdown.bottom_footnotes = true;
let tera = ZOLA_TERA.clone();
let permalinks = HashMap::new();
let context = make_context(&config, &tera, &permalinks);
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let context = make_context(&config, &tera, &permalinks, &wikilinks);

let content = "Some text *without* footnotes.\n\nOnly ~~fancy~~ formatting.";
let rendered = State::default().render(content, &context).unwrap();
Expand All @@ -827,8 +854,9 @@ mod tests {
let mut config = Config::default();
config.markdown.bottom_footnotes = true;
let tera = ZOLA_TERA.clone();
let permalinks = HashMap::new();
let context = make_context(&config, &tera, &permalinks);
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let context = make_context(&config, &tera, &permalinks, &wikilinks);

let content = "This text has a footnote[^1]\n [^1]:But it is meaningless.";
let rendered = State::default().render(content, &context).unwrap();
Expand All @@ -840,8 +868,9 @@ mod tests {
let mut config = Config::default();
config.markdown.bottom_footnotes = true;
let tera = ZOLA_TERA.clone();
let permalinks = HashMap::new();
let context = make_context(&config, &tera, &permalinks);
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let context = make_context(&config, &tera, &permalinks, &wikilinks);

let content = "This text has two[^2] footnotes[^1]\n[^1]: not sorted.\n[^2]: But they are";
let rendered = State::default().render(content, &context).unwrap();
Expand All @@ -853,8 +882,9 @@ mod tests {
let mut config = Config::default();
config.markdown.bottom_footnotes = true;
let tera = ZOLA_TERA.clone();
let permalinks = HashMap::new();
let context = make_context(&config, &tera, &permalinks);
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let context = make_context(&config, &tera, &permalinks, &wikilinks);

let content = "[^1]:It's before the reference.\n\n There is footnote definition?[^1]";
let rendered = State::default().render(content, &context).unwrap();
Expand All @@ -866,8 +896,9 @@ mod tests {
let mut config = Config::default();
config.markdown.bottom_footnotes = true;
let tera = ZOLA_TERA.clone();
let permalinks = HashMap::new();
let context = make_context(&config, &tera, &permalinks);
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let context = make_context(&config, &tera, &permalinks, &wikilinks);

let content = "This text has two[^1] identical footnotes[^1]\n[^1]: So one is present.\n[^2]: But another in not.";
let rendered = State::default().render(content, &context).unwrap();
Expand All @@ -879,8 +910,9 @@ mod tests {
let mut config = Config::default();
config.markdown.bottom_footnotes = true;
let tera = ZOLA_TERA.clone();
let permalinks = HashMap::new();
let context = make_context(&config, &tera, &permalinks);
let permalinks = AHashMap::new();
let wikilinks = AHashMap::new();
let context = make_context(&config, &tera, &permalinks, &wikilinks);

let content = "This text has a footnote[^1]\n[^1]: But the footnote has another footnote[^2].\n[^2]: That's it.";
let rendered = State::default().render(content, &context).unwrap();
Expand Down
17 changes: 13 additions & 4 deletions components/markdown/tests/common.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
#![allow(dead_code)]

use std::collections::HashMap;

use ahash::AHashMap;
use config::Config;
use errors::Result;
Expand All @@ -16,15 +14,25 @@ fn configurable_render(
) -> Result<Rendered> {
let mut tera = ZOLA_TERA.clone();

let mut permalinks = HashMap::new();
permalinks.insert("pages/about.md".to_owned(), "https://getzola.org/about/".to_owned());
let permalinks = AHashMap::from_iter([
("pages/about.md".to_owned(), "https://getzola.org/about/".to_owned()),
("guides/quickstart.md".to_owned(), "https://getzola.org/guides/quickstart/".to_owned()),
("about.md".to_owned(), "https://getzola.org/about/".to_owned()),
]);

let wikilinks = AHashMap::from_iter([
("guides/quickstart".to_owned(), "guides/quickstart.md".to_owned()),
("quickstart".to_owned(), "guides/quickstart.md".to_owned()),
("about".to_owned(), "about.md".to_owned()),
]);

tera.register_filter(
"markdown",
templates::filters::MarkdownFilter::new(
config.clone(),
permalinks.clone(),
AHashMap::new(),
wikilinks.clone(),
tera.clone(),
),
);
Expand All @@ -34,6 +42,7 @@ fn configurable_render(
config: &config,
permalinks: &permalinks,
colocated_assets: &colocated_assets,
wikilinks: &wikilinks,
lang: &config.default_language,
current_permalink: "https://www.getzola.org/test/",
current_path: "my_page.md",
Expand Down
43 changes: 39 additions & 4 deletions components/markdown/tests/markdown.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
use std::collections::HashMap;

use ahash::AHashMap;
use config::Config;
use markdown::{MarkdownContext, render_content};
Expand Down Expand Up @@ -109,14 +107,16 @@ fn can_insert_anchors() {
fn can_customise_anchor_template() {
let mut tera = ZOLA_TERA.clone();
tera.add_raw_template("anchor-link.html", " (in {{ lang }})").unwrap();
let permalinks_ctx = HashMap::new();
let permalinks_ctx = AHashMap::new();
let config = Config::default_for_test();
let colocated_assets = AHashMap::new();
let wikilinks_ctx = AHashMap::new();
let context = MarkdownContext {
tera: &tera,
config: &config,
permalinks: &permalinks_ctx,
colocated_assets: &colocated_assets,
wikilinks: &wikilinks_ctx,
lang: &config.default_language,
current_permalink: "",
current_path: "",
Expand All @@ -130,14 +130,16 @@ fn can_customise_anchor_template() {
fn can_customise_summary_template() {
let mut tera = ZOLA_TERA.clone();
tera.add_raw_template("summary-cutoff.html", " (in {{ lang }})").unwrap();
let permalinks_ctx = HashMap::new();
let permalinks_ctx = AHashMap::new();
let wikilinks_ctx = AHashMap::new();
let config = Config::default_for_test();
let colocated_assets = AHashMap::new();
let context = MarkdownContext {
tera: &tera,
config: &config,
permalinks: &permalinks_ctx,
colocated_assets: &colocated_assets,
wikilinks: &wikilinks_ctx,
lang: &config.default_language,
current_permalink: "",
current_path: "",
Expand Down Expand Up @@ -486,3 +488,36 @@ fn github_alerts() {
let body = common::render_with_config(&markdown, config).unwrap().body;
insta::assert_snapshot!(body);
}

#[test]
fn wikilink_resolution() {
let mut config = Config::default_for_test();
config.markdown.wikilinks = true;

let cases = vec![
"[[quickstart]]",
"[[guides/quickstart]]",
"[[quickstart#install]]",
"[[quickstart|Get Started]]",
];

let res = common::render_with_config(&cases.join("\n"), config).unwrap();
insta::assert_snapshot!(res.body);
assert!(res.internal_links.contains(&("guides/quickstart.md".into(), Some("install".into()))));
}

#[test]
fn wikilink_nonexistent() {
// warn level: renders with raw link
let mut config = Config::default_for_test();
config.markdown.wikilinks = true;
config.link_checker.internal_level = config::LinkCheckerLevel::Warn;
let body = common::render_with_config("[[nope]]", config).unwrap().body;
assert!(body.contains("href=\"nope\""), "body was: {body}");

// error level: returns Err
let mut config = Config::default_for_test();
config.markdown.wikilinks = true;
config.link_checker.internal_level = config::LinkCheckerLevel::Error;
assert!(common::render_with_config("[[nope]]", config).is_err());
}
Loading
Loading