From 16aa545c5b626907bd59711d50ecc11434e5b256 Mon Sep 17 00:00:00 2001 From: projektir Date: Thu, 29 Jun 2017 00:35:20 -0400 Subject: [PATCH] Integrating Ace #247 --- src/book/mod.rs | 6 +- src/config/bookconfig.rs | 4 +- src/config/htmlconfig.rs | 18 ++++- src/config/mod.rs | 2 + src/config/playpenconfig.rs | 68 +++++++++++++++++++ src/config/tomlconfig.rs | 9 ++- src/renderer/html_handlebars/hbs_renderer.rs | 63 +++++++++++------ src/theme/book.css | 3 + src/theme/book.js | 64 +++++++++++++++--- src/theme/index.hbs | 9 ++- src/theme/mod.rs | 3 +- src/theme/playpen_editor/editor.js | 25 +++++++ src/theme/playpen_editor/mod.rs | 71 ++++++++++++++++++++ src/theme/stylus/print.styl | 4 ++ tests/tomlconfig.rs | 44 +++++++++++- 15 files changed, 356 insertions(+), 37 deletions(-) create mode 100644 src/config/playpenconfig.rs create mode 100644 src/theme/playpen_editor/editor.js create mode 100644 src/theme/playpen_editor/mod.rs diff --git a/src/book/mod.rs b/src/book/mod.rs index d5effb93..f07ab665 100644 --- a/src/book/mod.rs +++ b/src/book/mod.rs @@ -13,9 +13,9 @@ use errors::*; use config::BookConfig; use config::tomlconfig::TomlConfig; +use config::htmlconfig::HtmlConfig; use config::jsonconfig::JsonConfig; - pub struct MDBook { config: BookConfig, @@ -496,6 +496,10 @@ impl MDBook { .get_additional_css() } + pub fn get_html_config(&self) -> &HtmlConfig { + self.config.get_html_config() + } + // Construct book fn parse_summary(&mut self) -> Result<()> { // When append becomes stable, use self.content.append() ... diff --git a/src/config/bookconfig.rs b/src/config/bookconfig.rs index fba62c0c..5682492e 100644 --- a/src/config/bookconfig.rs +++ b/src/config/bookconfig.rs @@ -112,7 +112,7 @@ impl BookConfig { self.get_mut_html_config() .fill_from_tomlconfig(root, tomlhtmlconfig); } - + self } @@ -213,7 +213,7 @@ impl BookConfig { self.authors.as_slice() } - pub fn set_html_config(&mut self, htmlconfig: HtmlConfig) -> &mut Self { + pub fn set_html_config(&mut self, htmlconfig: HtmlConfig) -> &mut Self { self.html_config = htmlconfig; self } diff --git a/src/config/htmlconfig.rs b/src/config/htmlconfig.rs index 5d5cac94..d51c5394 100644 --- a/src/config/htmlconfig.rs +++ b/src/config/htmlconfig.rs @@ -1,6 +1,7 @@ use std::path::{PathBuf, Path}; use super::tomlconfig::TomlHtmlConfig; +use super::playpenconfig::PlaypenConfig; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct HtmlConfig { @@ -11,6 +12,7 @@ pub struct HtmlConfig { google_analytics: Option, additional_css: Vec, additional_js: Vec, + playpen: PlaypenConfig, } impl HtmlConfig { @@ -27,14 +29,16 @@ impl HtmlConfig { /// ``` pub fn new>(root: T) -> Self { let root = root.into(); + let theme = root.join("theme"); HtmlConfig { destination: root.clone().join("book"), - theme: root.join("theme"), + theme: theme.clone(), curly_quotes: false, mathjax_support: false, google_analytics: None, additional_css: Vec::new(), additional_js: Vec::new(), + playpen: PlaypenConfig::new(theme), } } @@ -81,6 +85,10 @@ impl HtmlConfig { } } + if let Some(playpen) = tomlconfig.playpen { + self.playpen.fill_from_tomlconfig(&self.theme, playpen); + } + self } @@ -154,4 +162,12 @@ impl HtmlConfig { pub fn get_additional_js(&self) -> &[PathBuf] { &self.additional_js } + + pub fn get_playpen_config(&self) -> &PlaypenConfig { + &self.playpen + } + + pub fn get_mut_playpen_config(&mut self) -> &mut PlaypenConfig { + &mut self.playpen + } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 90a8e2e4..412bcc54 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,9 +1,11 @@ pub mod bookconfig; pub mod htmlconfig; +pub mod playpenconfig; pub mod tomlconfig; pub mod jsonconfig; // Re-export the config structs pub use self::bookconfig::BookConfig; pub use self::htmlconfig::HtmlConfig; +pub use self::playpenconfig::PlaypenConfig; pub use self::tomlconfig::TomlConfig; diff --git a/src/config/playpenconfig.rs b/src/config/playpenconfig.rs new file mode 100644 index 00000000..e2c6e891 --- /dev/null +++ b/src/config/playpenconfig.rs @@ -0,0 +1,68 @@ +use std::path::{PathBuf, Path}; + +use super::tomlconfig::TomlPlaypenConfig; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PlaypenConfig { + editor: PathBuf, + editable: bool, +} + +impl PlaypenConfig { + /// Creates a new `PlaypenConfig` for playpen configuration. + /// + /// ``` + /// # use std::path::PathBuf; + /// # use mdbook::config::PlaypenConfig; + /// # + /// let editor = PathBuf::from("root/editor"); + /// let config = PlaypenConfig::new(PathBuf::from("root")); + /// + /// assert_eq!(config.get_editor(), &editor); + /// assert_eq!(config.is_editable(), false); + /// ``` + pub fn new>(root: T) -> Self { + PlaypenConfig { + editor: root.into().join("editor"), + editable: false, + } + } + + pub fn fill_from_tomlconfig>(&mut self, root: T, tomlplaypenconfig: TomlPlaypenConfig) -> &mut Self { + let root = root.into(); + + if let Some(editor) = tomlplaypenconfig.editor { + if editor.is_relative() { + self.editor = root.join(editor); + } else { + self.editor = editor; + } + } + + if let Some(editable) = tomlplaypenconfig.editable { + self.editable = editable; + } + + self + } + + pub fn is_editable(&self) -> bool { + self.editable + } + + pub fn get_editor(&self) -> &Path { + &self.editor + } + + pub fn set_editor>(&mut self, root: T, editor: T) -> &mut Self { + let editor = editor.into(); + + if editor.is_relative() { + self.editor = root.into().join(editor); + } else { + self.editor = editor; + } + + self + } +} diff --git a/src/config/tomlconfig.rs b/src/config/tomlconfig.rs index 6a318097..edac4c1d 100644 --- a/src/config/tomlconfig.rs +++ b/src/config/tomlconfig.rs @@ -29,6 +29,13 @@ pub struct TomlHtmlConfig { pub mathjax_support: Option, pub additional_css: Option>, pub additional_js: Option>, + pub playpen: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct TomlPlaypenConfig { + pub editor: Option, + pub editable: Option, } /// Returns a `TomlConfig` from a TOML string @@ -52,5 +59,3 @@ impl TomlConfig { Ok(config) } } - - diff --git a/src/renderer/html_handlebars/hbs_renderer.rs b/src/renderer/html_handlebars/hbs_renderer.rs index 8f119360..2e3972f4 100644 --- a/src/renderer/html_handlebars/hbs_renderer.rs +++ b/src/renderer/html_handlebars/hbs_renderer.rs @@ -3,8 +3,9 @@ use preprocess; use renderer::Renderer; use book::MDBook; use book::bookitem::{BookItem, Chapter}; -use utils; -use theme::{self, Theme}; +use config::PlaypenConfig; +use {utils, theme}; +use theme::{Theme, playpen_editor}; use errors::*; use regex::{Regex, Captures}; @@ -19,7 +20,6 @@ use handlebars::Handlebars; use serde_json; - #[derive(Default)] pub struct HtmlHandlebars; @@ -69,8 +69,11 @@ impl HtmlHandlebars { // Render the handlebars template with the data debug!("[*]: Render template"); let rendered = ctx.handlebars.render("index", &ctx.data)?; + let filename = Path::new(&ch.path).with_extension("html"); - let rendered = self.post_process(rendered, filename.file_name().unwrap().to_str().unwrap_or("")); + let rendered = self.post_process(rendered, + filename.file_name().unwrap().to_str().unwrap_or(""), + ctx.book.get_html_config().get_playpen_config()); // Write to file info!("[*] Creating {:?} ✓", filename.display()); @@ -116,11 +119,11 @@ impl HtmlHandlebars { Ok(()) } - fn post_process(&self, rendered: String, filename: &str) -> String { + fn post_process(&self, rendered: String, filename: &str, playpen_config: &PlaypenConfig) -> String { let rendered = build_header_links(&rendered, filename); let rendered = fix_anchor_links(&rendered, filename); let rendered = fix_code_blocks(&rendered); - let rendered = add_playpen_pre(&rendered); + let rendered = add_playpen_pre(&rendered, playpen_config); rendered } @@ -171,6 +174,19 @@ impl HtmlHandlebars { theme::FONT_AWESOME_TTF, )?; + let playpen_config = book.get_html_config().get_playpen_config(); + + // Ace is a very large dependency, so only load it when requested + if playpen_config.is_editable() { + // Load the editor + let editor = playpen_editor::PlaypenEditor::new(playpen_config.get_editor()); + book.write_file("editor.js", &editor.js)?; + book.write_file("ace.js", &editor.ace_js)?; + book.write_file("mode-rust.js", &editor.mode_rust_js)?; + book.write_file("theme-dawn.js", &editor.theme_dawn_js)?; + book.write_file("theme-tomorrow_night.js", &editor.theme_tomorrow_night_js)?; + } + Ok(()) } @@ -273,8 +289,10 @@ impl Renderer for HtmlHandlebars { debug!("[*]: Render template"); let rendered = handlebars.render("index", &data)?; - let rendered = self.post_process(rendered, "print.html"); + let rendered = self.post_process(rendered, "print.html", + book.get_html_config().get_playpen_config()); + book.write_file( Path::new("print").with_extension("html"), &rendered.into_bytes(), @@ -354,6 +372,15 @@ fn make_data(book: &MDBook) -> Result data.insert("additional_js".to_owned(), json!(js)); } + if book.get_html_config().get_playpen_config().is_editable() { + data.insert("playpens_editable".to_owned(), json!(true)); + data.insert("editor_js".to_owned(), json!("editor.js")); + data.insert("ace_js".to_owned(), json!("ace.js")); + data.insert("mode_rust_js".to_owned(), json!("mode-rust.js")); + data.insert("theme_dawn_js".to_owned(), json!("theme-dawn.js")); + data.insert("theme_tomorrow_night_js".to_owned(), json!("theme-tomorrow_night.js")); + } + let mut chapters = vec![]; for item in book.iter() { @@ -512,7 +539,7 @@ fn fix_code_blocks(html: &str) -> String { .into_owned() } -fn add_playpen_pre(html: &str) -> String { +fn add_playpen_pre(html: &str, playpen_config: &PlaypenConfig) -> String { let regex = Regex::new(r##"((?s)]?class="([^"]+)".*?>(.*?))"##).unwrap(); regex .replace_all(html, |caps: &Captures| { @@ -522,22 +549,18 @@ fn add_playpen_pre(html: &str) -> String { if classes.contains("language-rust") && !classes.contains("ignore") { // wrap the contents in an external pre block - - if text.contains("fn main") || text.contains("quick_main!") { + if playpen_config.is_editable() && + classes.contains("editable") || text.contains("fn main") || text.contains("quick_main!") { format!("
{}
", text) } else { // we need to inject our own main let (attrs, code) = partition_source(code); - format!( - "
# #![allow(unused_variables)]
-{}#fn main() {{
-\
-                             {}
-#}}
", - classes, - attrs, - code - ) + + format!("
\n# #![allow(unused_variables)]\n\
+                        {}#fn main() {{\n\
+                        {}\
+                        #}}
", + classes, attrs, code) } } else { // not language-rust, so no-op diff --git a/src/theme/book.css b/src/theme/book.css index 362dd8bc..f1d73dbd 100644 --- a/src/theme/book.css +++ b/src/theme/book.css @@ -936,6 +936,9 @@ table thead td { /* Force background to be printed in Chrome */ -webkit-print-color-adjust: exact; } + pre > .buttons { + z-index: 2; + } a, a:visited, a:active, diff --git a/src/theme/book.js b/src/theme/book.js index 50564d32..4ff47cbf 100644 --- a/src/theme/book.js +++ b/src/theme/book.js @@ -12,16 +12,25 @@ $( document ).ready(function() { set_theme(theme); - // Syntax highlighting Configuration hljs.configure({ tabReplace: ' ', // 4 spaces languages: [], // Languages used for auto-detection }); + + if (window.ace) { + // language-rust class needs to be removed for editable + // blocks or highlightjs will capture events + $('code.editable').removeClass('language-rust'); - $('code').each(function(i, block) { - hljs.highlightBlock(block); - }); + $('code').not('.editable').each(function(i, block) { + hljs.highlightBlock(block); + }); + } else { + $('code').each(function(i, block) { + hljs.highlightBlock(block); + }); + } // Adding the hljs class gives code blocks the color css // even if highlighting doesn't apply @@ -118,18 +127,32 @@ $( document ).ready(function() { }); function set_theme(theme) { + let ace_theme; + if (theme == 'coal' || theme == 'navy') { $("[href='ayu-highlight.css']").prop('disabled', true); $("[href='tomorrow-night.css']").prop('disabled', false); $("[href='highlight.css']").prop('disabled', true); + + ace_theme = "ace/theme/tomorrow_night"; } else if (theme == 'ayu') { $("[href='ayu-highlight.css']").prop('disabled', false); $("[href='tomorrow-night.css']").prop('disabled', true); $("[href='highlight.css']").prop('disabled', true); + + ace_theme = "ace/theme/tomorrow_night"; } else { $("[href='ayu-highlight.css']").prop('disabled', true); $("[href='tomorrow-night.css']").prop('disabled', true); $("[href='highlight.css']").prop('disabled', false); + + ace_theme = "ace/theme/dawn"; + } + + if (window.ace && window.editors) { + window.editors.forEach(function(editor) { + editor.setTheme(ace_theme); + }); } store.set('theme', theme); @@ -187,7 +210,6 @@ $( document ).ready(function() { }); }); - // Process playpen code blocks $(".playpen").each(function(block){ var pre_block = $(this); @@ -200,18 +222,37 @@ $( document ).ready(function() { buttons.prepend(""); buttons.prepend(""); + let code_block = pre_block.find("code").first(); + if (window.ace && code_block.hasClass("editable")) { + buttons.prepend(""); + } + buttons.find(".play-button").click(function(e){ run_rust_code(pre_block); }); buttons.find(".clip-button").mouseout(function(e){ hideTooltip(e.currentTarget); }); + buttons.find(".reset-button").click(function() { + if (!window.ace) { return; } + let editor = window.ace.edit(code_block.get(0)); + editor.setValue(editor.originalCode); + editor.clearSelection(); + }); }); var clipboardSnippets = new Clipboard('.clip-button', { text: function(trigger) { hideTooltip(trigger); - return $(trigger).parents(".playpen").find("code.language-rust.hljs")[0].textContent; + let playpen = $(trigger).parents(".playpen"); + let code_block = playpen.find("code").first(); + + if (window.ace && code_block.hasClass("editable")) { + let editor = window.ace.edit(code_block.get(0)); + return editor.getValue(); + } else { + return code_block.get(0).textContent; + } } }); clipboardSnippets.on('success', function(e) { @@ -221,7 +262,6 @@ $( document ).ready(function() { clipboardSnippets.on('error', function(e) { showTooltip(e.trigger, "Clipboard error!"); }); - }); function hideTooltip(elem) { @@ -260,7 +300,15 @@ function run_rust_code(code_block) { result_block = code_block.find(".result"); } - var text = code_block.find(".language-rust").text(); + let text; + + let inner_code_block = code_block.find("code").first(); + if (window.ace && inner_code_block.hasClass("editable")) { + let editor = window.ace.edit(inner_code_block.get(0)); + text = editor.getValue(); + } else { + text = inner_code_block.text(); + } var params = { version: "stable", diff --git a/src/theme/index.hbs b/src/theme/index.hbs index 769d1caa..9340ba7a 100644 --- a/src/theme/index.hbs +++ b/src/theme/index.hbs @@ -146,8 +146,15 @@ ga('create', '{{google_analytics}}', 'auto'); ga('send', 'pageview'); - {{/if}} + {{/if}} + {{#if playpens_editable}} + + + + + + {{/if}} diff --git a/src/theme/mod.rs b/src/theme/mod.rs index d01a2a2f..da6ec25a 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,10 +1,11 @@ +pub mod playpen_editor; + use std::path::Path; use std::fs::File; use std::io::Read; use errors::*; - pub static INDEX: &'static [u8] = include_bytes!("index.hbs"); pub static CSS: &'static [u8] = include_bytes!("book.css"); pub static FAVICON: &'static [u8] = include_bytes!("favicon.png"); diff --git a/src/theme/playpen_editor/editor.js b/src/theme/playpen_editor/editor.js new file mode 100644 index 00000000..4208b0c3 --- /dev/null +++ b/src/theme/playpen_editor/editor.js @@ -0,0 +1,25 @@ +window.editors = []; +(function(editors) { + if (typeof(ace) === 'undefined' || !ace) { + return; + } + + $(".editable").each(function() { + let editor = ace.edit(this); + editor.setOptions({ + highlightActiveLine: false, + showPrintMargin: false, + showLineNumbers: false, + showGutter: false, + maxLines: Infinity + }); + + editor.$blockScrolling = Infinity; + + editor.getSession().setMode("ace/mode/rust"); + + editor.originalCode = editor.getValue(); + + editors.push(editor); + }); +})(window.editors); diff --git a/src/theme/playpen_editor/mod.rs b/src/theme/playpen_editor/mod.rs new file mode 100644 index 00000000..5602532a --- /dev/null +++ b/src/theme/playpen_editor/mod.rs @@ -0,0 +1,71 @@ +use std::path::Path; + +use theme::load_file_contents; + +pub static JS: &'static [u8] = include_bytes!("editor.js"); +pub static ACE_JS: &'static [u8] = include_bytes!("ace.js"); +pub static MODE_RUST_JS: &'static [u8] = include_bytes!("mode-rust.js"); +pub static THEME_DAWN_JS: &'static [u8] = include_bytes!("theme-dawn.js"); +pub static THEME_TOMORROW_NIGHT_JS: &'static [u8] = include_bytes!("theme-tomorrow_night.js"); + +/// Integration of a JavaScript editor for playpens. +/// Uses the Ace editor: https://ace.c9.io/. +/// The Ace editor itself, the mode, and the theme files are the +/// generated minified no conflict versions. +/// +/// The `PlaypenEditor` struct should be used instead of the static variables because +/// the `new()` method +/// will look if the user has an editor directory in his source folder and use +/// the users editor instead +/// of the default. +/// +/// You should exceptionnaly use the static variables only if you need the +/// default editor even if the +/// user has specified another editor. +pub struct PlaypenEditor { + pub js: Vec, + pub ace_js: Vec, + pub mode_rust_js: Vec, + pub theme_dawn_js: Vec, + pub theme_tomorrow_night_js: Vec, +} + +impl PlaypenEditor { + pub fn new(src: &Path) -> Self { + let mut editor = PlaypenEditor { + js: JS.to_owned(), + ace_js: ACE_JS.to_owned(), + mode_rust_js: MODE_RUST_JS.to_owned(), + theme_dawn_js: THEME_DAWN_JS.to_owned(), + theme_tomorrow_night_js: THEME_TOMORROW_NIGHT_JS.to_owned(), + }; + + // Check if the given path exists + if !src.exists() || !src.is_dir() { + return editor; + } + + // Check for individual files if they exist + { + let files = vec![ + (src.join("editor.js"), &mut editor.js), + (src.join("ace.js"), &mut editor.ace_js), + (src.join("mode-rust.js"), &mut editor.mode_rust_js), + (src.join("theme-dawn.js"), &mut editor.theme_dawn_js), + (src.join("theme-tomorrow_night.js"), &mut editor.theme_tomorrow_night_js), + ]; + + for (filename, dest) in files { + if !filename.exists() { + continue; + } + + if let Err(e) = load_file_contents(&filename, dest) { + warn!("Couldn't load custom file, {}: {}", filename.display(), e); + } + } + } + + editor + } +} diff --git a/src/theme/stylus/print.styl b/src/theme/stylus/print.styl index e2c81099..977ba353 100644 --- a/src/theme/stylus/print.styl +++ b/src/theme/stylus/print.styl @@ -30,6 +30,10 @@ -webkit-print-color-adjust: exact } + pre > .buttons { + z-index: 2; + } + a, a:visited, a:active, a:hover { color: #4183c4 text-decoration: none diff --git a/tests/tomlconfig.rs b/tests/tomlconfig.rs index b07d0050..eaf83128 100644 --- a/tests/tomlconfig.rs +++ b/tests/tomlconfig.rs @@ -59,7 +59,49 @@ fn from_toml_authors() { assert_eq!(config.get_authors(), &[String::from("John Doe"), String::from("Jane Doe")]); } -// Tests that the `output.html.destination` key is correctly parsed in the TOML config +// Tests that the default `playpen` config is correct in the TOML config +#[test] +fn from_toml_playpen_default() { + let toml = ""; + + let parsed = TomlConfig::from_toml(toml).expect("This should parse"); + let config = BookConfig::from_tomlconfig("root", parsed); + + let playpenconfig = config.get_html_config().get_playpen_config(); + + assert_eq!(playpenconfig.get_editor(), PathBuf::from("root/theme/editor")); + assert_eq!(playpenconfig.is_editable(), false); +} + +// Tests that the `playpen.editor` key is correctly parsed in the TOML config +#[test] +fn from_toml_playpen_editor() { + let toml = r#"[output.html.playpen] + editor = "editordir""#; + + let parsed = TomlConfig::from_toml(toml).expect("This should parse"); + let config = BookConfig::from_tomlconfig("root", parsed); + + let playpenconfig = config.get_html_config().get_playpen_config(); + + assert_eq!(playpenconfig.get_editor(), PathBuf::from("root/theme/editordir")); +} + +// Tests that the `playpen.editable` key is correctly parsed in the TOML config +#[test] +fn from_toml_playpen_editable() { + let toml = r#"[output.html.playpen] + editable = true"#; + + let parsed = TomlConfig::from_toml(toml).expect("This should parse"); + let config = BookConfig::from_tomlconfig("root", parsed); + + let playpenconfig = config.get_html_config().get_playpen_config(); + + assert_eq!(playpenconfig.is_editable(), true); +} + +// Tests that the `output.html.destination` key is correcly parsed in the TOML config #[test] fn from_toml_output_html_destination() { let toml = r#"[output.html]