diff --git a/css/global/_code.scss b/css/global/_code.scss
index 3c25b6fb..59a4b794 100644
--- a/css/global/_code.scss
+++ b/css/global/_code.scss
@@ -56,6 +56,9 @@ aside {
&.cpp::before {
content: "c++";
}
+ &.djot::before {
+ content: "djot";
+ }
&.bash::before {
content: "bash";
}
diff --git a/drafts/blogging_in_djot_instead_of_markdown.dj b/drafts/blogging_in_djot_instead_of_markdown.dj
new file mode 100644
index 00000000..6aa34a12
--- /dev/null
+++ b/drafts/blogging_in_djot_instead_of_markdown.dj
@@ -0,0 +1,127 @@
+---
+title: "Blogging in Djot instead of Markdown"
+tags: ["Djot", "Webpage", "Rust"]
+---
+
+I recently happened to see an offhand comment on [Hacker News][] about a markup language called [Djot][].
+I don't really have any large issues with the {-Markdown-} [CommonMark][] I use to generate the posts for this website, but my brain saw a chance to get sidetracked yet again, and here we are.
+
+I spent some hours to port my hacky custom markup extensions to Djot and started to write my posts in Djot.
+Having used Markdown for many years, I'm still not comfortable with some of Djot's different syntax choices, but it ...
+
+# What and why Djot?
+
+The creator of Djot is John MacFarlane, the same philosophy professor that also created [Pandoc][] and [CommonMark][].
+These might be two of the most influential projects in the markup space, so you'd get the feeling that there should be a good reason for [Djot][]'s creation.
+
+The rationale for [Djot][] is specified in the [GitHub repo][Djot] and references his blog post [Beyond Markdown][] that inspired the whole project.
+
+If I should make an attempt to summarize the goals of [Djot][], I'd say that Djot tries to fix flaws of CommonMark in two areas:
+
+1. It's much easier to parse.
+
+ {author="John Macfarlane"}
+ > So, to fully specify emphasis parsing, we need additional rules. The 17 discouragingly complex rules in the CommonMark spec are intended to force the sorts of readings that humans will find most natural.
+
+ 17 rules just to parse emphasis sounds like fun times...
+
+ At least it's better than Markdown that's ambiguous.
+
+1. More features than CommonMark.
+
+ For instance proper support for footnotes, math, divs, and attributes that can be applied to any element.
+
+I'm sympathetic to the parsing problem, and for that alone I was willing to at least try it out.
+But what really sold me was first-class support for attributes and divs, something I've added ugly hacks to work around.
+
+If I'm going to bring up the negative parts, it's that for a regular user Djot mostly fixes edge-cases.
+For me, 95% of the time it's just like writing Markdown, and (unfortunately) it'll have difficulties overcoming the "good enough" barrier of the various flavors of Markdown.
+
+# Tools
+
+Given that Djot is a relatively young project, I expected the tooling to be lacking.
+There are for sure some things missing, but it wasn't so bad for my use-case.
+
+I found a Djot Sublime Text grammar I can use for syntax highlighting the blog,
+and there is Vim syntax highlighting in the [Djot repo][Djot].
+
+(Sad for those who don't use Vim I guess.)
+
+More annoying is that there's no treesitter implementation for Djot.
+This is unfortunate, as with treesitter I get proper syntax highlighting inside code blocks for Markdown and I have a general treesitter jump command that, for Markdown, jumps between headers with `]g` and `[g`.
+(In Rust it jumps between structs, implementations, enums, and functions.)
+
+Not a deal-breaker of course.
+Using the Markdown treesitter works well enough for the time being.
+(Maybe I need to explore how treesitter grammars work one day, and create one for Djot.)
+
+As for parsing I found [Jotdown][], which is a Rust library with an API inspired by [pulldown-cmark][], the library I use to parse CommonMark.
+
+# Abstracting away markup parsing
+
+# Customized markup
+
+For the blog I have some [markup transformations][] I apply to Markdown.
+This includes demoting headers (they only `h1` i want is the post title), embedding bare YouTube links and prettifying code.
+
+[markup transformations]: /blog/2022/08/29/rewriting_my_blog_in_rust_for_fun_and_profit/#markdown-transformations
+
+## Epigraph
+
+Before:
+
+```markdown
+> This is an epigraph
+{ :epigraph }
+```
+
+```djot
+::: epigraph
+> This is an epigraph
+:::
+```
+
+Produces:
+
+```html
+
+
+ This is an epigraph
+
+
+```
+
+## Asides
+
+```markdown
+> This is an
+{ :notice }
+```
+
+## Images and figures
+
+::: Flex
+/images/configura14/octree1.png
+/images/configura14/octree2.png
+:::
+
+```markdown
+::: Flex
+/images/configura14/octree1.png
+/images/configura14/octree2.png
+:::
+```
+
+```djot
+::: flex
+{ height=100 }
+{ height=100 }
+:::
+```
+
+[CommonMark]: https://commonmark.org/
+[Pandoc]: https://pandoc.org/
+[Beyond Markdown]: https://johnmacfarlane.net/beyond-markdown.html
+[Djot]: https://github.com/jgm/djot
+[pulldown-cmark]: https://crates.io/crates/pulldown-cmark
+[Jotdown]: https://github.com/hellux/jotdown
diff --git a/drafts/djottest.dj b/drafts/djottest.dj
index a54293dc..834150fa 100644
--- a/drafts/djottest.dj
+++ b/drafts/djottest.dj
@@ -70,8 +70,9 @@ Ordered list:
# Quotes and attributes
+::: epigraph
> This is an epigraph
-{ :epigraph }
+:::
Text
@@ -95,8 +96,8 @@ This is [a notice](#)
Text
+{ author="John Doe" }
> With author attribution
-{ author=John Doe }
# Tables
diff --git a/src/gen.rs b/src/gen.rs
index 501cb1c4..32d7bd8f 100644
--- a/src/gen.rs
+++ b/src/gen.rs
@@ -23,11 +23,11 @@ pub fn new_draft(title: String) -> Result<()> {
fn post_path(slug: &str) -> Utf8PathBuf {
let now = Utc::now();
- format!("posts/{}-{}.markdown", now.format("%Y-%m-%d"), slug).into()
+ format!("posts/{}-{}.dj", now.format("%Y-%m-%d"), slug).into()
}
fn draft_path(slug: &str) -> Utf8PathBuf {
- format!("drafts/{slug}.markdown").into()
+ format!("drafts/{slug}.dj").into()
}
fn new_prototype(title: &str, path: &Utf8Path) -> Result<()> {
@@ -41,7 +41,7 @@ fn prototype_post(title: &str) -> String {
format!(
r#"---
title: "{title}"
-tags: [Tag1, Tag2]
+tags: ["Tag1", "Tag2"]
---
Lorem ipsum...
diff --git a/src/markup/djot/mod.rs b/src/markup/djot/mod.rs
index ae3440a9..b562f707 100644
--- a/src/markup/djot/mod.rs
+++ b/src/markup/djot/mod.rs
@@ -2,6 +2,7 @@ mod auto_figures;
mod code;
mod div_transforms;
mod embed_youtube;
+mod quote_transforms;
mod transform_headers;
use eyre::Result;
@@ -11,6 +12,7 @@ use self::auto_figures::AutoFigures;
use self::code::{CodeBlockSyntaxHighlight, InlineCodeSyntaxHighlight};
use self::div_transforms::DivTransforms;
use self::embed_youtube::EmbedYoutube;
+use self::quote_transforms::QuoteTransforms;
use self::transform_headers::TransformHeaders;
pub fn djot_to_html(djot: &str) -> Result {
@@ -21,6 +23,7 @@ pub fn djot_to_html(djot: &str) -> Result {
let transformed = CodeBlockSyntaxHighlight::new(transformed);
let transformed = InlineCodeSyntaxHighlight::new(transformed);
let transformed = DivTransforms::new(transformed);
+ let transformed = QuoteTransforms::new(transformed);
let mut body = String::new();
html::Renderer::default().push(transformed, &mut body)?;
diff --git a/src/markup/djot/quote_transforms.rs b/src/markup/djot/quote_transforms.rs
new file mode 100644
index 00000000..eaed4f6e
--- /dev/null
+++ b/src/markup/djot/quote_transforms.rs
@@ -0,0 +1,113 @@
+use jotdown::{Attributes, Container, Event};
+
+pub struct QuoteTransforms<'a, I: Iterator- >> {
+ parent: I,
+ event_queue: Vec
>,
+}
+
+impl<'a, I: Iterator- >> QuoteTransforms<'a, I> {
+ pub fn new(parent: I) -> Self {
+ Self {
+ parent,
+ event_queue: vec![],
+ }
+ }
+}
+
+impl<'a, I: Iterator
- >> Iterator for QuoteTransforms<'a, I> {
+ type Item = Event<'a>;
+
+ fn next(&mut self) -> Option
{
+ if let Some(event) = self.event_queue.pop() {
+ return Some(event);
+ }
+
+ let author = match self.parent.next()? {
+ Event::Start(Container::Blockquote, attrs) => {
+ if let Some(author) = attrs.get("author") {
+ author.to_string()
+ } else {
+ return Some(Event::Start(Container::Blockquote, attrs));
+ }
+ }
+ other => return Some(other),
+ };
+
+ let mut events = Vec::new();
+ loop {
+ match self.parent.next()? {
+ // Yeah, don't support nesting for now.
+ Event::End(Container::Blockquote) => {
+ break;
+ }
+ other => events.push(other),
+ }
+ }
+
+ let html = Container::RawBlock { format: "html" };
+ self.event_queue.push(Event::End(Container::Blockquote));
+ self.event_queue.push(Event::End(html.clone()));
+ self.event_queue.push(Event::Str(
+ format!(
+ r#""#,
+ html_escape::encode_text(&author),
+ )
+ .into(),
+ ));
+ self.event_queue.push(Event::Start(html, Attributes::new()));
+ for x in events.into_iter().rev() {
+ self.event_queue.push(x);
+ }
+ Some(Event::Start(Container::Blockquote, Attributes::new()))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use eyre::Result;
+ use jotdown::{html, Parser, Render};
+
+ fn convert(s: &str) -> Result {
+ let parser = Parser::new(s);
+ let transformed = QuoteTransforms::new(parser);
+ let mut body = String::new();
+ html::Renderer::default().push(transformed, &mut body)?;
+ Ok(body)
+ }
+
+ #[test]
+ fn test_parse_notice() -> Result<()> {
+ let s = "::: notice
+Text here
+:::";
+ assert_eq!(
+ convert(s)?,
+ r"
+"
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn test_quote_src() -> Result<()> {
+ let s = r#"
+{author="John > Jane"}
+> Text here
+"#;
+ assert_eq!(
+ convert(s)?,
+ r#"
+
+Text here
+
+
+"#
+ );
+
+ Ok(())
+ }
+}
diff --git a/src/markup/syntax_highlight.rs b/src/markup/syntax_highlight.rs
index 816363db..94138e5a 100644
--- a/src/markup/syntax_highlight.rs
+++ b/src/markup/syntax_highlight.rs
@@ -206,6 +206,7 @@ fn syntect_lang_name(lang: &str) -> &str {
"shell" => "Shell-Unix-Generic",
"erlang" => "Erlang",
"js" => "JavaScript",
+ "djot" => "Djot",
// "pollen" => "",
x => x,
diff --git a/syntaxes/Djot.sublime-syntax b/syntaxes/Djot.sublime-syntax
new file mode 100644
index 00000000..1ea1fc30
--- /dev/null
+++ b/syntaxes/Djot.sublime-syntax
@@ -0,0 +1,3529 @@
+#
+# SPDX-License-Identifier: MIT
+#
+# Copyright (C) 2012 Brett Terpstra
+# Copyright (C) 2023 Shun Sakai
+#
+
+%YAML 1.2
+---
+#
+# This definition aims to meet Djot syntax reference:
+#
+# https://github.com/jgm/djot/blob/main/doc/syntax.md
+#
+# Syntax definition of `.sublime-syntax` file:
+#
+# https://www.sublimetext.com/docs/syntax.html
+#
+name: Djot
+file_extensions: [dj]
+scope: text.html.djot
+hidden: true
+
+variables:
+ atx_heading: (?:[ ]{,3}[#]{1,6}(?:[ \t]|$)) # between 0 and 3 spaces, followed 1 to 6 hashes, followed by at least one space or tab or by end of the line
+ atx_heading_space: (?:(?=[ \t]+#+[ \t]*$)|[ \t]+|$) # consume spaces only if heading is not empty to ensure `atx_heading_end` can fully match closing hashes
+ atx_heading_end: (?:[ \t]+(#+))?[ \t]*($\n?) # \n is optional so ## is matched as end punctuation in new document (at eof)
+ setext_escape: ^(?=[ ]{,3}(?:=+|-+)\s*$) # between 0 and 3 spaces, followed by at least one hyphon or equal sign (setext underline can be of any length)
+
+ block_quote: (?:[ ]{,3}(>)[ ]?) # between 0 and 3 spaces, followed by a greater than sign, (followed by any character or the end of the line = "only care about optional space!")
+ indented_code_block: (?:[ ]{4}|[ ]{0,3}\t) # a visual tab of width 4 consisting of 4 spaces or 0 to 3 spaces followed by 1 tab
+
+ first_list_item: (?:[ ]{,3}(?:1[.)]|[*+-])\s) # between 0 and 3 spaces, followed by either: at least one integer and a full stop or a parenthesis, or (a star, plus or dash), followed by whitespace
+ list_item: (?:[ ]{,3}(?:\d{1,9}[.)]|[*+-])\s) # between 0 and 3 spaces, followed by either: at least one integer and a full stop or a parenthesis, or (a star, plus or dash), followed by whitespace
+
+ thematic_break: |-
+ (?x:
+ [ ]{,3} # between 0 to 3 spaces
+ (?: # followed by one of the following:
+ [-](?:[ \t]*[-]){2,} # - a dash, followed by the following at least twice: any number of spaces or tabs followed by a dash
+ | [*](?:[ \t]*[*]){2,} # - a star, followed by the following at least twice: any number of spaces or tabs followed by a star
+ | [_](?:[ \t]*[_]){2,} # - an underscore, followed by the following at least twice: any number of spaces or tabs followed by an underscore
+ )
+ [ \t]*$ # followed by any number of tabs or spaces, followed by the end of the line
+ )
+
+ backticks: |-
+ (?x:
+ (`{4})(?![\s`])(?:[^`]+(?=`)|(?!`{4})`+(?!`))+(`{4})(?!`) # 4 backticks, followed by at least one non whitespace, non backtick character, followed by (less than 4 backticks, or at least one non backtick character) at least once, followed by exactly 4 backticks
+ | (`{3})(?![\s`])(?:[^`]+(?=`)|(?!`{3})`+(?!`))+(`{3})(?!`) # 3 backticks, followed by at least one non whitespace, non backtick character, followed by (less than 3 backticks, or at least one non backtick character) at least once, followed by exactly 3 backticks
+ | (`{2})(?![\s`])(?:[^`]+(?=`)|(?!`{2})`+(?!`))+(`{2})(?!`) # 2 backticks, followed by at least one non whitespace, non backtick character, followed by (less than 2 backticks, or at least one non backtick character) at least once, followed by exactly 2 backticks
+ | (`{1})(?![\s`])(?:[^`]+(?=`)|(?!`{1})`+(?!`))+(`{1})(?!`) # 1 backtick, followed by at least one non whitespace, non backtick character, followed by ( at least one non backtick character) at least once, followed by exactly 1 backtick
+ )
+ escapes: \\[-+*/!"#$%&'(),.:;<=>?@\[\\\]^_`{|}~]
+
+ balance_square_brackets: |-
+ (?x:
+ (?:
+ (?:{{escapes}})+ # escape characters
+ | [^\[\]`\\]+(?=[\[\]`\\]|$) # anything that isn't a square bracket or a backtick or the start of an escape character
+ | {{backticks}} # inline code
+ | \[(?: # nested square brackets (one level deep)
+ [^\[\]`]+(?=[\[\]`]) # anything that isn't a square bracket or a backtick
+ {{backticks}}? # balanced backticks
+ )*\] # closing square bracket
+ )+
+ )
+ balance_square_brackets_and_emphasis: |-
+ (?x:
+ (?:
+ (?:{{escapes}})+ # escape characters
+ | [^\[\]`\\_*]+(?=[\[\]`\\_*]|$) # anything that isn't a square bracket, a backtick, the start of an escape character, or an emphasis character
+ | {{backticks}} # inline code
+ | \[(?: # nested square brackets (one level deep)
+ [^\[\]`]+(?=[\[\]`]) # anything that isn't a square bracket or a backtick
+ {{backticks}}? # balanced backticks
+ )*\] # closing square bracket
+ )+ # at least one character
+ )
+ balance_square_brackets_pipes_and_emphasis: |-
+ (?x:
+ (?:
+ (?:{{escapes}})+ # escape characters
+ | [^\[\]`\\_*|]+(?=[\[\]`\\_*|]|$) # anything that isn't a square bracket, a backtick, the start of an escape character, or an emphasis character
+ | {{backticks}} # inline code
+ | \[(?: # nested square brackets (one level deep)
+ [^\[\]`]+(?=[\[\]`]) # anything that isn't a square bracket or a backtick
+ {{backticks}}? # balanced backticks
+ )*\] # closing square bracket
+ )+ # at least one character
+ )
+ balanced_emphasis: |-
+ (?x:
+ \* (?!\*){{balance_square_brackets_and_emphasis}}+\* (?!\*)
+ | \*\* {{balance_square_brackets_and_emphasis}}+\*\*
+ | _ (?!_) {{balance_square_brackets_and_emphasis}}+_ (?!_)
+ | __ {{balance_square_brackets_and_emphasis}}+__
+ )
+
+ table_cell: |-
+ (?x:
+ # Pipes inside other inline spans (such as emphasis, code, etc.) will not break a cell,
+ # emphasis in table cells can't span multiple lines
+ (?:
+ {{balance_square_brackets_pipes_and_emphasis}}
+ | {{balanced_emphasis}}
+ )+ # at least one character
+ )
+ table_first_row: |-
+ (?x:
+ # at least 2 non-escaped pipe chars on the line
+ (?:{{table_cell}}?\|){2}
+
+ # something other than whitespace followed by a pipe char or hyphon,
+ # followed by something other than whitespace and the end of the line
+ | (?! \s*\-\s+ | \s+\|){{table_cell}}\|(?!\s+$)
+ )
+
+ fenced_code_block_start: |-
+ (?x:
+ ([ \t]*)
+ (
+ (`){3,} # 3 or more backticks
+ (?![^`]*`) # not followed by any more backticks on the same line
+ | # or
+ (~){3,} # 3 or more tildas
+ )
+ \s* # allow for whitespace between code block start and info string
+ )
+ fenced_code_block_language: |-
+ (?x: # first word of an infostring is used as language specifier
+ (
+ [[:alpha:]] # starts with a letter to make sure not to hit any attribute annotation
+ [^`\s]* # optionally followed by any nonwhitespace character (except backticks)
+ )
+ )
+ fenced_code_block_trailing_infostring_characters: |-
+ (?x:
+ (
+ \s* # any whitespace, or ..
+ |
+ \s[^`]* # any characters (except backticks), separated by whitespace ...
+ )
+ $\n? # ... until EOL
+ )
+ fenced_code_block_end: |-
+ (?x:
+ [ \t]*
+ (
+ \2 # the backtick/tilde combination that opened the code fence
+ (?:\3|\4)* # plus optional additional closing characters
+ )
+ \s*$ # any amount of whitespace until EOL
+ )
+ fenced_code_block_escape: ^{{fenced_code_block_end}}
+
+ # https://spec.commonmark.org/0.30/#email-autolink
+ email_domain_commonmark: '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?'
+ email_user_commonmark: '[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+'
+
+ # https://spec.commonmark.org/0.30/#html-blocks
+ html_block: |-
+ (?x:
+ [ ]{,3}
+ (?:
+ {{html_tag_block_end_at_close_tag}} # html block type 1
+ | {{html_tag_block_end_at_blank_line}} # html block type 6
+ | {{html_block_open_tag}} # html block type 7
+ | {{html_block_close_tag}} # html block type 7
+ | {{html_block_comment}} # html block type 2
+ | {{html_block_decl}} # html block type 4
+ | {{html_block_cdata}} # html block type 5
+ | {{html_block_preprocessor}} # html block type 3
+ )
+ )
+ html_block_comment: