implemented automatic summary generation, and added timestamps in page list
This commit is contained in:
@@ -1,11 +0,0 @@
|
|||||||
# The Random Collection
|
|
||||||
|
|
||||||
Welcome to this automatically generated book! This is the **Summary** page, acting as the entry point for our small website.
|
|
||||||
|
|
||||||
### Table of Contents
|
|
||||||
1. [The Mascot: Ferris](./01-ferris.md) - A look at the Rust mascot.
|
|
||||||
2. [The Dark Elixir](./02-coffee.md) - A brief history of coffee.
|
|
||||||
3. [To the Stars](./03-space.md) - Facts about the Voyager 1 probe.
|
|
||||||
|
|
||||||
---
|
|
||||||
*Generated for the Axum Markdown Server test.*
|
|
||||||
71
src/main.rs
71
src/main.rs
@@ -8,10 +8,12 @@ use axum::{
|
|||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use pulldown_cmark::{Options, Parser as MarkdownParser, html};
|
use pulldown_cmark::{Options, Parser as MarkdownParser, html};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::{io::Cursor, path::PathBuf};
|
use std::{io::Cursor, path::PathBuf};
|
||||||
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
|
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};
|
||||||
use tera::{Context, Tera};
|
use tera::{Context, Tera};
|
||||||
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
|
|
||||||
mod codeblocks;
|
mod codeblocks;
|
||||||
use codeblocks::*;
|
use codeblocks::*;
|
||||||
@@ -20,6 +22,8 @@ lazy_static! {
|
|||||||
pub static ref TEMPLATES: Tera = {
|
pub static ref TEMPLATES: Tera = {
|
||||||
let mut tera = Tera::default();
|
let mut tera = Tera::default();
|
||||||
tera.add_raw_templates(vec![
|
tera.add_raw_templates(vec![
|
||||||
|
("_base.html", include_str!("../templates/_base.html")),
|
||||||
|
("home.html", include_str!("../templates/home.html")),
|
||||||
("page.html", include_str!("../templates/page.html")),
|
("page.html", include_str!("../templates/page.html")),
|
||||||
("style.css", include_str!("../templates/style.css")),
|
("style.css", include_str!("../templates/style.css")),
|
||||||
])
|
])
|
||||||
@@ -100,7 +104,54 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn render_summary(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
async fn render_summary(State(state): State<Arc<AppState>>) -> impl IntoResponse {
|
||||||
render_md_file("SUMMARY.md", state).await
|
let mut context = Context::new();
|
||||||
|
context.insert("title", "Pages");
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize)]
|
||||||
|
struct Page {
|
||||||
|
filename: String,
|
||||||
|
title: String,
|
||||||
|
datetime: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut pages: Vec<Page> = Vec::new();
|
||||||
|
if let Ok(mut entries) = tokio::fs::read_dir(&state.docs_dir).await {
|
||||||
|
while let Ok(Some(entry)) = entries.next_entry().await {
|
||||||
|
let path = entry.path();
|
||||||
|
let filename = entry.file_name();
|
||||||
|
let filename = filename.to_str().unwrap_or("<filename>");
|
||||||
|
|
||||||
|
let title = if let Ok(file) = tokio::fs::File::open(&path).await {
|
||||||
|
let mut reader = BufReader::new(file);
|
||||||
|
let mut line = String::new();
|
||||||
|
match reader.read_line(&mut line).await {
|
||||||
|
Ok(_) => line.trim_start_matches('#').trim().to_string(),
|
||||||
|
Err(_) => filename.to_string(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filename.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let datetime = filename
|
||||||
|
.split_once('@')
|
||||||
|
.and_then(|(_, ts_with_ext)| ts_with_ext.split('.').next())
|
||||||
|
.map(|dt| dt.to_string())
|
||||||
|
.unwrap_or_else(|| "Invalid Date".to_string());
|
||||||
|
|
||||||
|
pages.push(Page {
|
||||||
|
filename: filename.to_string(),
|
||||||
|
title,
|
||||||
|
datetime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context.insert("files", &pages);
|
||||||
|
|
||||||
|
match TEMPLATES.render("home.html", &context) {
|
||||||
|
Ok(rendered) => Html(rendered),
|
||||||
|
Err(e) => Html(format!("<h1>Template Error</h1><pre>{}</pre>", e)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_page(
|
async fn render_page(
|
||||||
@@ -112,7 +163,14 @@ async fn render_page(
|
|||||||
} else {
|
} else {
|
||||||
format!("{}.md", page)
|
format!("{}.md", page)
|
||||||
};
|
};
|
||||||
render_md_file(&filename, state).await
|
|
||||||
|
let file_path = state.docs_dir.join(&filename);
|
||||||
|
|
||||||
|
let content = match tokio::fs::read_to_string(&file_path).await {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(_) => return Html("<h1>404</h1><p>Page not found</p>".to_string()),
|
||||||
|
};
|
||||||
|
render_md_file(&content, &filename, state).await
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn serve_css() -> impl IntoResponse {
|
async fn serve_css() -> impl IntoResponse {
|
||||||
@@ -125,14 +183,7 @@ async fn serve_css() -> impl IntoResponse {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render_md_file(filename: &str, state: Arc<AppState>) -> Html<String> {
|
async fn render_md_file(content: &String, filename: &str, state: Arc<AppState>) -> Html<String> {
|
||||||
let file_path = state.docs_dir.join(filename);
|
|
||||||
|
|
||||||
let content = match tokio::fs::read_to_string(&file_path).await {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(_) => return Html("<h1>404</h1><p>Page not found</p>".to_string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut options = Options::empty();
|
let mut options = Options::empty();
|
||||||
options.insert(Options::ENABLE_TABLES);
|
options.insert(Options::ENABLE_TABLES);
|
||||||
options.insert(Options::ENABLE_FOOTNOTES);
|
options.insert(Options::ENABLE_FOOTNOTES);
|
||||||
|
|||||||
19
templates/_base.html
Normal file
19
templates/_base.html
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
{% block head %}
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>{{ title }}</title>
|
||||||
|
<link rel="stylesheet" href="/style.css">
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||||
|
{% endblock head %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div id="content">{% block content %}{% endblock content %}</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
41
templates/home.html
Normal file
41
templates/home.html
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
{% extends "_base.html" %}
|
||||||
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
|
{% block content %}
|
||||||
|
<h1>{{ title }}</h1>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
{% for file in files %}
|
||||||
|
<li>
|
||||||
|
<a href="./{{ file.filename }}">{{ file.title }}</a> -
|
||||||
|
<span class="local-date" data-timestamp="{{ file.datetime }}">
|
||||||
|
{{ file.datetime }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", function() {
|
||||||
|
const dateElements = document.querySelectorAll('.local-date');
|
||||||
|
|
||||||
|
dateElements.forEach(el => {
|
||||||
|
const rawValue = el.getAttribute('data-timestamp');
|
||||||
|
const unixTimestamp = parseInt(rawValue);
|
||||||
|
|
||||||
|
if (!isNaN(unixTimestamp)) {
|
||||||
|
const date = new Date(unixTimestamp * 1000);
|
||||||
|
|
||||||
|
el.textContent = date.toLocaleString(undefined, {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock content %}
|
||||||
@@ -1,21 +1,10 @@
|
|||||||
<!DOCTYPE html>
|
{% extends "_base.html" %}
|
||||||
<html lang="en">
|
{% block title %}{{ title }}{% endblock title %}
|
||||||
<head>
|
{% block content %}
|
||||||
<meta charset="UTF-8">
|
|
||||||
<title>{{ title }}</title>
|
|
||||||
<link rel="stylesheet" href="/style.css">
|
|
||||||
<!-- Font Awesome -->
|
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<article>
|
<article>
|
||||||
{{ content | safe }}
|
{{ content | safe }}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<nav>
|
|
||||||
<a href="/">Home (Summary)</a>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.querySelectorAll('pre[data-code]').forEach((block) => {
|
document.querySelectorAll('pre[data-code]').forEach((block) => {
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
@@ -51,5 +40,4 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
{% endblock content %}
|
||||||
</html>
|
|
||||||
|
|||||||
Reference in New Issue
Block a user