Compare commits
16 Commits
Author | SHA1 | Date |
---|---|---|
Tomáš Mládek | 1b1914f351 | |
Tomáš Mládek | e4dd5954b4 | |
Tomáš Mládek | 0a6fcba373 | |
Tomáš Mládek | 7ee85e5c62 | |
Tomáš Mládek | 7e4659cfed | |
Tomáš Mládek | 703b8b1388 | |
Tomáš Mládek | e90d3c8807 | |
Tomáš Mládek | 0b1c2e29a5 | |
Tomáš Mládek | 162edb25a9 | |
Tomáš Mládek | 86b414262f | |
Tomáš Mládek | 71ce442f64 | |
Tomáš Mládek | 188548900c | |
Tomáš Mládek | f5d1d02e65 | |
Tomáš Mládek | a2debf5b2c | |
Tomáš Mládek | f5d6c9c05b | |
Tomáš Mládek | bca533c701 |
|
@ -980,7 +980,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gardenserver"
|
||||
version = "0.9.0"
|
||||
version = "0.10.4-alpha.0"
|
||||
dependencies = [
|
||||
"actix",
|
||||
"actix-files",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "gardenserver"
|
||||
version = "0.9.0"
|
||||
version = "0.10.4-alpha.0"
|
||||
authors = ["Tomáš Mládek <t@mldk.cz>"]
|
||||
edition = "2018"
|
||||
publish = false
|
||||
|
|
109
src/main.rs
109
src/main.rs
|
@ -14,6 +14,7 @@ use percent_encoding::{percent_decode_str, utf8_percent_encode};
|
|||
use pulldown_cmark::{html, Event, Options, Parser, Tag};
|
||||
use regex::{Captures, Regex};
|
||||
use slug::slugify;
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::HashMap;
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
|
@ -44,6 +45,7 @@ struct MutableState {
|
|||
struct GardenCache {
|
||||
pages: HashMap<String, ParsedPage>,
|
||||
files: Vec<PathBuf>,
|
||||
tags: HashMap<String, u32>,
|
||||
}
|
||||
|
||||
impl Default for GardenCache {
|
||||
|
@ -51,6 +53,7 @@ impl Default for GardenCache {
|
|||
GardenCache {
|
||||
pages: HashMap::new(),
|
||||
files: vec![],
|
||||
tags: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -255,7 +258,7 @@ async fn render(
|
|||
links.push(
|
||||
[
|
||||
("source".to_string(), normalized_path.clone()),
|
||||
("target".to_string(), link.clone()),
|
||||
("target".to_string(), link),
|
||||
]
|
||||
.iter()
|
||||
.cloned()
|
||||
|
@ -297,6 +300,12 @@ async fn render(
|
|||
data.title.as_ref().unwrap_or(&"Digital Garden".to_string()),
|
||||
);
|
||||
context.insert("files", &cache.files);
|
||||
|
||||
let mut tags: Vec<(&String, &u32)> = cache.tags.iter().collect();
|
||||
tags.sort_by_key(|(t, _)| *t);
|
||||
tags.sort_by_key(|(_, n)| Reverse(*n));
|
||||
context.insert("tags", &tags);
|
||||
|
||||
context.insert(
|
||||
"recently_changed",
|
||||
&recently_changed
|
||||
|
@ -387,6 +396,7 @@ fn update_garden<P: AsRef<Path>>(
|
|||
}
|
||||
|
||||
let mut pages = current.pages;
|
||||
let mut tags = current.tags;
|
||||
|
||||
let markdown_paths = files
|
||||
.iter()
|
||||
|
@ -407,7 +417,7 @@ fn update_garden<P: AsRef<Path>>(
|
|||
let mut file_string = String::new();
|
||||
file.read_to_string(&mut file_string)?;
|
||||
let markdown_source = preprocess_markdown(file_string);
|
||||
let result = GardenParser::parse(&markdown_source)?;
|
||||
let result = parse_garden(&markdown_source)?;
|
||||
|
||||
pages.insert(
|
||||
String::from(path.to_str().unwrap()),
|
||||
|
@ -428,73 +438,70 @@ fn update_garden<P: AsRef<Path>>(
|
|||
},
|
||||
},
|
||||
);
|
||||
|
||||
result.tags.into_iter().for_each(|tag| {
|
||||
*tags.entry(tag).or_insert(0) += 1;
|
||||
});
|
||||
}
|
||||
|
||||
let result = GardenCache { pages, files };
|
||||
let result = GardenCache { pages, files, tags };
|
||||
trace!("{:#?}", result);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
struct GardenParser<'a> {
|
||||
parser: Parser<'a>,
|
||||
last_nontext_event: Option<Event<'a>>,
|
||||
current_top_heading: u32,
|
||||
top_heading_text: &'a mut Option<String>,
|
||||
links: &'a mut Vec<String>,
|
||||
}
|
||||
|
||||
struct ParseResult {
|
||||
html: String,
|
||||
title: Option<String>,
|
||||
links: Vec<String>,
|
||||
tags: Vec<String>,
|
||||
}
|
||||
|
||||
impl<'a> GardenParser<'a> {
|
||||
fn parse<S: AsRef<str>>(text: &'a S) -> anyhow::Result<ParseResult> {
|
||||
let mut title: Option<String> = None;
|
||||
let mut links: Vec<String> = vec![];
|
||||
fn parse_garden<S: AsRef<str>>(text: S) -> anyhow::Result<ParseResult> {
|
||||
let mut current_top_heading = 999;
|
||||
let mut top_heading_text: Option<String> = None;
|
||||
|
||||
let parser = GardenParser {
|
||||
parser: Parser::new_ext(text.as_ref(), Options::all()),
|
||||
last_nontext_event: None,
|
||||
current_top_heading: 999,
|
||||
top_heading_text: &mut title,
|
||||
links: &mut links,
|
||||
};
|
||||
let mut last_nontext_event: Option<Event> = None;
|
||||
|
||||
let mut html = String::new();
|
||||
html::push_html(&mut html, parser);
|
||||
html = postprocess_html(html)?;
|
||||
let mut links: Vec<String> = vec![];
|
||||
let mut tags: Vec<String> = vec![];
|
||||
|
||||
Ok(ParseResult { html, title, links })
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for GardenParser<'a> {
|
||||
type Item = Event<'a>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let event = self.parser.next();
|
||||
|
||||
if let Some(event) = &event {
|
||||
if let Event::Start(Tag::Link(_, str, _)) = &event {
|
||||
self.links.push(str.to_string());
|
||||
}
|
||||
|
||||
if let Some(Event::Start(Tag::Heading(hl))) = self.last_nontext_event {
|
||||
if hl < self.current_top_heading {
|
||||
self.current_top_heading = hl;
|
||||
if let Event::Text(str) = &event {
|
||||
*self.top_heading_text = Some(str.clone().into_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.last_nontext_event = Some(event.clone());
|
||||
let parser = Parser::new_ext(text.as_ref(), Options::all()).map(|event| {
|
||||
if let Event::Start(Tag::Link(_, dest, _)) = &event {
|
||||
links.push(dest.to_string());
|
||||
}
|
||||
|
||||
if let Some(Event::Start(Tag::Link(_, _, _))) = &last_nontext_event {
|
||||
if let Event::Text(str) = &event {
|
||||
if str.starts_with('#') {
|
||||
tags.push(str[1..].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Event::Start(Tag::Heading(hl))) = last_nontext_event {
|
||||
if hl < current_top_heading {
|
||||
current_top_heading = hl;
|
||||
if let Event::Text(str) = &event {
|
||||
top_heading_text = Some(str.clone().into_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
last_nontext_event = Some(event.clone());
|
||||
|
||||
event
|
||||
}
|
||||
});
|
||||
|
||||
let mut html = String::new();
|
||||
html::push_html(&mut html, parser);
|
||||
html = postprocess_html(html)?;
|
||||
|
||||
Ok(ParseResult {
|
||||
html,
|
||||
title: top_heading_text,
|
||||
links,
|
||||
tags,
|
||||
})
|
||||
}
|
||||
|
||||
fn preprocess_markdown(string: String) -> String {
|
||||
|
|
|
@ -1,5 +1,15 @@
|
|||
body {
|
||||
@import url('https://rsms.me/inter/inter.css');
|
||||
|
||||
@supports (font-variation-settings: normal) {
|
||||
html {
|
||||
font-family: 'Inter var', sans-serif;
|
||||
font-feature-settings: "frac", "cpsp", "ss02", "ss03";
|
||||
}
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: 'Inter', sans-serif;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
|
@ -183,6 +193,10 @@ nav .graph-view {
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.tag .count {
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 1em 0;
|
||||
color: gray;
|
||||
|
|
|
@ -73,6 +73,22 @@
|
|||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% if tags %}
|
||||
<section>
|
||||
<h2>Tags</h2>
|
||||
<ul>
|
||||
{% for tag_cnt in tags %}
|
||||
{% if tag_cnt.1 > 1 %}
|
||||
<li class="tag">
|
||||
<a href="/{{tag_cnt.0}}">
|
||||
<span class="label">#{{tag_cnt.0}}</span> <span class="count">({{tag_cnt.1}})</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
<section class="graph-view">
|
||||
<a href="/!graph">Graph view (beta)</a>
|
||||
</section>
|
||||
|
|
Loading…
Reference in New Issue