Integrating Ace #247

This commit is contained in:
projektir 2017-06-29 00:35:20 -04:00
parent 6601dbdd61
commit 16aa545c5b
15 changed files with 356 additions and 37 deletions

View file

@ -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() ...

View file

@ -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
}

View file

@ -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<String>,
additional_css: Vec<PathBuf>,
additional_js: Vec<PathBuf>,
playpen: PlaypenConfig,
}
impl HtmlConfig {
@ -27,14 +29,16 @@ impl HtmlConfig {
/// ```
pub fn new<T: Into<PathBuf>>(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
}
}

View file

@ -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;

View file

@ -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<T: Into<PathBuf>>(root: T) -> Self {
PlaypenConfig {
editor: root.into().join("editor"),
editable: false,
}
}
pub fn fill_from_tomlconfig<T: Into<PathBuf>>(&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<T: Into<PathBuf>>(&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
}
}

View file

@ -29,6 +29,13 @@ pub struct TomlHtmlConfig {
pub mathjax_support: Option<bool>,
pub additional_css: Option<Vec<PathBuf>>,
pub additional_js: Option<Vec<PathBuf>>,
pub playpen: Option<TomlPlaypenConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TomlPlaypenConfig {
pub editor: Option<PathBuf>,
pub editable: Option<bool>,
}
/// Returns a `TomlConfig` from a TOML string
@ -52,5 +59,3 @@ impl TomlConfig {
Ok(config)
}
}

View file

@ -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<serde_json::Map<String, serde_json::Value>
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)<code[^>]?class="([^"]+)".*?>(.*?)</code>)"##).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!("<pre class=\"playpen\">{}</pre>", text)
} else {
// we need to inject our own main
let (attrs, code) = partition_source(code);
format!(
"<pre class=\"playpen\"><code class=\"{}\"># #![allow(unused_variables)]
{}#fn main() {{
\
{}
#}}</code></pre>",
classes,
attrs,
code
)
format!("<pre class=\"playpen\"><code class=\"{}\">\n# #![allow(unused_variables)]\n\
{}#fn main() {{\n\
{}\
#}}</code></pre>",
classes, attrs, code)
}
} else {
// not language-rust, so no-op

View file

@ -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,

View file

@ -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("<i class=\"fa fa-play play-button\"></i>");
buttons.prepend("<i class=\"fa fa-copy clip-button\"><i class=\"tooltiptext\"></i></i>");
let code_block = pre_block.find("code").first();
if (window.ace && code_block.hasClass("editable")) {
buttons.prepend("<i class=\"fa fa-history reset-button\"></i>");
}
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",

View file

@ -146,8 +146,15 @@
ga('create', '{{google_analytics}}', 'auto');
ga('send', 'pageview');
</script>
{{/if}}
{{/if}}
{{#if playpens_editable}}
<script src="{{ ace_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ editor_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ mode_rust_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ theme_dawn_js }}" type="text/javascript" charset="utf-8"></script>
<script src="{{ theme_tomorrow_night_js }}" type="text/javascript" charset="utf-8"></script>
{{/if}}
<script src="highlight.js"></script>
<script src="book.js"></script>

View file

@ -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");

View file

@ -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);

View file

@ -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<u8>,
pub ace_js: Vec<u8>,
pub mode_rust_js: Vec<u8>,
pub theme_dawn_js: Vec<u8>,
pub theme_tomorrow_night_js: Vec<u8>,
}
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
}
}

View file

@ -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

View file

@ -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]