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