first commit
This commit is contained in:
commit
0966a27e99
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/Cargo.lock
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "notion-to-markdown"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-recursion = "1.0.0"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_variant = "0.1.1"
|
||||||
|
notion = { git = "https://github.com/bram-dingelstad/notion-client-rs.git", package = "notion-client" }
|
21
LICENSE
Normal file
21
LICENSE
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Bram Dingelstad
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
2
README.md
Normal file
2
README.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
# notion-to-markdown
|
||||||
|
A simple Rust library to turn Notion datatypes into Markdown
|
228
src/lib.rs
Normal file
228
src/lib.rs
Normal file
|
@ -0,0 +1,228 @@
|
||||||
|
|
||||||
|
use async_recursion::async_recursion;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
}
|
||||||
|
|
||||||
|
use notion;
|
||||||
|
use notion::BlockType;
|
||||||
|
|
||||||
|
pub fn convert_rich_text(text: ¬ion::RichText) -> String {
|
||||||
|
match text {
|
||||||
|
notion::RichText::Text(text, _) => {
|
||||||
|
let mut string = text.content.to_owned();
|
||||||
|
|
||||||
|
if text.annotations.bold {
|
||||||
|
string = format!("**{string}**");
|
||||||
|
}
|
||||||
|
|
||||||
|
if text.annotations.italic {
|
||||||
|
string = format!("*{string}*");
|
||||||
|
}
|
||||||
|
|
||||||
|
if text.annotations.code {
|
||||||
|
string = format!("`{string}`");
|
||||||
|
}
|
||||||
|
|
||||||
|
string
|
||||||
|
},
|
||||||
|
_ => "".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_recursion]
|
||||||
|
pub async fn convert_blocks(notion: ¬ion::Client, blocks: &Vec<notion::Block>) -> Result<String, Error> {
|
||||||
|
let mut output = vec![];
|
||||||
|
|
||||||
|
for block in blocks.iter() {
|
||||||
|
let string = match &block.value {
|
||||||
|
BlockType::Heading1(heading) |
|
||||||
|
BlockType::Heading2(heading) |
|
||||||
|
BlockType::Heading3(heading) => {
|
||||||
|
let content = heading.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let markdown_heading = match &block.value {
|
||||||
|
BlockType::Heading1(_) => "#",
|
||||||
|
BlockType::Heading2(_) => "##",
|
||||||
|
BlockType::Heading3(_) | _ => "###",
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(format!("{markdown_heading} {content}"))
|
||||||
|
},
|
||||||
|
BlockType::Paragraph(paragraph) => {
|
||||||
|
Some(
|
||||||
|
paragraph.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockType::Code(code) => {
|
||||||
|
let language = serde_variant::to_variant_name(&code.language).unwrap();
|
||||||
|
let content = code.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format!("```{language}\n{content}\n```")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockType::BulletedListItem(list_item) => {
|
||||||
|
let content = list_item.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// TODO: Recurse down to `children`
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format!("* {content}")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockType::NumberedListItem(list_item) => {
|
||||||
|
// TODO: Hold state for numbering
|
||||||
|
let content = list_item.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// TODO: Recurse down to `children`
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format!("1. {content}")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockType::ToDo(todo_item) => {
|
||||||
|
let content = todo_item.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let checked = if todo_item.checked.unwrap_or(false) {
|
||||||
|
"x"
|
||||||
|
} else {
|
||||||
|
" "
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Recurse down to `children`
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format!("[{checked}] {content}")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockType::Quote(quote) => {
|
||||||
|
let content = quote.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
// TODO: Recurse down to `children`
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format!("> {content}")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockType::Callout(callout) => {
|
||||||
|
let content = callout.rich_text
|
||||||
|
.iter()
|
||||||
|
.map(|text| convert_rich_text(text))
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let icon = if let Some(value) = &callout.icon {
|
||||||
|
match value {
|
||||||
|
notion::Icon::Emoji(emoji) => emoji,
|
||||||
|
_ => ""
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: Recurse down to `children`
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format!("> {icon} {content}")
|
||||||
|
)
|
||||||
|
},
|
||||||
|
BlockType::Image(image) => {
|
||||||
|
match &image.image {
|
||||||
|
notion::File::External(url) => Some(format!(r#"<img style="margin: 0 auto" src="{url}">"#)),
|
||||||
|
// TODO: Implement reupload of Notion file type
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BlockType::Video(video) => {
|
||||||
|
match &video.video {
|
||||||
|
notion::File::External(url) => Some(format!(r#"<video controls src="{url}" />"#)),
|
||||||
|
// TODO: Implement reupload of Notion file type
|
||||||
|
_ => None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
BlockType::Divider => Some("---".to_string()),
|
||||||
|
BlockType::Unsupported(string) => {
|
||||||
|
println!("Did not catch {string}");
|
||||||
|
None
|
||||||
|
},
|
||||||
|
BlockType::ColumnList(_) => {
|
||||||
|
if block.has_children {
|
||||||
|
let columns = notion
|
||||||
|
.blocks()
|
||||||
|
.children()
|
||||||
|
.list(notion::BlockChildrenListOptions { block_id: &block.id })
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.results;
|
||||||
|
|
||||||
|
let mut content = vec![];
|
||||||
|
for column in columns.iter() {
|
||||||
|
let children = notion.blocks().children().list(notion::BlockChildrenListOptions { block_id: &column.id })
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.results;
|
||||||
|
|
||||||
|
content.push(convert_blocks(¬ion, &children).await.unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
format!(
|
||||||
|
r#"<div style="display: flex;">{content}</div>"#,
|
||||||
|
content = content
|
||||||
|
.iter()
|
||||||
|
.map(
|
||||||
|
|column| format!(r#"<div style="margin: 0 16px">{column}</div>"#)
|
||||||
|
)
|
||||||
|
.collect::<Vec<String>>()
|
||||||
|
.join("\n")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
BlockType::Column(_) |
|
||||||
|
BlockType::Table |
|
||||||
|
BlockType::Bookmark |
|
||||||
|
BlockType::File(_) | BlockType::PDF(_) |
|
||||||
|
|
||||||
|
BlockType::TableOfContents |
|
||||||
|
BlockType::ChildPage(_) |
|
||||||
|
BlockType::ChildDatabase(_) |
|
||||||
|
BlockType::SyncedBlock |
|
||||||
|
BlockType::Template |
|
||||||
|
BlockType::Toggle => None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(string) = string {
|
||||||
|
output.push(string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(output.join("\n\n"))
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue