├── run_simple.sh ├── .gitignore ├── examples ├── records.csv ├── sponge_with_dir.rs └── simple.rs ├── .travis.yml ├── Changelog.md ├── README.md ├── TODO.md ├── rustfmt.toml ├── src ├── lib.rs ├── plugins.rs └── parser.rs ├── Cargo.toml ├── LICENSE └── out └── sponge.html /run_simple.sh: -------------------------------------------------------------------------------- 1 | reset && cargo run --features "csv file" --example simple 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | *.sw* 3 | *.bk 4 | out 5 | Cargo.lock 6 | 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /examples/records.csv: -------------------------------------------------------------------------------- 1 | col1,col2,col3 2 | 1, 2, 3 3 | 4, 5, 6 4 | banana, batman, orange 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | - beta 5 | - stable 6 | script: 7 | - cargo build 8 | - cargo test 9 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.4.1 4 | - update to svgbob `0.5.0-alpha.4` 5 | 6 | ## 0.4.0 7 | - update to latest comrak 8 | - use syntect fancy regex 9 | - add embed_file 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Spongedown 2 | 3 | Spongedown converts markdown file to html with support for [svgbob diagrams](https://github.com/ivanceras/svgbobrus) 4 | 5 | [Demo](https://ivanceras.github.io/spongedown/) 6 | 7 | 8 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## TODO 2 | 3 | - [] side-to-side, s2s 4 | 5 | ```s2s 6 | ```txt |```bob 7 | raw text here |bob generated here 8 | ``` |``` 9 | ``` 10 | 11 | ```side-to-side:raw|bob 12 | content 13 | ``` 14 | 15 | - [] use pulldown-cmark and sauron::markdown module to parse html and source code. 16 | - [] incorporate data-viewer, gauntlet, restq as plugin 17 | - [] Add test cases 18 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | # Use unstable features 2 | unstable_features = true 3 | 4 | max_width = 80 5 | 6 | ## Visually align, useful in writing the view 7 | indent_style = "Block" 8 | imports_indent = "Block" 9 | reorder_imports = true 10 | reorder_impl_items = true 11 | merge_imports = true 12 | ## I want to be able to delete unused imports easily 13 | imports_layout = "Vertical" 14 | ## Default value is false, yet clipy keeps nagging on this 15 | use_field_init_shorthand = true 16 | 17 | ## also format macro 18 | format_macro_matchers = true 19 | force_multiline_blocks = true 20 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(warnings)] 2 | 3 | mod parser; 4 | mod plugins; 5 | 6 | pub use parser::{ 7 | parse, parse_with_base_dir, parse_with_settings, Html, Settings, 8 | }; 9 | 10 | mod errors { 11 | use crate::parser::ParseError; 12 | use crate::plugins::PluginError; 13 | use thiserror::Error; 14 | 15 | #[derive(Error, Debug)] 16 | pub enum SdError { 17 | #[error("markdown parse error: `{0}`")] 18 | ParseError(#[from] ParseError), 19 | #[error("PluginError: `{0}`")] 20 | PluginError(#[from] PluginError), 21 | } 22 | } 23 | 24 | #[cfg(test)] 25 | mod test { 26 | 27 | use super::*; 28 | 29 | #[test] 30 | fn title() { 31 | let input = "# Hello\n 32 | world"; 33 | let html = parse(input); 34 | println!("html: {:?}", html); 35 | assert!(html.is_ok()); 36 | let html = html.unwrap(); 37 | assert_eq!(Some("Hello".to_string()), html.title); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "spongedown" 3 | version = "0.5.0-alpha.1" 4 | authors = [ "Jovansonlee Cesar " ] 5 | license = "MIT" 6 | description = "Converts markdown to html with svgbob support" 7 | readme = "README.md" 8 | repository = "https://github.com/ivanceras/spongedown" 9 | documentation = "https://docs.rs/spongedown" 10 | keywords = ["markdown", "svg", "bob", "spongedown"] 11 | edition = "2018" 12 | 13 | [dependencies] 14 | svgbob = "0.5.0-alpha.8" 15 | csv = {version = "1", optional = true} 16 | comrak = {version = "0.8", default-features = false} 17 | typed-arena = "1.2" 18 | url = "1.7.1" 19 | url_path = "0.1.3" 20 | log = "0.4" 21 | file = {version = "1.1.2", optional = true} 22 | syntect = { version = "4.1.0", default-features = false, features = ["default-fancy"]} 23 | thiserror = "1" 24 | 25 | 26 | 27 | [features] 28 | default = ["with-csv", "with-embed"] 29 | with-csv = ["csv"] 30 | with-embed = ["file"] 31 | 32 | [profile.release] 33 | debug = false 34 | lto = true 35 | opt-level = 'z' 36 | panic = "abort" 37 | 38 | #[patch.crates-io] 39 | #svgbob = { path = "../svgbob/svgbob" } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jovansonlee Cesar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /examples/sponge_with_dir.rs: -------------------------------------------------------------------------------- 1 | extern crate spongedown; 2 | use std::fs; 3 | 4 | fn main() { 5 | let arg = r#" 6 | [Link to](./README.md) 7 | 8 | [Linux notes](/home/lee/PersonalBooks/notes/src/LINUX_NOTES.md) 9 | 10 | [Link to parent readme](../README.md) 11 | 12 | [Link to github](https://raw.githubusercontent.com/ivanceras/svgbob/master/TODO.md) 13 | 14 | ## Spongedown 15 | 16 | 17 | | table | Data | here| 18 | |----------|------|-----| 19 | | 1 | 2 | 3 | 20 | 21 | ```bob 22 | 23 | .--> Base::Class::Derived_A 24 | / 25 | .----> Base::Class::Derived_B 26 | Something -------. / \ 27 | \ / .---> Base::Class::Derived 28 | Something::else \ / \ 29 | \ \ / '--> Base::Class::Derived 30 | \ \ / 31 | \ \ .-----------> Base::Class::Derived_C 32 | \ \ / 33 | '------ Base::Class 34 | / \ \ \ 35 | ' \ \ \ 36 | | \ \ \ 37 | . \ \ '--- The::Latest 38 | /| \ \ \ 39 | With::Some::fantasy ' \ \ '---- The::Latest::Greatest 40 | /| \ \ 41 | More::Stuff ' \ '- I::Am::Running::Out::Of::Ideas 42 | /| \ 43 | More::Stuff ' \ 44 | / '--- Last::One 45 | More::Stuff V 46 | 47 | .---. .---. .---. .---. .---. .---. 48 | OS API '---' '---' '---' '---' '---' '---' 49 | | | | | | | 50 | v v | v | v 51 | .------------. | .-----------. | .-----. 52 | | Some | | | Diagrams | | | here| 53 | '------------' | '-----------' | '-----' 54 | | | | | 55 | v | | v 56 | .----. | | .---------. 57 | | IO |<----' | | | 58 | '----' | '---------' 59 | | | | 60 | v v v 61 | .---------------------------------------. 62 | | {out} Output | 63 | '---------------------------------------' 64 | 65 | # Legend: 66 | out = {fill: papayawhip } 67 | ``` 68 | 69 | 70 | And a text 71 | with abbr 72 | 73 | ```rust 74 | fn main(){ 75 | println!("hello world!"); 76 | } 77 | 78 | ``` 79 | 80 | "#; 81 | let html = spongedown::parse_with_base_dir(arg, "md", &None).unwrap(); 82 | println!("{}", html.content); 83 | fs::create_dir_all("out"); 84 | fs::write("out/sponge.html", &html.content); 85 | } 86 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | extern crate spongedown; 2 | use std::fs; 3 | 4 | fn main() { 5 | let arg = r#" 6 | 7 | ![an image](image.jpg) 8 | 9 | [Link to](./README.md) 10 | 11 | [Linux notes](/home/lee/PersonalBooks/notes/src/LINUX_NOTES.md) 12 | 13 | 14 | ![{display:hidden}](records.csv) 15 | 16 | ![{id:records,display:hidden}](records.csv) 17 | 18 | or 19 | 20 | ![][records] 21 | 22 | ```csv 23 | col1,col2,col3 24 | 1, 2, 3 25 | 4, 5, 6 26 | banana, batman, orange 27 | ``` 28 | ## Spongedown 29 | 30 | 31 | | table | Data | here| 32 | |----------|------|-----| 33 | | 1 | 2 | 3 | 34 | 35 | ```bob 36 | 37 | .--> Base::Class::Derived_A 38 | / 39 | .----> Base::Class::Derived_B 40 | Something -------. / \ 41 | \ / .---> Base::Class::Derived 42 | Something::else \ / \ 43 | \ \ / '--> Base::Class::Derived 44 | \ \ / 45 | \ \ .-----------> Base::Class::Derived_C 46 | \ \ / 47 | '------ Base::Class 48 | / \ \ \ 49 | ' \ \ \ 50 | | \ \ \ 51 | . \ \ '--- The::Latest 52 | /| \ \ \ 53 | With::Some::fantasy ' \ \ '---- The::Latest::Greatest 54 | /| \ \ 55 | More::Stuff ' \ '- I::Am::Running::Out::Of::Ideas 56 | /| \ 57 | More::Stuff ' \ 58 | / '--- Last::One 59 | More::Stuff V 60 | 61 | .---. .---. .---. .---. .---. .---. 62 | OS API '---' '---' '---' '---' '---' '---' 63 | | | | | | | 64 | v v | v | v 65 | .------------. | .-----------. | .-----. 66 | | Some | | | Diagrams | | | here| 67 | '------------' | '-----------' | '-----' 68 | | | | | 69 | v | | v 70 | .----. | | .---------. 71 | | IO |<----' | | | 72 | '----' | '---------' 73 | | | | 74 | v v v 75 | .---------------------------------------. 76 | | Output | 77 | '---------------------------------------' 78 | 79 | ``` 80 | 81 | 82 | And a text 83 | with abbr 84 | 85 | ```rust 86 | fn main(){ 87 | println!("hello world!"); 88 | } 89 | 90 | ``` 91 | 92 | [records]: ./records.csv 93 | 94 | "#; 95 | let html = spongedown::parse(arg).unwrap(); 96 | println!("{}", html.content); 97 | fs::create_dir_all("out"); 98 | fs::write("out/simple.html", &html.content); 99 | } 100 | -------------------------------------------------------------------------------- /src/plugins.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap}; 2 | use std::string::FromUtf8Error; 3 | use svgbob::Render; 4 | use thiserror::Error; 5 | use url_path::UrlPath; 6 | 7 | /// Plugin info, format: 8 | /// [] [@version][://] 9 | /// example: 10 | /// #table1 csv://data_file.csv 11 | #[allow(dead_code)] 12 | pub struct PluginInfo { 13 | selector: Option, 14 | plugin_name: String, 15 | version: Option, 16 | uri: Option, 17 | } 18 | 19 | #[derive(Error, Debug)] 20 | pub enum PluginError { 21 | #[error("Embeded file is not supplied")] 22 | EmbededFilesNotSupplied, 23 | #[error("Embeded file not found: `{0}`")] 24 | EmbedFileNotFound(String), 25 | #[error("Embed file has no extension")] 26 | EmbedFileNoExtension, 27 | #[error("Plugin does not exist: `{0}`")] 28 | PluginNotExist(String), 29 | #[error("Utf8Error: `{0}`")] 30 | Utf8Error(#[from] FromUtf8Error), 31 | } 32 | 33 | pub fn get_plugins( 34 | ) -> HashMap Result>> { 35 | let mut plugins: HashMap< 36 | String, 37 | Box Result>, 38 | > = HashMap::new(); 39 | plugins.insert("bob".into(), Box::new(bob_handler)); 40 | #[cfg(feature = "csv")] 41 | plugins.insert("csv".into(), Box::new(csv_handler)); 42 | plugins 43 | } 44 | 45 | /// convert bob ascii diagrams to svg 46 | fn bob_handler(input: &str) -> Result { 47 | let cb = svgbob::CellBuffer::from(input); 48 | let (node, width, height): (svgbob::Node<()>, f32, f32) = 49 | cb.get_node_with_size(&svgbob::Settings::default()); 50 | let svg = node.render_to_string(); 51 | let bob_container = format!( 52 | "
{}
", 53 | width, height, svg 54 | ); 55 | Ok(bob_container) 56 | } 57 | 58 | /// convert csv content into html table 59 | #[cfg(feature = "csv")] 60 | fn csv_handler(s: &str) -> Result { 61 | let mut buff = String::new(); 62 | let mut rdr = csv::Reader::from_reader(s.as_bytes()); 63 | buff.push_str(""); 64 | buff.push_str(""); 65 | for header in rdr.headers() { 66 | buff.push_str(""); 67 | for h in header { 68 | buff.push_str(&format!("", h)); 69 | } 70 | buff.push_str(""); 71 | } 72 | buff.push_str(""); 73 | buff.push_str(""); 74 | buff.push_str(""); 75 | for record in rdr.records() { 76 | buff.push_str(""); 77 | if let Ok(record) = record { 78 | for value in record.iter() { 79 | buff.push_str(&format!("", value)); 80 | } 81 | } 82 | buff.push_str(""); 83 | } 84 | buff.push_str(""); 85 | buff.push_str("
{}
{}
"); 86 | Ok(buff) 87 | } 88 | 89 | pub fn plugin_executor( 90 | plugin_name: &str, 91 | input: &str, 92 | ) -> Result { 93 | let plugins = get_plugins(); 94 | if let Some(handler) = plugins.get(plugin_name) { 95 | handler(input) 96 | } else { 97 | Err(PluginError::PluginNotExist(plugin_name.to_string())) 98 | } 99 | } 100 | 101 | pub fn is_in_plugins(plugin_name: &str) -> bool { 102 | let plugins = get_plugins(); 103 | if let Some(_handler) = plugins.get(plugin_name) { 104 | true 105 | } else { 106 | false 107 | } 108 | } 109 | 110 | /// handle the embed of the file with the supplied content 111 | #[cfg(feature = "file")] 112 | pub fn embed_handler( 113 | url: &str, 114 | embed_files: &Option>>, 115 | ) -> Result { 116 | if let Some(embed_files) = embed_files { 117 | let url_path = UrlPath::new(&url); 118 | if let Some(ext) = url_path.extension() { 119 | if is_in_plugins(&ext) { 120 | if let Some(content) = embed_files.get(url) { 121 | let content = String::from_utf8(content.to_owned())?; 122 | plugin_executor(&ext, &content) 123 | } else { 124 | Err(PluginError::EmbedFileNotFound(url.to_string())) // file is not in the embeded files 125 | } 126 | } else { 127 | Err(PluginError::PluginNotExist(ext.to_string())) 128 | } 129 | } else { 130 | Err(PluginError::EmbedFileNoExtension) // no extension on the embeded file 131 | } 132 | } else { 133 | Err(PluginError::EmbededFilesNotSupplied) // no embedded file supplied 134 | } 135 | } 136 | 137 | #[cfg(feature = "file")] 138 | pub fn fetch_file_contents(files: Vec) -> BTreeMap> { 139 | let mut embed_files = BTreeMap::new(); 140 | for fname in files { 141 | match file::get(&fname) { 142 | Ok(content) => { 143 | embed_files.insert(fname, content); 144 | } 145 | Err(e) => { 146 | log::error!("fetching file error: {:?}", e); 147 | } 148 | } 149 | } 150 | embed_files 151 | } 152 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use crate::plugins; 2 | use comrak::{ 3 | format_html, 4 | nodes::{AstNode, NodeHtmlBlock, NodeValue}, 5 | parse_document, ComrakExtensionOptions, ComrakOptions, ComrakParseOptions, 6 | ComrakRenderOptions, 7 | }; 8 | use std::string::FromUtf8Error; 9 | use std::{ 10 | collections::BTreeMap, 11 | sync::{Arc, Mutex}, 12 | }; 13 | use syntect::{ 14 | highlighting::{Color, ThemeSet}, 15 | html::highlighted_html_for_string, 16 | parsing::SyntaxSet, 17 | }; 18 | use thiserror::Error; 19 | use typed_arena::Arena; 20 | use url_path::UrlPath; 21 | 22 | #[derive(Error, Debug)] 23 | pub enum ParseError { 24 | #[error("Utf8Error: `{0}`")] 25 | Utf8Error(#[from] FromUtf8Error), 26 | #[error("Error getting lock from embed file")] 27 | EmbedFileLockError, 28 | #[error("Error parsing md file")] 29 | MdParseError, 30 | } 31 | 32 | #[derive(Debug)] 33 | pub struct Html { 34 | pub title: Option, 35 | pub content: String, 36 | } 37 | 38 | pub struct Settings { 39 | /// add a base directory for all links to other md files 40 | base_dir: Option, 41 | } 42 | 43 | impl Default for Settings { 44 | fn default() -> Self { 45 | Settings { base_dir: None } 46 | } 47 | } 48 | 49 | pub fn parse(arg: &str) -> Result { 50 | let referred_files = pre_parse_get_embedded_files(arg); 51 | let embed_files = if let Ok(referred_files) = referred_files { 52 | let file_contents = plugins::fetch_file_contents(referred_files); 53 | Some(file_contents) 54 | } else { 55 | None 56 | }; 57 | parse_with_settings(arg, &embed_files, &Settings::default()) 58 | } 59 | 60 | pub fn parse_with_base_dir( 61 | arg: &str, 62 | base_dir: &str, 63 | embed_files: &Option>>, 64 | ) -> Result { 65 | let settings = Settings { 66 | base_dir: Some(base_dir.to_string()), 67 | ..Default::default() 68 | }; 69 | parse_with_settings(arg, &embed_files, &settings) 70 | } 71 | 72 | pub fn parse_with_settings( 73 | arg: &str, 74 | embed_files: &Option>>, 75 | settings: &Settings, 76 | ) -> Result { 77 | let html = parse_via_comrak(arg, &embed_files, settings); 78 | html 79 | } 80 | 81 | fn get_comrak_options() -> ComrakOptions { 82 | ComrakOptions { 83 | extension: ComrakExtensionOptions { 84 | strikethrough: true, 85 | tagfilter: false, 86 | table: true, 87 | autolink: true, 88 | tasklist: true, 89 | superscript: false, 90 | header_ids: None, 91 | footnotes: true, 92 | description_lists: true, 93 | }, 94 | parse: ComrakParseOptions { 95 | smart: false, 96 | default_info_string: None, 97 | }, 98 | render: ComrakRenderOptions { 99 | hardbreaks: true, 100 | github_pre_lang: true, 101 | width: 0, 102 | unsafe_: true, 103 | escape: false, 104 | }, 105 | } 106 | } 107 | 108 | fn iter_nodes<'a, F>( 109 | node: &'a AstNode<'a>, 110 | is_heading: Arc>, 111 | title: Arc>>, 112 | f: &F, 113 | ) -> Result<(), ParseError> 114 | where 115 | F: Fn(&'a AstNode<'a>) -> Result<(), ParseError>, 116 | { 117 | f(node)?; 118 | for c in node.children() { 119 | iter_nodes(c, is_heading.clone(), title.clone(), f)?; 120 | } 121 | Ok(()) 122 | } 123 | 124 | fn pre_iter_nodes<'a, F>( 125 | node: &'a AstNode<'a>, 126 | files: Arc>>, 127 | f: &F, 128 | ) -> Result<(), ParseError> 129 | where 130 | F: Fn(&'a AstNode<'a>) -> Result<(), ParseError>, 131 | { 132 | f(node)?; 133 | for c in node.children() { 134 | pre_iter_nodes(c, files.clone(), f)?; 135 | } 136 | Ok(()) 137 | } 138 | /// 139 | /// Extract the embeded files in img image and make it as a lookup 140 | pub fn pre_parse_get_embedded_files( 141 | arg: &str, 142 | ) -> Result, ParseError> { 143 | // The returned nodes are created in the supplied Arena, and are bound by its lifetime. 144 | let arena = Arena::new(); 145 | let option = get_comrak_options(); 146 | let root = parse_document(&arena, arg, &option); 147 | let embed_files: Arc>> = Arc::new(Mutex::new(vec![])); 148 | 149 | pre_iter_nodes(root, embed_files.clone(), &|node| { 150 | let ref mut value = node.data.borrow_mut().value; 151 | let new_value = match value { 152 | &mut NodeValue::Image(ref link) => { 153 | let link_url = String::from_utf8(link.url.clone())?; 154 | if let Ok(mut embed_files) = embed_files.lock() { 155 | embed_files.push(link_url); 156 | } 157 | value.clone() 158 | } 159 | _ => value.clone(), 160 | }; 161 | *value = new_value; 162 | Ok(()) 163 | })?; 164 | let embedded = match embed_files.lock() { 165 | Ok(files) => Ok((*files).to_owned()), 166 | Err(_e) => Err(ParseError::EmbedFileLockError), 167 | }; 168 | embedded 169 | } 170 | 171 | fn parse_via_comrak( 172 | arg: &str, 173 | embed_files: &Option>>, 174 | settings: &Settings, 175 | ) -> Result { 176 | // The returned nodes are created in the supplied Arena, and are bound by its lifetime. 177 | let arena = Arena::new(); 178 | let option = get_comrak_options(); 179 | let title: Arc>> = Arc::new(Mutex::new(None)); 180 | let is_heading: Arc> = Arc::new(Mutex::new(false)); 181 | let root = parse_document(&arena, arg, &option); 182 | 183 | iter_nodes(root, is_heading.clone(), title.clone(), &|node| { 184 | let ref mut value = node.data.borrow_mut().value; 185 | let new_value = match value { 186 | &mut NodeValue::CodeBlock(ref codeblock) => { 187 | let codeblock_info = 188 | String::from_utf8(codeblock.info.to_owned()) 189 | .expect("error converting to string"); 190 | let codeblock_literal = 191 | String::from_utf8(codeblock.literal.to_owned()) 192 | .expect("error converting to string"); 193 | if let Ok(out) = plugins::plugin_executor( 194 | &codeblock_info, 195 | &codeblock_literal, 196 | ) { 197 | NodeValue::HtmlBlock(NodeHtmlBlock { 198 | literal: out.into_bytes(), 199 | block_type: 0, 200 | }) 201 | } else if let Some(code_block_html) = 202 | format_source_code(&codeblock_info, &codeblock_literal) 203 | { 204 | code_block_html 205 | } else { 206 | value.clone() 207 | } 208 | } 209 | &mut NodeValue::Link(ref nodelink) => { 210 | if let Ok(url) = String::from_utf8(nodelink.url.clone()) { 211 | if let Some(ref base_dir) = settings.base_dir { 212 | let url1 = UrlPath::new(&url); 213 | let url2 = url1.normalize(); 214 | let url3 = if url1.is_external() { 215 | url2 216 | } else if url1.is_absolute() { 217 | url2 218 | } else { 219 | format!("{}/{}", base_dir, url) 220 | }; 221 | let url4 = UrlPath::new(&url3); 222 | let url5 = url4.normalize(); 223 | let url6 = 224 | if url4.is_external() && !url4.is_extension("md") { 225 | // leave as it 226 | url5 227 | } else { 228 | format!("/#{}", url5) 229 | }; 230 | log::info!("url6: {}", url6); 231 | let mut new_nodelink = nodelink.clone(); 232 | new_nodelink.url = url6.into_bytes(); 233 | NodeValue::Link(new_nodelink) 234 | } else { 235 | value.clone() 236 | } 237 | } else { 238 | value.clone() 239 | } 240 | } 241 | &mut NodeValue::Heading(ref heading) => { 242 | if heading.level == 1 { 243 | if let Ok(mut is_heading) = is_heading.lock() { 244 | *is_heading = true; 245 | } 246 | } 247 | value.clone() 248 | } 249 | &mut NodeValue::Text(ref text) => { 250 | if let Ok(is_heading) = is_heading.lock() { 251 | if *is_heading { 252 | let txt = String::from_utf8(text.to_owned()) 253 | .expect("Unable to convert to string"); 254 | if let Ok(mut title) = title.lock() { 255 | if title.is_none() { 256 | // only when unset 257 | *title = Some(txt.to_string()); 258 | } 259 | } 260 | } 261 | } 262 | value.clone() 263 | } 264 | &mut NodeValue::Image(ref link) => { 265 | let link_url = String::from_utf8(link.url.clone()) 266 | .expect("unable to convert to string"); 267 | match plugins::embed_handler(&link_url, embed_files) { 268 | Ok(html) => NodeValue::HtmlBlock(NodeHtmlBlock { 269 | literal: html.into_bytes(), 270 | block_type: 0, 271 | }), 272 | Err(e) => { 273 | log::error!("error: {:#?}", e); 274 | value.clone() 275 | } 276 | } 277 | } 278 | _ => value.clone(), 279 | }; 280 | *value = new_value; 281 | Ok(()) 282 | })?; 283 | 284 | let mut html = vec![]; 285 | 286 | if let Ok(()) = format_html(root, &option, &mut html) { 287 | let render_html = String::from_utf8(html)?; 288 | let title = if let Ok(got) = title.lock() { 289 | if let Some(ref got) = *got { 290 | Some(got.to_string()) 291 | } else { 292 | None 293 | } 294 | } else { 295 | None 296 | }; 297 | Ok(Html { 298 | title, 299 | content: render_html, 300 | }) 301 | } else { 302 | Err(ParseError::MdParseError) 303 | } 304 | } 305 | 306 | fn format_source_code(lang: &str, literal: &str) -> Option { 307 | let lang_name = match lang { 308 | "rust" => "Rust", 309 | _ => "text", 310 | }; 311 | 312 | let ss = SyntaxSet::load_defaults_newlines(); 313 | let ts = ThemeSet::load_defaults(); 314 | let theme = &ts.themes["base16-ocean.light"]; 315 | let _c = theme.settings.background.unwrap_or(Color::WHITE); 316 | 317 | if let Some(syntax) = ss.find_syntax_by_name(lang_name) { 318 | let html = highlighted_html_for_string(literal, &ss, &syntax, theme); 319 | Some(NodeValue::HtmlBlock(NodeHtmlBlock { 320 | literal: html.into_bytes(), 321 | block_type: 0, 322 | })) 323 | } else { 324 | None 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /out/sponge.html: -------------------------------------------------------------------------------- 1 |

Link to

2 |

Linux notes

3 |

Link to parent readme

4 |

Link to github

5 |

Spongedown

6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
tableDatahere
123
22 |
23 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | Some 119 | 120 | Diagrams 121 | 122 | IO 123 | 124 | 125 | Output 126 | 127 | 128 | here 129 | 130 | 131 | 132 | Something::else 133 | 134 | 135 | Base::Class 136 | 137 | ' 138 | 139 | 140 | 141 | 142 | 143 | 144 | Base::Class::Derived 145 | 146 | A 147 | Base::Class::Derived 148 | 149 | B 150 | 151 | 152 | 153 | Something 154 | Base::Class::Derived 155 | Base::Class::Derived 156 | Base::Class::Derived 157 | 158 | C 159 | The::Latest 160 | With::Some::fantasy 161 | The::Latest::Greatest 162 | More::Stuff 163 | I::Am::Running::Out::Of::Ideas 164 | More::Stuff 165 | Last::One 166 | More::Stuff 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | OS 178 | API 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 |
236 |

And a text
237 | with abbr

238 |
fn main(){
239 |     println!("hello world!");
240 | }
241 | 
242 | 
243 | --------------------------------------------------------------------------------