From f519152465dc1caa125d3d220c8f370316035dde Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sun, 12 Apr 2020 16:53:51 +0800 Subject: [PATCH] integrate hljs into backend to provide code highlighting 1. Use hljs and wasm_bindgen to parse code 2. Allow the final user to choose what language to include by generating inline_js in build.rs for #[wasm_bindgen] (a thin wrapper around the raw hljs to load whatever is needed by config.json statically rather than dynamically) 3. Integrate the monokai color scheme into our default theme --- build.rs | 35 ++++++++++++++++++++++++ package-lock.json | 6 ++++ package.json | 1 + src/blog.rs | 34 ++++++++++++++++++++++- src/hljs.rs | 17 ++++++++++++ src/hljs_tpl.rs | 7 +++++ src/lib.rs | 1 + theme/default/head.hbs | 1 + theme/default/static/monokai-sublime.css | 1 + 9 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/hljs.rs create mode 100644 src/hljs_tpl.rs create mode 120000 theme/default/static/monokai-sublime.css diff --git a/build.rs b/build.rs index 40aee0c..2ab7e9f 100644 --- a/build.rs +++ b/build.rs @@ -7,6 +7,12 @@ fn main() { // Load theme name from config.json and output code to load the theme via include_dir! let config: serde_json::Value = serde_json::from_str(&std::fs::read_to_string("./config.json").unwrap()).unwrap(); + + generate_theme_loader(&config); + generate_hljs_loader(&config); +} + +fn generate_theme_loader(config: &serde_json::Value) { let theme_name = match config.get("theme") { Some(name) => name, None => panic!("Please define `theme` in `config.json`") @@ -16,4 +22,33 @@ fn main() { let mut out_file = std::fs::File::create(out_path.join("load_theme.rs")).unwrap(); out_file.write_all(theme_load_code.as_bytes()).unwrap(); out_file.sync_data().unwrap(); +} + +fn generate_hljs_loader(config: &serde_json::Value) { + println!("cargo:rerun-if-changed=src/hljs_tpl.rs"); + + let highlight_lang = match config.get("hljs") { + Some(val) => val, + None => panic!("Please specify what language for hljs to support in `config.json` with `hljs`") + }; + + if !highlight_lang.is_array() { + panic!("`hljs` is not an array"); + } + + let highlight_lang = highlight_lang.as_array().unwrap().into_iter().map(|lang| { + let lang = lang.as_str().unwrap(); + // Require only the needed language definition files + format!("hljs.registerLanguage('{}', require('highlight.js/lib/languages/{}'));\n", lang, lang) + }).collect::(); + + let js_code = format!( + "const hljs = require(\\\"highlight.js/lib/highlight.js\\\");\n{}module.exports = hljs;", + highlight_lang); + let rs_code = std::fs::read_to_string("./src/hljs_tpl.rs").unwrap(); + let rs_code = format!("#[wasm_bindgen(inline_js = \"{}\")]\n{}", js_code, rs_code); + let out_path = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); + let mut out_file = std::fs::File::create(out_path.join("load_hljs.rs")).unwrap(); + out_file.write_all(rs_code.as_bytes()).unwrap(); + out_file.sync_data().unwrap(); } \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bab64b4..a4c924c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1982,6 +1982,12 @@ "minimalistic-assert": "^1.0.1" } }, + "highlight.js": { + "version": "9.18.1", + "resolved": "https://registry.npm.taobao.org/highlight.js/download/highlight.js-9.18.1.tgz", + "integrity": "sha1-7SGqAB/mJSuxCj121HVzxlOf4Tw=", + "dev": true + }, "hmac-drbg": { "version": "1.0.1", "resolved": "https://registry.npm.taobao.org/hmac-drbg/download/hmac-drbg-1.0.1.tgz", diff --git a/package.json b/package.json index a9e4e5f..aeac0b4 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "homepage": "https://github.com/PeterCxy/paprika#readme", "devDependencies": { "@wasm-tool/wasm-pack-plugin": "^1.2.0", + "highlight.js": "^9.18.1", "webpack": "^4.42.1" } } diff --git a/src/blog.rs b/src/blog.rs index 28854f0..9acea93 100644 --- a/src/blog.rs +++ b/src/blog.rs @@ -120,7 +120,7 @@ impl Post { // library updates. Updaing this value invalidates all // existing cache and they will be recompiled when someone // visits. -const CACHE_VERSION: &'static str = "0009"; +const CACHE_VERSION: &'static str = "0015"; // The prefix path used for caching remote images pub const IMG_CACHE_PREFIX: &'static str = "/imgcache/"; @@ -267,6 +267,13 @@ impl PostContentCache { last_idx = idx + 3 + inserted_id.len(); } + + // Transform all
 to 

