commit 0966a27e991a0b6fc9baa38e7846c0e173a0341e Author: Bram Dingelstad Date: Sun Jan 29 13:21:27 2023 +0100 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..dd9bb17 --- /dev/null +++ b/Cargo.toml @@ -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" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7cb4644 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e59fb4e --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# notion-to-markdown +A simple Rust library to turn Notion datatypes into Markdown diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4751de9 --- /dev/null +++ b/src/lib.rs @@ -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) -> Result { + 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::(); + + 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::() + ) + }, + 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::(); + + Some( + format!("```{language}\n{content}\n```") + ) + }, + BlockType::BulletedListItem(list_item) => { + let content = list_item.rich_text + .iter() + .map(|text| convert_rich_text(text)) + .collect::(); + + // 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::(); + + // 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::(); + + 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::(); + + // TODO: Recurse down to `children` + + Some( + format!("> {content}") + ) + }, + BlockType::Callout(callout) => { + let content = callout.rich_text + .iter() + .map(|text| convert_rich_text(text)) + .collect::(); + + 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#""#)), + // TODO: Implement reupload of Notion file type + _ => None + } + }, + BlockType::Video(video) => { + match &video.video { + notion::File::External(url) => Some(format!(r#"