Skip to content

Commit

Permalink
Djot support
Browse files Browse the repository at this point in the history
  • Loading branch information
treeman committed Jan 28, 2024
1 parent 200ee75 commit 101af7c
Show file tree
Hide file tree
Showing 9 changed files with 3,782 additions and 5 deletions.
3 changes: 3 additions & 0 deletions css/global/_code.scss
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ aside {
&.cpp::before {
content: "c++";
}
&.djot::before {
content: "djot";
}
&.bash::before {
content: "bash";
}
Expand Down
127 changes: 127 additions & 0 deletions drafts/blogging_in_djot_instead_of_markdown.dj
Original file line number Diff line number Diff line change
@@ -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
<div class="epigraph">
<blockquote>
<p>This is an epigraph</p>
</blockquote>
</div>
```

## Asides

```markdown
> This is an <aside>
{ :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
![](/images/configura14/octree1.png){ height=100 }
![](/images/configura14/octree2.png){ 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
5 changes: 3 additions & 2 deletions drafts/djottest.dj
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ Ordered list:

# Quotes and attributes

::: epigraph
> This is an epigraph
{ :epigraph }
:::

Text

Expand All @@ -95,8 +96,8 @@ This is [a notice](#)

Text

{ author="John Doe" }
> With author attribution
{ author=John Doe }

# Tables

Expand Down
6 changes: 3 additions & 3 deletions src/gen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
Expand All @@ -41,7 +41,7 @@ fn prototype_post(title: &str) -> String {
format!(
r#"---
title: "{title}"
tags: [Tag1, Tag2]
tags: ["Tag1", "Tag2"]
---
Lorem ipsum...
Expand Down
3 changes: 3 additions & 0 deletions src/markup/djot/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ mod auto_figures;
mod code;
mod div_transforms;
mod embed_youtube;
mod quote_transforms;
mod transform_headers;

use eyre::Result;
Expand All @@ -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<String> {
Expand All @@ -21,6 +23,7 @@ pub fn djot_to_html(djot: &str) -> Result<String> {
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)?;
Expand Down
113 changes: 113 additions & 0 deletions src/markup/djot/quote_transforms.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
use jotdown::{Attributes, Container, Event};

pub struct QuoteTransforms<'a, I: Iterator<Item = Event<'a>>> {
parent: I,
event_queue: Vec<Event<'a>>,
}

impl<'a, I: Iterator<Item = Event<'a>>> QuoteTransforms<'a, I> {
pub fn new(parent: I) -> Self {
Self {
parent,
event_queue: vec![],
}
}
}

impl<'a, I: Iterator<Item = Event<'a>>> Iterator for QuoteTransforms<'a, I> {
type Item = Event<'a>;

fn next(&mut self) -> Option<Self::Item> {
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#"<footer><span class="author">{}</span></footer>"#,
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<String> {
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"<aside>
<p>Text here</p>
</aside>
"
);

Ok(())
}

#[test]
fn test_quote_src() -> Result<()> {
let s = r#"
{author="John > Jane"}
> Text here
"#;
assert_eq!(
convert(s)?,
r#"
<blockquote>
<p>Text here</p>
<footer><span class="author">John &gt; Jane</span></footer>
</blockquote>
"#
);

Ok(())
}
}
1 change: 1 addition & 0 deletions src/markup/syntax_highlight.rs
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ fn syntect_lang_name(lang: &str) -> &str {
"shell" => "Shell-Unix-Generic",
"erlang" => "Erlang",
"js" => "JavaScript",
"djot" => "Djot",

// "pollen" => "",
x => x,
Expand Down
Loading

0 comments on commit 101af7c

Please sign in to comment.