+        // For syntax highlighting
+        // Note that though the Markdown engine may also insert classes,
+        // because we insert our class before that class, ours will
+        // always take precedence
+        *html = html.replace("
 = Parser::new_ext(&post.content, Options::all()).collect();
+        let mut in_code_block = false;
+        let mut code_block_lang = "";
         for ev in parser.iter_mut() {
             match ev {
+                Event::Start(Tag::CodeBlock(block)) => {
+                    in_code_block = true;
+                    match block {
+                        CodeBlockKind::Fenced(lang) => code_block_lang = lang,
+                        CodeBlockKind::Indented => code_block_lang = ""
+                    }
+                },
+                Event::End(Tag::CodeBlock(_)) => {
+                    in_code_block = false;
+                    code_block_lang = "";
+                },
                 Event::Start(tag) | Event::End(tag) => {
                     Self::transform_tag(tag).await;
+                },
+                Event::Text(text) => {
+                    if in_code_block {
+                        let highlighted = if code_block_lang != "" {
+                            crate::hljs::highlight(code_block_lang, text)
+                        } else {
+                            crate::hljs::highlight_auto(text)
+                        };
+
+                        *ev = Event::Html(
+                            highlighted.into());
+                    }
                 }
                 _ => ()
             };
diff --git a/src/hljs.rs b/src/hljs.rs
new file mode 100644
index 0000000..7dbdc17
--- /dev/null
+++ b/src/hljs.rs
@@ -0,0 +1,17 @@
+// Simple bindings for Highlight.js
+// We don't have something equivalent in Rust
+// and I don't really want to run these on client
+use js_sys::Reflect;
+use wasm_bindgen::prelude::*;
+
+include!(concat!(env!("OUT_DIR"), "/load_hljs.rs"));
+
+pub fn highlight_auto(code: &str) -> String {
+    Reflect::get(&hljs_highlight_auto(code), &"value".into())
+        .unwrap().as_string().unwrap()
+}
+
+pub fn highlight(lang: &str, code: &str) -> String {
+    Reflect::get(&hljs_highlight(lang, code), &"value".into())
+        .unwrap().as_string().unwrap()
+}
\ No newline at end of file
diff --git a/src/hljs_tpl.rs b/src/hljs_tpl.rs
new file mode 100644
index 0000000..19d16bd
--- /dev/null
+++ b/src/hljs_tpl.rs
@@ -0,0 +1,7 @@
+// Will be loaded by build.rs and add #[wasm_bindgen(inline_js = "...")] here
+extern "C" {
+    #[wasm_bindgen(js_name = "highlightAuto")]
+    fn hljs_highlight_auto(code: &str) -> JsValue;
+    #[wasm_bindgen(js_name = "highlight")]
+    fn hljs_highlight(lang: &str, code: &str) -> JsValue;
+}
\ No newline at end of file
diff --git a/src/lib.rs b/src/lib.rs
index fc610f2..20f6858 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -13,6 +13,7 @@ extern crate serde_json;
 mod utils;
 mod router;
 mod store;
+mod hljs;
 mod blog;
 mod sn;
 mod render;
diff --git a/theme/default/head.hbs b/theme/default/head.hbs
index 18346b9..e2d5d0b 100644
--- a/theme/default/head.hbs
+++ b/theme/default/head.hbs
@@ -2,6 +2,7 @@
     
     
     {{ #if title }}{{ title }} - {{ blog.title }}{{ else }}{{ blog.title }}{{ /if }}
+    
     
     
 
\ No newline at end of file
diff --git a/theme/default/static/monokai-sublime.css b/theme/default/static/monokai-sublime.css
new file mode 120000
index 0000000..fd6c528
--- /dev/null
+++ b/theme/default/static/monokai-sublime.css
@@ -0,0 +1 @@
+../../../node_modules/highlight.js/styles/monokai-sublime.css
\ No newline at end of file