post parsing with different comrak

This commit is contained in:
Michal Vanko 2024-03-02 13:12:55 +01:00
parent 4fd4373ed0
commit 49b92088b7
8 changed files with 188 additions and 130 deletions

View File

@ -10,8 +10,8 @@ askama = { version = "0.12", features = ["with-axum", "mime", "mime_guess"] }
askama_axum = "0.4.0" askama_axum = "0.4.0"
axum = "0.7.3" axum = "0.7.3"
chrono = { version = "0.4.31", features = ["serde"] } chrono = { version = "0.4.31", features = ["serde"] }
comrak = { version = "0.21", features = ["shortcodes"] }
gray_matter = "0.2.6" gray_matter = "0.2.6"
markdown = "1.0.0-alpha.16"
rss = "2.0.7" rss = "2.0.7"
serde = "1.0.195" serde = "1.0.195"
serde_json = "1.0.111" serde_json = "1.0.111"

View File

@ -26,7 +26,9 @@ async fn main() {
.init(); .init();
// build our application with a single route // build our application with a single route
let app = router::get_router().nest_service("/styles", ServeDir::new("styles")); let app = router::get_router()
.nest_service("/styles", ServeDir::new("styles"))
.nest_service("/images", ServeDir::new("../static/images"));
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
let app = app.layer(LiveReloadLayer::new()); let app = app.layer(LiveReloadLayer::new());

View File

@ -1,3 +1,4 @@
use crate::filters;
use askama::Template; use askama::Template;
use axum::{extract::Path, http::StatusCode}; use axum::{extract::Path, http::StatusCode};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
@ -28,6 +29,7 @@ pub struct PostMetadata {
pub struct PostTemplate { pub struct PostTemplate {
pub title: String, pub title: String,
pub body: String, pub body: String,
pub date: DateTime<Utc>,
pub site_footer: SiteFooter, pub site_footer: SiteFooter,
pub header_props: HeaderProps, pub header_props: HeaderProps,
} }
@ -42,6 +44,7 @@ pub async fn render_post(Path(post_id): Path<String>) -> Result<PostTemplate, St
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
Ok(PostTemplate { Ok(PostTemplate {
title: parsed.metadata.title, title: parsed.metadata.title,
date: parsed.metadata.date,
body: parsed.body, body: parsed.body,
site_footer, site_footer,
header_props: HeaderProps::default(), header_props: HeaderProps::default(),

View File

@ -2,8 +2,12 @@ use std::path::Path;
use axum::http::StatusCode; use axum::http::StatusCode;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use comrak::{
format_html,
nodes::{AstNode, NodeValue},
parse_document, Arena, Options,
};
use gray_matter::{engine::YAML, Matter}; use gray_matter::{engine::YAML, Matter};
use markdown::{to_html_with_options, CompileOptions, Constructs, Options, ParseOptions};
use serde::{de::DeserializeOwned, Deserialize, Deserializer}; use serde::{de::DeserializeOwned, Deserialize, Deserializer};
use tokio::fs; use tokio::fs;
@ -35,26 +39,6 @@ pub async fn parse_post<'de, Metadata: DeserializeOwned>(
// TODO Proper reasoning for an error // TODO Proper reasoning for an error
.map_err(|_| StatusCode::NOT_FOUND)?; .map_err(|_| StatusCode::NOT_FOUND)?;
let markdown_options = Options {
parse: ParseOptions {
constructs: Constructs {
frontmatter: true,
..Default::default()
},
..Default::default()
},
compile: CompileOptions {
allow_dangerous_html: true,
..Default::default()
},
..Default::default()
};
let body = to_html_with_options(&file_contents, &markdown_options).map_err(|reason| {
tracing::error!(reason);
return StatusCode::INTERNAL_SERVER_ERROR;
})?;
let matter = Matter::<YAML>::new(); let matter = Matter::<YAML>::new();
let metadata = matter let metadata = matter
.parse_with_struct::<Metadata>(&file_contents) .parse_with_struct::<Metadata>(&file_contents)
@ -63,6 +47,8 @@ pub async fn parse_post<'de, Metadata: DeserializeOwned>(
return StatusCode::INTERNAL_SERVER_ERROR; return StatusCode::INTERNAL_SERVER_ERROR;
})?; })?;
let body = parse_html(&metadata.content);
let filename = Path::new(path) let filename = Path::new(path)
.file_stem() .file_stem()
.ok_or(StatusCode::INTERNAL_SERVER_ERROR)? .ok_or(StatusCode::INTERNAL_SERVER_ERROR)?
@ -76,3 +62,51 @@ pub async fn parse_post<'de, Metadata: DeserializeOwned>(
slug: filename, slug: filename,
}); });
} }
fn parse_html(markdown: &str) -> String {
let mut options = Options::default();
options.parse.smart = true;
options.parse.relaxed_autolinks = true;
options.extension.strikethrough = true;
// options.extension.tagfilter = true;
options.extension.table = true;
options.extension.autolink = true;
options.extension.tasklist = true;
options.extension.superscript = true;
options.extension.header_ids = Some("".to_string());
options.extension.footnotes = false;
options.extension.description_lists = false;
options.extension.multiline_block_quotes = false;
options.extension.shortcodes = true;
options.render.hardbreaks = true;
options.render.github_pre_lang = true;
options.render.full_info_string = true;
options.render.unsafe_ = true;
// The returned nodes are created in the supplied Arena, and are bound by its lifetime.
let arena = Arena::new();
let root = parse_document(&arena, markdown, &options);
fn iter_nodes<'a, F>(node: &'a AstNode<'a>, f: &F)
where
F: Fn(&'a AstNode<'a>),
{
f(node);
for c in node.children() {
iter_nodes(c, f);
}
}
iter_nodes(root, &|node| match &mut node.data.borrow_mut().value {
&mut NodeValue::Text(ref mut _text) => {
// let orig = std::mem::replace(text, String::new());
// *text = orig.replace("my", "your");
}
_ => (),
});
let mut html = vec![];
format_html(root, &options, &mut html).unwrap();
return String::from_utf8(html).unwrap();
}

