This commit is contained in:
Reid D. McKenzie 2025-06-09 23:23:01 -06:00
commit 3c96b5efb9
13 changed files with 3072 additions and 0 deletions

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.cache/*
.jj/*
_posts/*
_static/*
scratch/*
target/*
config.toml

3
.gitmodules vendored Normal file
View file

@ -0,0 +1,3 @@
[submodule "vendor/rusticnotion"]
path = vendor/rusticnotion
url = git@github.com:arrdem/rusticnotion.git

1894
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

25
Cargo.toml Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
pub mod cache;
pub mod render;

145
src/main.rs Normal file
View 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
View 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
View 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
View file

@ -0,0 +1,3 @@
pub mod html;
mod intermediary;
mod notion;

336
src/render/notion.rs Normal file
View 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

@ -0,0 +1 @@
Subproject commit 4903c139d86025c548e8f4854366064ec438b127