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
This commit is contained in:
Peter Cai 2020-04-12 16:53:51 +08:00
parent 26bb98b11e
commit f519152465
No known key found for this signature in database
GPG Key ID: 71F5FB4E4F3FD54F
9 changed files with 102 additions and 1 deletions

View File

@ -7,6 +7,12 @@ fn main() {
// Load theme name from config.json and output code to load the theme via include_dir! // Load theme name from config.json and output code to load the theme via include_dir!
let config: serde_json::Value = let config: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string("./config.json").unwrap()).unwrap(); 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") { let theme_name = match config.get("theme") {
Some(name) => name, Some(name) => name,
None => panic!("Please define `theme` in `config.json`") 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(); 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.write_all(theme_load_code.as_bytes()).unwrap();
out_file.sync_data().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::<String>();
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();
} }

6
package-lock.json generated
View File

@ -1982,6 +1982,12 @@
"minimalistic-assert": "^1.0.1" "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": { "hmac-drbg": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npm.taobao.org/hmac-drbg/download/hmac-drbg-1.0.1.tgz", "resolved": "https://registry.npm.taobao.org/hmac-drbg/download/hmac-drbg-1.0.1.tgz",

View File

@ -18,6 +18,7 @@
"homepage": "https://github.com/PeterCxy/paprika#readme", "homepage": "https://github.com/PeterCxy/paprika#readme",
"devDependencies": { "devDependencies": {
"@wasm-tool/wasm-pack-plugin": "^1.2.0", "@wasm-tool/wasm-pack-plugin": "^1.2.0",
"highlight.js": "^9.18.1",
"webpack": "^4.42.1" "webpack": "^4.42.1"
} }
} }

View File

@ -120,7 +120,7 @@ impl Post {
// library updates. Updaing this value invalidates all // library updates. Updaing this value invalidates all
// existing cache and they will be recompiled when someone // existing cache and they will be recompiled when someone
// visits. // visits.
const CACHE_VERSION: &'static str = "0009"; const CACHE_VERSION: &'static str = "0015";
// The prefix path used for caching remote images // The prefix path used for caching remote images
pub const IMG_CACHE_PREFIX: &'static str = "/imgcache/"; pub const IMG_CACHE_PREFIX: &'static str = "/imgcache/";
@ -267,6 +267,13 @@ impl PostContentCache {
last_idx = idx + 3 + inserted_id.len(); last_idx = idx + 3 + inserted_id.len();
} }
// Transform all <pre><code> to <pre><code class="hljs">
// 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("<pre><code", "<pre><code class=\"hljs\" ");
} }
// Only renders the content and spits out a cache object // Only renders the content and spits out a cache object
@ -278,10 +285,35 @@ impl PostContentCache {
// because we need to asynchronously transform the events // because we need to asynchronously transform the events
// which could not be done through mapping on iterators // which could not be done through mapping on iterators
let mut parser: Vec<Event> = Parser::new_ext(&post.content, Options::all()).collect(); let mut parser: Vec<Event> = 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() { for ev in parser.iter_mut() {
match ev { 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) => { Event::Start(tag) | Event::End(tag) => {
Self::transform_tag(tag).await; 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());
}
} }
_ => () _ => ()
}; };

17
src/hljs.rs Normal file
View File

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

7
src/hljs_tpl.rs Normal file
View File

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

View File

@ -13,6 +13,7 @@ extern crate serde_json;
mod utils; mod utils;
mod router; mod router;
mod store; mod store;
mod hljs;
mod blog; mod blog;
mod sn; mod sn;
mod render; mod render;

View File

@ -2,6 +2,7 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ #if title }}{{ title }} - {{ blog.title }}{{ else }}{{ blog.title }}{{ /if }}</title> <title>{{ #if title }}{{ title }} - {{ blog.title }}{{ else }}{{ blog.title }}{{ /if }}</title>
<link rel="stylesheet" href="/static/monokai-sublime.css?ver={{ build_num }}" />
<link rel="stylesheet" href="/static/style.css?ver={{ build_num }}" /> <link rel="stylesheet" href="/static/style.css?ver={{ build_num }}" />
<link rel="alternate" type="application/rss+xml" title="RSS Feed for {{ blog.title }}" href="/feed.xml" /> <link rel="alternate" type="application/rss+xml" title="RSS Feed for {{ blog.title }}" href="/feed.xml" />
</head> </head>

View File

@ -0,0 +1 @@
../../../node_modules/highlight.js/styles/monokai-sublime.css