View File

@ -1,3 +1,20 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
@layer base {
.article-body {
h1 {
@apply px-4 text-2xl text-blue-900 my-2;
}
h2 {
@apply px-4 text-xl text-blue-900 my-2;
}
p {
@apply px-4 my-2;
}
pre {
@apply p-4 my-1 overflow-auto text-sm;
}
}
}

View File

@ -444,6 +444,63 @@ video {
display: none; display: none;
} }
.article-body {
h1 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
h1 {
padding-left: 1rem;
padding-right: 1rem;
}
h1 {
font-size: 1.5rem;
line-height: 2rem;
}
h1 {
--tw-text-opacity: 1;
color: rgb(30 58 138 / var(--tw-text-opacity));
}
h2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
h2 {
padding-left: 1rem;
padding-right: 1rem;
}
h2 {
font-size: 1.25rem;
line-height: 1.75rem;
}
h2 {
--tw-text-opacity: 1;
color: rgb(30 58 138 / var(--tw-text-opacity));
}
p {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
p {
padding-left: 1rem;
padding-right: 1rem;
}
pre {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
pre {
overflow: auto;
}
pre {
padding: 1rem;
}
pre {
font-size: 0.875rem;
line-height: 1.25rem;
}
}
*, ::before, ::after { *, ::before, ::after {
--tw-border-spacing-x: 0; --tw-border-spacing-x: 0;
--tw-border-spacing-y: 0; --tw-border-spacing-y: 0;
@ -544,64 +601,30 @@ video {
--tw-backdrop-sepia: ; --tw-backdrop-sepia: ;
} }
.m-3 {
margin: 0.75rem;
}
.m-1 { .m-1 {
margin: 0.25rem; margin: 0.25rem;
} }
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.my-5 {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
.my-8 {
margin-top: 2rem;
margin-bottom: 2rem;
}
.my-10 {
margin-top: 2.5rem;
margin-bottom: 2.5rem;
}
.my-12 {
margin-top: 3rem;
margin-bottom: 3rem;
}
.mx-4 { .mx-4 {
margin-left: 1rem; margin-left: 1rem;
margin-right: 1rem; margin-right: 1rem;
} }
.my-2 {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.mx-2 {
margin-left: 0.5rem;
margin-right: 0.5rem;
}
.mx-3 {
margin-left: 0.75rem;
margin-right: 0.75rem;
}
.mx-6 { .mx-6 {
margin-left: 1.5rem; margin-left: 1.5rem;
margin-right: 1.5rem; margin-right: 1.5rem;
} }
.my-12 {
margin-top: 3rem;
margin-bottom: 3rem;
}
.my-3 {
margin-top: 0.75rem;
margin-bottom: 0.75rem;
}
.mb-3 { .mb-3 {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
@ -661,15 +684,6 @@ video {
padding: 0.75rem; padding: 0.75rem;
} }
.p-2 {
padding: 0.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-4 { .px-4 {
padding-left: 1rem; padding-left: 1rem;
padding-right: 1rem; padding-right: 1rem;
@ -680,38 +694,10 @@ video {
padding-right: 1.25rem; padding-right: 1.25rem;
} }
.pr-2 {
padding-right: 0.5rem;
}
.pr-3 {
padding-right: 0.75rem;
}
.text-right { .text-right {
text-align: right; text-align: right;
} }
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-2xl {
font-size: 1.5rem;
line-height: 2rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.text-3xl { .text-3xl {
font-size: 1.875rem; font-size: 1.875rem;
line-height: 2.25rem; line-height: 2.25rem;
@ -722,6 +708,25 @@ video {
line-height: 2.5rem; line-height: 2.5rem;
} }
.text-base {
font-size: 1rem;
line-height: 1.5rem;
}
.text-lg {
font-size: 1.125rem;
line-height: 1.75rem;
}
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
.font-extrabold {
font-weight: 800;
}
.font-medium { .font-medium {
font-weight: 500; font-weight: 500;
} }
@ -730,23 +735,10 @@ video {
font-weight: 600; font-weight: 600;
} }
.font-bold {
font-weight: 700;
}
.font-extrabold {
font-weight: 800;
}
.italic { .italic {
font-style: italic; font-style: italic;
} }
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.text-blue-700 { .text-blue-700 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity)); color: rgb(29 78 216 / var(--tw-text-opacity));
@ -757,21 +749,21 @@ video {
color: rgb(30 64 175 / var(--tw-text-opacity)); color: rgb(30 64 175 / var(--tw-text-opacity));
} }
.text-gray-800 {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity));
}
.text-blue-950 {
--tw-text-opacity: 1;
color: rgb(23 37 84 / var(--tw-text-opacity));
}
.text-blue-900 { .text-blue-900 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(30 58 138 / var(--tw-text-opacity)); color: rgb(30 58 138 / var(--tw-text-opacity));
} }
.text-gray-600 {
--tw-text-opacity: 1;
color: rgb(75 85 99 / var(--tw-text-opacity));
}
.text-gray-800 {
--tw-text-opacity: 1;
color: rgb(31 41 55 / var(--tw-text-opacity));
}
.drop-shadow-md { .drop-shadow-md {
--tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06)); --tw-drop-shadow: drop-shadow(0 4px 3px rgb(0 0 0 / 0.07)) drop-shadow(0 2px 2px rgb(0 0 0 / 0.06));
filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);

View File

@ -3,9 +3,19 @@
{% block title %}{{title}}{% endblock %} {% block title %}{{title}}{% endblock %}
{% block content %} {% block content %}
<h1>{{title}}</h1> <article>
<header class="px-4">
<h1 class="text-3xl text-blue-900 mb-3">{{title}}</h1>
<section class="created-at m-1 text-right text-sm text-gray-600">
<span>Published on</span>
<time datetime="{date}"> {{date|pretty_date}} </time>
</section>
</header>
<article>{{body|escape("none")}}</article> <section class="article-body">
{{body|escape("none")}}
</section>
</article>
{# footer #} {# footer #}
{% endblock %} {% endblock %}