Initial.
This commit is contained in:
commit
3c96b5efb9
13 changed files with 3072 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
.cache/*
|
||||
.jj/*
|
||||
_posts/*
|
||||
_static/*
|
||||
scratch/*
|
||||
target/*
|
||||
config.toml
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
[submodule "vendor/rusticnotion"]
|
||||
path = vendor/rusticnotion
|
||||
url = git@github.com:arrdem/rusticnotion.git
|
1894
Cargo.lock
generated
Normal file
1894
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
|
@ -0,0 +1,25 @@
|
|||
[package]
|
||||
name = "blog"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
name = "blog"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
path = "src/main.rs"
|
||||
name = "blog"
|
||||
doc = false
|
||||
|
||||
[dependencies]
|
||||
chrono = "0.4.41"
|
||||
clap = { version = "4.5.39", features = ["derive", "string"] }
|
||||
filetime = "0.2.25"
|
||||
log = "0.4.27"
|
||||
miette = { version = "7.6.0", features = ["fancy"] }
|
||||
rusticnotion = { path = "vendor/rusticnotion" }
|
||||
serde = { version = "1.0.219", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.140"
|
||||
tokio = { version = "1.45.1", features = ["macros", "rt-multi-thread"] }
|
||||
toml = "0.8.22"
|
8
notes.md
Normal file
8
notes.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# Document Title
|
||||
|
||||
https://github.com/miyuchina/mistletoe
|
||||
https://github.com/emoriarty/jekyll-notion
|
||||
|
||||
https://developers.notion.com/reference/block
|
||||
|
||||
https://github.com/ramnes/notion-sdk-py
|
41
src/cache/mod.rs
vendored
Normal file
41
src/cache/mod.rs
vendored
Normal file
|
@ -0,0 +1,41 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use miette::IntoDiagnostic;
|
||||
use miette::Result;
|
||||
use serde::Serialize;
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::fs;
|
||||
|
||||
pub struct Cache {
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
pub struct CacheKey {
|
||||
id: PathBuf,
|
||||
mtime: DateTime<Utc>,
|
||||
}
|
||||
|
||||
pub trait Cacheable {
|
||||
fn to_cache_key(self) -> CacheKey;
|
||||
}
|
||||
|
||||
impl Cache {
|
||||
pub fn is_fresh(self: &Cache, cacheable: impl Cacheable) -> Result<bool> {
|
||||
let key = cacheable.to_cache_key();
|
||||
let p = self.root.join(key.id);
|
||||
if !fs::exists(&p).into_diagnostic()? {
|
||||
return Ok(false);
|
||||
}
|
||||
let meta = fs::metadata(&p).into_diagnostic()?;
|
||||
Ok(meta.modified().unwrap() == key.mtime.into())
|
||||
}
|
||||
|
||||
pub fn put<T>(self: &Cache, cacheable: impl Cacheable, value: &T)
|
||||
where
|
||||
T: ?Sized + Serialize,
|
||||
{
|
||||
let p = self.root.join(cacheable.to_cache_key().id);
|
||||
let file = File::create(p).unwrap();
|
||||
serde_json::to_writer(file, value).unwrap();
|
||||
}
|
||||
}
|
2
src/lib.rs
Normal file
2
src/lib.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
pub mod cache;
|
||||
pub mod render;
|
145
src/main.rs
Normal file
145
src/main.rs
Normal file
|
@ -0,0 +1,145 @@
|
|||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::fs::FileTimes;
|
||||
use std::os::macos::fs::FileTimesExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::option::Option;
|
||||
use std::str::FromStr;
|
||||
use std::time::SystemTime;
|
||||
|
||||
use clap::Parser;
|
||||
use filetime::set_file_mtime;
|
||||
use miette::miette;
|
||||
use miette::Context;
|
||||
use miette::IntoDiagnostic;
|
||||
use miette::Result;
|
||||
use rusticnotion::models::block::Block;
|
||||
use rusticnotion::models::paging::PagingCursor;
|
||||
use rusticnotion::models::search::DatabaseQuery;
|
||||
use rusticnotion::models::paging::Pageable;
|
||||
use rusticnotion::models::search::NotionSearch;
|
||||
use rusticnotion::models::search::SortDirection::Descending;
|
||||
use rusticnotion::models::paging::Paging;
|
||||
use rusticnotion::ids::BlockId;
|
||||
|
||||
use rusticnotion::models::ListResponse;
|
||||
use rusticnotion::NotionApi;
|
||||
use rusticnotion::ids::*;
|
||||
|
||||
use tokio;
|
||||
use serde::Deserialize;
|
||||
|
||||
use filetime::set_file_times;
|
||||
use filetime::FileTime;
|
||||
|
||||
use blog::render::html::from_blocks;
|
||||
|
||||
fn default_cache_dir() -> PathBuf {
|
||||
return PathBuf::from(".cache/aspect-blog");
|
||||
}
|
||||
|
||||
fn default_config_file() -> PathBuf {
|
||||
return PathBuf::from("blog.toml");
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
/// Source Python interpreter path to symlink into the venv.
|
||||
#[arg(long, default_value=default_config_file().into_os_string())]
|
||||
config: PathBuf,
|
||||
|
||||
// Annoyingly PathBuf doesn't implement Display so we can't use it directly as the
|
||||
#[arg(long, default_value=default_cache_dir().into_os_string())]
|
||||
cache_dir: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Config {
|
||||
notion_key: String,
|
||||
blog_database: DatabaseId,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let args = Args::parse();
|
||||
|
||||
if !args.config.exists() {
|
||||
return Err(miette!(format!(
|
||||
"Specified config file {} doesn't exist",
|
||||
args.config.to_str().unwrap()
|
||||
)));
|
||||
}
|
||||
|
||||
let config: Config = {
|
||||
toml::from_str(
|
||||
&fs::read_to_string(&args.config)
|
||||
.into_diagnostic()
|
||||
.wrap_err(format!(
|
||||
"Unable to read config file {}",
|
||||
&args.config.to_str().unwrap()
|
||||
))
|
||||
.unwrap(),
|
||||
)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to decode TOML config")
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
let client = NotionApi::new(config.notion_key)
|
||||
.unwrap();
|
||||
|
||||
let blogdb = client.get_database(&config.blog_database)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut cursor: Option<PagingCursor> = None;
|
||||
|
||||
loop {
|
||||
let batch = client.query_database(
|
||||
blogdb.clone(),
|
||||
DatabaseQuery{
|
||||
filter: None,
|
||||
paging: None,
|
||||
sorts: None
|
||||
}.start_from(cursor)
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for entry in batch.results {
|
||||
|
||||
let cache_entry = args.cache_dir.join(format!("{}.json", entry.id));
|
||||
if !fs::exists(&cache_entry).unwrap_or(false) || {
|
||||
let meta = fs::metadata(&cache_entry).unwrap();
|
||||
meta.modified().unwrap() != entry.last_edited_time.into()
|
||||
} {
|
||||
println!("Page {}: \"{}\" has changed or is uncached; refreshing...", entry.id, entry.title().unwrap_or("(nothing)".to_string()));
|
||||
|
||||
let block = client.get_block(BlockId::from(entry.as_id().clone())).await.unwrap();
|
||||
|
||||
fs::write(&cache_entry, serde_json::to_string(&block).into_diagnostic()?).into_diagnostic()?;
|
||||
let cts: SystemTime = entry.created_time.into();
|
||||
let mts: SystemTime = entry.last_edited_time.into();
|
||||
File::open(&cache_entry)
|
||||
.into_diagnostic()?
|
||||
.set_times(FileTimes::new()
|
||||
.set_created(cts)
|
||||
.set_modified(mts))
|
||||
.into_diagnostic()?;
|
||||
} else {
|
||||
println!("Page {}: \"{}\" is fresh", entry.id, entry.title().unwrap_or("(nothing)".to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
if batch.has_more {
|
||||
cursor = batch.next_cursor;
|
||||
continue;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
210
src/render/html.rs
Normal file
210
src/render/html.rs
Normal file
|
@ -0,0 +1,210 @@
|
|||
use crate::render::intermediary::parse_blocks;
|
||||
use crate::render::intermediary::Block;
|
||||
use crate::render::intermediary::RichText;
|
||||
use log::trace;
|
||||
use log::warn;
|
||||
use std::boxed::Box;
|
||||
use std::rc::Rc;
|
||||
|
||||
use rusticnotion::models::block::Block as NotionBlock;
|
||||
|
||||
pub fn from_notion(blocks: Vec<Box<NotionBlock>>, extra: bool) -> String {
|
||||
from_blocks(parse_blocks(blocks), extra)
|
||||
}
|
||||
|
||||
pub fn from_blocks(blocks: Vec<Block>, extra: bool) -> String {
|
||||
let mut out = String::new();
|
||||
|
||||
for block in preprocess(blocks) {
|
||||
match block {
|
||||
Block::Header { rich_text, size } => {
|
||||
out += &format!("<h{}>{}</h{}>", size, rich_text_to_html(rich_text), size);
|
||||
}
|
||||
Block::Divider => out += "<hr />",
|
||||
Block::Quote {
|
||||
rich_text,
|
||||
children,
|
||||
} => {
|
||||
out += &format!("<blockquote>{}</blockquote>", rich_text_to_html(rich_text));
|
||||
if let Some(children) = children {
|
||||
out += &from_blocks(children, false);
|
||||
}
|
||||
}
|
||||
Block::CodeBlock { text, lang } => {
|
||||
out += &format!(
|
||||
"<pre><code class=\"language-{}\">{}</code></pre>",
|
||||
lang, text
|
||||
);
|
||||
}
|
||||
Block::List { items } => {
|
||||
out += "<ul>";
|
||||
for item in items {
|
||||
let h = from_blocks(vec![item], true);
|
||||
out += &format!("<li>{}</li>", h);
|
||||
}
|
||||
out += "</ul>";
|
||||
}
|
||||
Block::NumberedList { items } => {
|
||||
out += "<ol>";
|
||||
for item in items {
|
||||
let h = from_blocks(vec![item], true);
|
||||
out += &format!("<li>{}</li>", h);
|
||||
}
|
||||
out += "</ol>";
|
||||
}
|
||||
Block::TodoList { items: list } => {
|
||||
out += "<ul>";
|
||||
for (checked, item) in list {
|
||||
out += &format!(
|
||||
"<li><input type=\"checkbox\" {}>{}</li>",
|
||||
if checked { "checked" } else { "" },
|
||||
from_blocks(vec![item], false)
|
||||
);
|
||||
}
|
||||
out += "</ul>";
|
||||
}
|
||||
Block::Line { rich_text } => {
|
||||
if rich_text.is_empty() {
|
||||
out += "<br />";
|
||||
} else {
|
||||
if extra {
|
||||
trace!("extra rich_text: {:#?}", rich_text);
|
||||
}
|
||||
out += &format!("<p>{}</p>", rich_text_to_html(rich_text));
|
||||
}
|
||||
}
|
||||
_ => warn!("Can't find html block type for {:?}", block.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn preprocess(blocks: Vec<Block>) -> Vec<Block> {
|
||||
let mut out = vec![];
|
||||
// temporary placeholder
|
||||
let mut last_block = Block::Empty;
|
||||
|
||||
for block in blocks {
|
||||
match block {
|
||||
Block::Header { rich_text, size } => {
|
||||
out.push(last_block);
|
||||
last_block = Block::Header { rich_text, size };
|
||||
}
|
||||
Block::Divider => {
|
||||
out.push(last_block);
|
||||
last_block = Block::Divider;
|
||||
}
|
||||
Block::Quote {
|
||||
rich_text,
|
||||
children: _,
|
||||
} => {
|
||||
out.push(last_block);
|
||||
last_block = Block::Quote {
|
||||
rich_text,
|
||||
children: None,
|
||||
};
|
||||
}
|
||||
Block::CodeBlock { text, lang } => {
|
||||
out.push(last_block);
|
||||
last_block = Block::CodeBlock { text, lang };
|
||||
}
|
||||
Block::List { items } => match last_block {
|
||||
Block::List { items: last_items } => {
|
||||
let mut new_items = last_items;
|
||||
new_items.extend(items);
|
||||
last_block = Block::List { items: new_items };
|
||||
}
|
||||
_ => {
|
||||
out.push(last_block);
|
||||
last_block = Block::List { items };
|
||||
}
|
||||
},
|
||||
Block::NumberedList { items } => match last_block {
|
||||
Block::NumberedList { items: last_items } => {
|
||||
let mut new_items = last_items;
|
||||
new_items.extend(items);
|
||||
last_block = Block::NumberedList { items: new_items };
|
||||
}
|
||||
_ => {
|
||||
out.push(last_block);
|
||||
last_block = Block::NumberedList { items };
|
||||
}
|
||||
},
|
||||
Block::TodoList { items } => {
|
||||
out.push(last_block);
|
||||
last_block = Block::TodoList { items };
|
||||
}
|
||||
Block::Line { rich_text } => {
|
||||
if rich_text.is_empty() {
|
||||
out.push(last_block);
|
||||
last_block = Block::Line { rich_text };
|
||||
} else {
|
||||
match last_block {
|
||||
Block::Line {
|
||||
rich_text: last_rich_text,
|
||||
} => {
|
||||
let mut new_rich_text = last_rich_text;
|
||||
new_rich_text.push(RichText::default());
|
||||
new_rich_text.extend(rich_text);
|
||||
last_block = Block::Line {
|
||||
rich_text: new_rich_text,
|
||||
};
|
||||
}
|
||||
_ => {
|
||||
out.push(last_block);
|
||||
last_block = Block::Line { rich_text };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => warn!(
|
||||
"Can't find intermediary block type for {:?}",
|
||||
block.to_string()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
out.push(last_block);
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn rich_text_to_html(rich_text: Vec<RichText>) -> String {
|
||||
let mut out = String::new();
|
||||
for text in rich_text {
|
||||
if text == RichText::default() {
|
||||
out += "<br />";
|
||||
continue;
|
||||
}
|
||||
let mut tags = vec![];
|
||||
if text.bold {
|
||||
tags.push("b");
|
||||
}
|
||||
if text.italic {
|
||||
tags.push("i");
|
||||
}
|
||||
if text.underline {
|
||||
tags.push("u");
|
||||
}
|
||||
if text.strikethrough {
|
||||
tags.push("s");
|
||||
}
|
||||
if text.code {
|
||||
tags.push("code");
|
||||
}
|
||||
|
||||
let output = &format!(
|
||||
"{}{}{}",
|
||||
tags.iter().map(|t| format!("<{}>", t)).collect::<String>(),
|
||||
text.plain_text,
|
||||
tags.iter().map(|t| format!("</{}>", t)).collect::<String>()
|
||||
);
|
||||
|
||||
match text.href {
|
||||
Some(href) => out += &format!("<a href=\"{}\">{}</a>", href, output),
|
||||
None => out += output,
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
397
src/render/intermediary.rs
Normal file
397
src/render/intermediary.rs
Normal file
|
@ -0,0 +1,397 @@
|
|||
use std::boxed::Box;
|
||||
use std::fmt::Display;
|
||||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
use log::warn;
|
||||
|
||||
use rusticnotion::models::block::Block as NotionBlock;
|
||||
use rusticnotion::models::block::CodeFields;
|
||||
use rusticnotion::models::block::CodeLanguage;
|
||||
use rusticnotion::models::block::CodeLanguage::*;
|
||||
use rusticnotion::models::block::TextAndChildren;
|
||||
use rusticnotion::models::block::ToDoFields;
|
||||
use rusticnotion::models::text::Annotations;
|
||||
use rusticnotion::models::text::RichText as NotionRichText;
|
||||
use rusticnotion::models::text::TextColor as NotionColor;
|
||||
|
||||
/// Adapted from https://github.com/EnyCode/notion2html
|
||||
/// Published under the terms of the MIT license
|
||||
///
|
||||
/// The Notion SDK has a number of peculiarities when it comes to its object
|
||||
/// model, especially around list-shaped things. Rather than having a wrapper
|
||||
/// "list" type, Notion just presents list elements (numbered, bulleted, todo)
|
||||
/// as inline block values.
|
||||
///
|
||||
/// Which works OK if your goal is to present some stuff, but isn't so good if
|
||||
/// your goal is to generate something approaching semantic HTML.
|
||||
///
|
||||
/// This mod exists to bridge the gap between the Notion SDK structs that come
|
||||
/// out of rusticnotion/serde and our own internal model which is more
|
||||
/// appropriate for rendering.
|
||||
///
|
||||
/// In a dynamically typed language we could probably avoid a lot of this
|
||||
/// adapter logic. But in Rust we can't, which results in this mod exporting a
|
||||
/// ton of types which are almost but _not quite_ the SDK types of the same
|
||||
/// name.
|
||||
|
||||
pub fn parse_blocks(notion: Vec<Box<NotionBlock>>) -> Vec<Block> {
|
||||
let mut out = Vec::new();
|
||||
for block in notion {
|
||||
match Box::deref(&block) {
|
||||
// TODO: not ignore color?
|
||||
NotionBlock::Heading1 { heading_1: t, .. } => out.push(Block::Header {
|
||||
rich_text: notion_to_text(t.rich_text.to_vec()),
|
||||
size: 1,
|
||||
}),
|
||||
NotionBlock::Heading2 { heading_2: t, .. } => out.push(Block::Header {
|
||||
rich_text: notion_to_text(t.rich_text.to_vec()),
|
||||
size: 2,
|
||||
}),
|
||||
NotionBlock::Heading3 { heading_3: t, .. } => out.push(Block::Header {
|
||||
rich_text: notion_to_text(t.rich_text.to_vec()),
|
||||
size: 3,
|
||||
}),
|
||||
NotionBlock::Quote {
|
||||
quote:
|
||||
TextAndChildren {
|
||||
rich_text: t,
|
||||
children,
|
||||
color: _,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
out.push(Block::Quote {
|
||||
rich_text: notion_to_text(t.to_vec()),
|
||||
children: if children.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(parse_blocks(children.to_vec()))
|
||||
},
|
||||
});
|
||||
}
|
||||
NotionBlock::Code {
|
||||
code:
|
||||
CodeFields {
|
||||
rich_text: t,
|
||||
language,
|
||||
caption: _, // TODO
|
||||
},
|
||||
..
|
||||
} => out.push(Block::CodeBlock {
|
||||
text: t.iter().map(|t| t.plain_text()).collect(),
|
||||
lang: lang_to_name(language.clone()),
|
||||
}),
|
||||
NotionBlock::ToDo {
|
||||
to_do:
|
||||
ToDoFields {
|
||||
rich_text,
|
||||
checked,
|
||||
children,
|
||||
color: _,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
if !children.is_empty() {
|
||||
out.push(Block::TodoList {
|
||||
items: vec![(
|
||||
*checked,
|
||||
Block::Line {
|
||||
rich_text: notion_to_text(rich_text.to_vec()),
|
||||
},
|
||||
)],
|
||||
});
|
||||
warn!("Ignoring children of todo block");
|
||||
} else {
|
||||
out.push(Block::TodoList {
|
||||
items: vec![(
|
||||
*checked,
|
||||
Block::Line {
|
||||
rich_text: notion_to_text(rich_text.to_vec()),
|
||||
},
|
||||
)],
|
||||
});
|
||||
}
|
||||
}
|
||||
NotionBlock::BulletedListItem {
|
||||
bulleted_list_item:
|
||||
TextAndChildren {
|
||||
rich_text,
|
||||
children,
|
||||
color: _,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
if !children.is_empty() {
|
||||
warn!("Ignoring children of list block");
|
||||
}
|
||||
out.push(Block::List {
|
||||
items: vec![Block::Line {
|
||||
rich_text: notion_to_text(rich_text.to_vec()),
|
||||
}],
|
||||
});
|
||||
}
|
||||
NotionBlock::NumberedListItem {
|
||||
numbered_list_item:
|
||||
TextAndChildren {
|
||||
rich_text,
|
||||
children,
|
||||
color: _,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
if !children.is_empty() {
|
||||
warn!("Ignoring children of list block");
|
||||
}
|
||||
out.push(Block::NumberedList {
|
||||
items: vec![Block::Line {
|
||||
rich_text: notion_to_text(rich_text.to_vec()),
|
||||
}],
|
||||
});
|
||||
}
|
||||
NotionBlock::Divider { .. } => out.push(Block::Divider),
|
||||
NotionBlock::Paragraph {
|
||||
paragraph:
|
||||
TextAndChildren {
|
||||
rich_text,
|
||||
children,
|
||||
color: _,
|
||||
},
|
||||
..
|
||||
} => {
|
||||
if !children.is_empty() {
|
||||
warn!("Ignoring children of paragraph block");
|
||||
}
|
||||
|
||||
out.push(Block::Line {
|
||||
rich_text: notion_to_text(rich_text.to_vec()),
|
||||
});
|
||||
}
|
||||
it => warn!("Can't find intermediary block type for {:?}", &it),
|
||||
};
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
const DEFAULT_ANNOTATIONS: Annotations = Annotations {
|
||||
bold: None,
|
||||
code: None,
|
||||
color: None,
|
||||
italic: None,
|
||||
strikethrough: None,
|
||||
underline: None,
|
||||
};
|
||||
|
||||
fn notion_to_text(text: Vec<NotionRichText>) -> Vec<RichText> {
|
||||
let mut out = Vec::new();
|
||||
for t in text {
|
||||
match t {
|
||||
NotionRichText::Text {
|
||||
rich_text: t,
|
||||
text: _,
|
||||
} => {
|
||||
let anns = t.annotations.unwrap_or(DEFAULT_ANNOTATIONS);
|
||||
out.push(RichText {
|
||||
plain_text: t.plain_text,
|
||||
bold: anns.bold.unwrap_or(false),
|
||||
italic: anns.italic.unwrap_or(false),
|
||||
underline: anns.underline.unwrap_or(false),
|
||||
strikethrough: anns.strikethrough.unwrap_or(false),
|
||||
code: anns.code.unwrap_or(false),
|
||||
href: t.href,
|
||||
color: if let Some(color) = anns.color {
|
||||
IntermediaryColor::from(color)
|
||||
} else {
|
||||
IntermediaryColor::Default
|
||||
},
|
||||
});
|
||||
}
|
||||
NotionRichText::Equation { .. } => {
|
||||
warn!("Equations are unsupported, {:?}", t)
|
||||
}
|
||||
NotionRichText::Mention { .. } => {
|
||||
warn!("Mentions are unsupported, {:?}", t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Block {
|
||||
Header {
|
||||
rich_text: Vec<RichText>,
|
||||
size: usize,
|
||||
},
|
||||
Divider,
|
||||
Quote {
|
||||
rich_text: Vec<RichText>,
|
||||
children: Option<Vec<Block>>,
|
||||
},
|
||||
CodeBlock {
|
||||
text: String,
|
||||
lang: String,
|
||||
},
|
||||
//Image {
|
||||
// url: String,
|
||||
//},
|
||||
List {
|
||||
items: Vec<Block>,
|
||||
},
|
||||
NumberedList {
|
||||
items: Vec<Block>,
|
||||
},
|
||||
TodoList {
|
||||
items: Vec<(bool, Block)>,
|
||||
},
|
||||
Line {
|
||||
rich_text: Vec<RichText>,
|
||||
},
|
||||
Empty,
|
||||
}
|
||||
|
||||
impl Display for Block {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Block::Header { .. } => "Header",
|
||||
Block::Divider => "Divider",
|
||||
Block::Quote { .. } => "Quote",
|
||||
Block::CodeBlock { .. } => "CodeBlock",
|
||||
//Block::Image { .. } => "Image",
|
||||
Block::List { .. } => "List",
|
||||
Block::NumberedList { .. } => "NumberedList",
|
||||
Block::TodoList { .. } => "TodoList",
|
||||
Block::Line { .. } => "Line",
|
||||
Block::Empty => "Empty",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub struct RichText {
|
||||
pub plain_text: String,
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
pub strikethrough: bool,
|
||||
pub code: bool,
|
||||
pub color: IntermediaryColor,
|
||||
pub href: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, PartialEq, Eq)]
|
||||
pub enum IntermediaryColor {
|
||||
Blue,
|
||||
Brown,
|
||||
#[default]
|
||||
Default,
|
||||
Gray,
|
||||
Green,
|
||||
Orange,
|
||||
Yellow,
|
||||
Pink,
|
||||
Purple,
|
||||
Red,
|
||||
}
|
||||
|
||||
impl From<NotionColor> for IntermediaryColor {
|
||||
fn from(value: NotionColor) -> Self {
|
||||
match value {
|
||||
NotionColor::Blue => IntermediaryColor::Blue,
|
||||
NotionColor::Brown => IntermediaryColor::Brown,
|
||||
NotionColor::Gray => IntermediaryColor::Gray,
|
||||
NotionColor::Green => IntermediaryColor::Green,
|
||||
NotionColor::Orange => IntermediaryColor::Orange,
|
||||
NotionColor::Yellow => IntermediaryColor::Yellow,
|
||||
NotionColor::Pink => IntermediaryColor::Pink,
|
||||
NotionColor::Purple => IntermediaryColor::Purple,
|
||||
NotionColor::Red => IntermediaryColor::Red,
|
||||
_ => IntermediaryColor::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: This is pretty terrible
|
||||
fn lang_to_name(lang: CodeLanguage) -> String {
|
||||
match lang {
|
||||
Abap => "abap",
|
||||
Arduino => "arduino",
|
||||
Bash => "bash",
|
||||
Basic => "basic",
|
||||
C => "c",
|
||||
Clojure => "clojure",
|
||||
Coffeescript => "coffeescript",
|
||||
CPlusPlus => "c++",
|
||||
CSharp => "c#",
|
||||
Css => "css",
|
||||
Dart => "dart",
|
||||
Diff => "diff",
|
||||
Docker => "dockerfile",
|
||||
Elixir => "elixer",
|
||||
Elm => "elm",
|
||||
Erlang => "erlang",
|
||||
Flow => "flow",
|
||||
Fortran => "fortran",
|
||||
FSharp => "f#",
|
||||
Gherkin => "gherkin",
|
||||
Glsl => "glsl",
|
||||
Go => "go",
|
||||
Graphql => "graphql",
|
||||
Groovy => "groovy",
|
||||
Haskell => "haskell",
|
||||
Html => "html",
|
||||
Java => "java",
|
||||
Javascript => "javascript",
|
||||
Json => "json",
|
||||
Julia => "julia",
|
||||
Kotlin => "kotlin",
|
||||
Latex => "latex",
|
||||
Less => "less",
|
||||
Lisp => "lisp",
|
||||
Livescript => "livescript",
|
||||
Lua => "lua",
|
||||
Makefile => "makefile",
|
||||
Markdown => "markdown",
|
||||
Markup => "markup",
|
||||
Matlab => "matlab",
|
||||
Mermaid => "mermaid",
|
||||
Nix => "nix",
|
||||
ObjectiveC => "objective-c",
|
||||
Ocaml => "ocaml",
|
||||
Pascal => "pascal",
|
||||
Perl => "perl",
|
||||
Php => "php",
|
||||
PlainText => "plain text",
|
||||
Powershell => "powershell",
|
||||
Prolog => "prolog",
|
||||
Protobuf => "protobuf",
|
||||
Python => "python",
|
||||
R => "r",
|
||||
Reason => "reason",
|
||||
Ruby => "ruby",
|
||||
Rust => "rust",
|
||||
Sass => "sass",
|
||||
Scala => "scala",
|
||||
Scheme => "scheme",
|
||||
Scss => "scss",
|
||||
Shell => "shell",
|
||||
Sql => "sql",
|
||||
Swift => "swift",
|
||||
Typescript => "typescript",
|
||||
VbNet => "vb.net",
|
||||
Verilog => "verilog",
|
||||
Vhdl => "vhdl",
|
||||
VisualBasic => "visual basic",
|
||||
Webassembly => "wasm",
|
||||
Xml => "xml",
|
||||
Yaml => "yaml",
|
||||
JavaCAndCPlusPlusAndCSharp => "java",
|
||||
}
|
||||
.to_string()
|
||||
}
|
3
src/render/mod.rs
Normal file
3
src/render/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod html;
|
||||
mod intermediary;
|
||||
mod notion;
|
336
src/render/notion.rs
Normal file
336
src/render/notion.rs
Normal file
|
@ -0,0 +1,336 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PageResponse {
|
||||
//pub object: String,
|
||||
pub results: Vec<Block>,
|
||||
//pub next_cursor: Option<String>,
|
||||
//pub has_more: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Block {
|
||||
//pub object: String,
|
||||
//pub id: String,
|
||||
//parent: Parent,
|
||||
//#[serde(rename = "created_time")]
|
||||
//pub created: String,
|
||||
//#[serde(rename = "last_edited_time")]
|
||||
//pub last_edited: String,
|
||||
// created_by: {object, id}
|
||||
// last_edited_by: {object, id}
|
||||
//pub has_children: bool,
|
||||
//pub archived: bool,
|
||||
//pub in_trash: bool,
|
||||
#[serde(rename = "type")]
|
||||
pub ty: String,
|
||||
#[serde(flatten)]
|
||||
pub block: BlockData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum BlockData {
|
||||
Bookmark {
|
||||
//caption: Vec<RichText>,
|
||||
//url: String,
|
||||
},
|
||||
Breadcrumb,
|
||||
BulletedListItem {
|
||||
rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
children: Option<Vec<Block>>,
|
||||
},
|
||||
Callout {
|
||||
//rich_text: Vec<RichText>,
|
||||
// icon (emoji or file)
|
||||
//color: NotionColor,
|
||||
},
|
||||
ChildDatabase {
|
||||
//title: String,
|
||||
},
|
||||
ChildPage {
|
||||
//title: String,
|
||||
},
|
||||
Code {
|
||||
//caption: Vec<RichText>,
|
||||
rich_text: Vec<RichText>,
|
||||
language: NotionLanguages,
|
||||
},
|
||||
ColumnList,
|
||||
Column,
|
||||
Divider,
|
||||
Embed {
|
||||
//url: String,
|
||||
},
|
||||
Equation {
|
||||
//expression: String,
|
||||
},
|
||||
File {
|
||||
// TODO: file
|
||||
//caption: Vec<RichText>,
|
||||
//#[serde(rename = "type")]
|
||||
//ty: String,
|
||||
//name: String,
|
||||
},
|
||||
#[serde(rename = "heading_1")]
|
||||
Heading1 {
|
||||
rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
//is_toggleable: bool,
|
||||
},
|
||||
#[serde(rename = "heading_2")]
|
||||
Heading2 {
|
||||
rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
//is_toggleable: bool,
|
||||
},
|
||||
#[serde(rename = "heading_3")]
|
||||
Heading3 {
|
||||
rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
//is_toggleable: bool,
|
||||
},
|
||||
Image {
|
||||
//#[serde(rename = "type")]
|
||||
//ty: String,
|
||||
// TODO: image
|
||||
},
|
||||
LinkPreview {
|
||||
//url: String,
|
||||
},
|
||||
Mention(/* MentionData */),
|
||||
NumberedListItem {
|
||||
rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
children: Option<Vec<Block>>,
|
||||
},
|
||||
Paragraph {
|
||||
rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
//children: Option<Vec<Block>>,
|
||||
},
|
||||
Pdf {
|
||||
// TODO: pdf
|
||||
},
|
||||
Quote {
|
||||
rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
children: Option<Vec<Block>>,
|
||||
},
|
||||
// synced block
|
||||
Table {
|
||||
//table_width: usize,
|
||||
//has_column_header: bool,
|
||||
//has_column_totals: bool,
|
||||
},
|
||||
TableRow {
|
||||
//cells: Vec<RichText>,
|
||||
},
|
||||
TableOfContents {
|
||||
//color: NotionColor,
|
||||
},
|
||||
ToDo {
|
||||
rich_text: Vec<RichText>,
|
||||
checked: bool,
|
||||
//color: NotionColor,
|
||||
children: Option<Vec<Block>>,
|
||||
},
|
||||
Toggle {
|
||||
//rich_text: Vec<RichText>,
|
||||
//color: NotionColor,
|
||||
//children: Option<Vec<Block>>,
|
||||
},
|
||||
Video {
|
||||
// TODO: file
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RichText {
|
||||
//#[serde(rename = "type")]
|
||||
//pub ty: String,
|
||||
//#[serde(flatten)]
|
||||
//pub data: RichTextData,
|
||||
pub annotations: Annotations,
|
||||
pub plain_text: String,
|
||||
pub href: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Annotations {
|
||||
pub bold: bool,
|
||||
pub italic: bool,
|
||||
pub strikethrough: bool,
|
||||
pub underline: bool,
|
||||
pub code: bool,
|
||||
pub color: NotionColor,
|
||||
}
|
||||
|
||||
/*#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RichTextData {
|
||||
Text {
|
||||
content: String,
|
||||
link: Option<Url>,
|
||||
},
|
||||
Equation {
|
||||
expression: String,
|
||||
},
|
||||
Mention {
|
||||
#[serde(rename = "type")]
|
||||
ty: String,
|
||||
#[serde(flatten)]
|
||||
data: MentionData,
|
||||
},
|
||||
}*/
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum NotionLanguages {
|
||||
Abap,
|
||||
Arduino,
|
||||
Bash,
|
||||
Basic,
|
||||
C,
|
||||
Clojure,
|
||||
Coffeescript,
|
||||
#[serde(rename = "c++")]
|
||||
Cpp,
|
||||
#[serde(rename = "c#")]
|
||||
CSharp,
|
||||
Css,
|
||||
Dart,
|
||||
Diff,
|
||||
Docker,
|
||||
Elixir,
|
||||
Elm,
|
||||
Erlang,
|
||||
Flow,
|
||||
Fortran,
|
||||
#[serde(rename = "f#")]
|
||||
FSharp,
|
||||
Gherkin,
|
||||
Glsl,
|
||||
Go,
|
||||
GraphQl,
|
||||
Groovy,
|
||||
Haskell,
|
||||
Html,
|
||||
Java,
|
||||
Javascript,
|
||||
Json,
|
||||
Julia,
|
||||
Kotlin,
|
||||
Latex,
|
||||
Less,
|
||||
Lisp,
|
||||
Liverscript,
|
||||
Lua,
|
||||
Makefile,
|
||||
Markdown,
|
||||
Markup,
|
||||
Matlab,
|
||||
Mermaid,
|
||||
Nix,
|
||||
#[serde(rename = "objective-c")]
|
||||
ObjectiveC,
|
||||
Ocaml,
|
||||
Pascal,
|
||||
Perl,
|
||||
Php,
|
||||
#[serde(rename = "plain text")]
|
||||
PlainText,
|
||||
Powershell,
|
||||
Prolog,
|
||||
Protobuf,
|
||||
Python,
|
||||
R,
|
||||
Reason,
|
||||
Ruby,
|
||||
Rust,
|
||||
Sass,
|
||||
Scala,
|
||||
Scheme,
|
||||
Scss,
|
||||
Shell,
|
||||
Sql,
|
||||
Swift,
|
||||
Typescript,
|
||||
#[serde(rename = "vb.net")]
|
||||
VbNet,
|
||||
Verilog,
|
||||
Vhdl,
|
||||
#[serde(rename = "visual basic")]
|
||||
VisualBasic,
|
||||
WebAssembly,
|
||||
Xml,
|
||||
Yaml,
|
||||
#[serde(rename = "java/c/c++/c#")]
|
||||
JavaOrCLangs,
|
||||
}
|
||||
|
||||
impl Display for NotionLanguages {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
/*#[derive(Debug, Deserialize)]
|
||||
pub struct Url {
|
||||
pub url: String,
|
||||
}*/
|
||||
|
||||
/*#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MentionData {
|
||||
Database {
|
||||
id: String,
|
||||
},
|
||||
Date {
|
||||
start: String,
|
||||
end: Option<String>,
|
||||
},
|
||||
LinkPreview {
|
||||
url: String,
|
||||
},
|
||||
Page {
|
||||
id: String,
|
||||
},
|
||||
TemplateMention {
|
||||
#[serde(rename = "type")]
|
||||
ty: String,
|
||||
template_mention_date: Option<String>,
|
||||
// we dont need the other one because its always "me" and we can tell from the type
|
||||
},
|
||||
User {
|
||||
object: String,
|
||||
id: String,
|
||||
},
|
||||
}*/
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NotionColor {
|
||||
Blue,
|
||||
BlueBackground,
|
||||
Brown,
|
||||
BrownBackground,
|
||||
Default,
|
||||
Gray,
|
||||
GrayBackground,
|
||||
Green,
|
||||
GreenBackground,
|
||||
Orange,
|
||||
OrangeBackground,
|
||||
Yellow,
|
||||
YellowBackground,
|
||||
Pink,
|
||||
PinkBackground,
|
||||
Purple,
|
||||
PurpleBackground,
|
||||
Red,
|
||||
RedBackground,
|
||||
}
|
1
vendor/rusticnotion
vendored
Submodule
1
vendor/rusticnotion
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 4903c139d86025c548e8f4854366064ec438b127
|
Loading…
Reference in a new issue