Skip to content

Commit

Permalink
Highlight code by JS.
Browse files Browse the repository at this point in the history
So that we can show line numbers.
  • Loading branch information
hongquan committed Mar 28, 2024
1 parent 555964e commit 846305a
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 14 deletions.
13 changes: 12 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "quanweb"
version = "1.1.0"
version = "1.2.0"
edition = "2021"
rust-version = "1.76"
default-run = "quanweb"
Expand Down Expand Up @@ -31,6 +31,7 @@ field_names = "0.2.0"
fluent-bundle = "0.15.2"
fluent-templates = "0.8.0"
fred = { version = "8.0.2", features = ["tracing"] }
htmlize = "1.0.5"
http = "1.0.0"
indexmap = { version = "2.0.0", features = ["serde"] }
libpassgen = "1.0.3"
Expand Down
39 changes: 37 additions & 2 deletions minijinja/base.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
{%- endblock css %}

{% block headjs -%}
<script src='https://unpkg.com/alpinejs' defer></script>
<script src='https://unpkg.com/[email protected]/dist/htmx.min.js' defer></script>
{%- endblock headjs %}
{%- endblock head %}
Expand Down Expand Up @@ -78,9 +77,45 @@
get created_at_full_display() {
return this.created_at ? longFormatter.format(this.created_at) : ''
},
}))
}));
})
</script>
<script type='module'>
import { codeToHtml } from 'https://esm.sh/[email protected]'
document.addEventListener('alpine:init', () => {
Alpine.data('need_highlight', () => ({
code: '',
lang: 'text',
init() {
const codeElm = this.$refs.orig_code;
const code = codeElm.textContent
this.code = code.trim()
let className = Array.from(codeElm.classList.values()).find((c) => c.startsWith('language-'))
if (className) {
this.lang = className.split('-')[1]
}
},
async highlight() {
const html = await codeToHtml(this.code, { lang: this.lang, theme: 'one-dark-pro' })
return html
}
}))
})
</script>
{%- endblock js %}
<script type='module'>
// There are pages which only have tiny apps defined directly in HTML markup, without any
// <script type='module'> block. We need to load Alpine to activate those apps too.
import Alpine from "https://esm.sh/[email protected]"
if (!window.Alpine) {
window.Alpine = Alpine
}
if (window.Alpine) {
console.log('Start tiny apps')
window.Alpine.start()
}
</script>
</body>
</html>
3 changes: 2 additions & 1 deletion src/consts.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
use syntect::html::ClassStyle;


pub const DB_NAME: &str = "quanweb";
pub const DEFAULT_PAGE_SIZE: u8 = 10;
pub const STATIC_URL: &str = "/static";
pub const UNCATEGORIZED_URL: &str = "/category/_uncategorized/";
pub const SYNTECT_CLASS_STYLE: ClassStyle = ClassStyle::SpacedPrefixed { prefix: "st-" };
pub const KEY_LANG: &str = "lang";
pub const DEFAULT_LANG: &str = "en";
pub const ALPINE_HIGHLIGHTING_APP: &str = "need_highlight";
pub const ALPINE_ORIG_CODE_ELM: &str = "orig_code";
73 changes: 64 additions & 9 deletions src/utils/markdown.rs
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
use std::collections::HashMap;
use std::io::Write;

use htmlize::escape_text;
use comrak::adapters::SyntaxHighlighterAdapter;
use comrak::html;
use comrak::{markdown_to_html_with_plugins, ComrakOptions, ComrakPlugins};
use syntect::html::ClassedHTMLGenerator;
use syntect::parsing::{SyntaxSet, SyntaxReference};
use syntect::parsing::{SyntaxReference, SyntaxSet};
use syntect::util::LinesWithEndings;

use crate::consts::SYNTECT_CLASS_STYLE;
use crate::consts::{SYNTECT_CLASS_STYLE, ALPINE_HIGHLIGHTING_APP, ALPINE_ORIG_CODE_ELM};

pub struct CssSyntectAdapter {
syntax_set: SyntaxSet,
}

#[allow(dead_code)]
impl CssSyntectAdapter {
pub fn new() -> Self {
Self {
syntax_set: two_face::syntax::extra_newlines(),
}
}

fn highlight_html(&self, code: &str, syntax: &SyntaxReference) -> Result<String, syntect::Error> {
fn highlight_html(
&self,
code: &str,
syntax: &SyntaxReference,
) -> Result<String, syntect::Error> {
let mut html_generator = ClassedHTMLGenerator::new_with_class_style(
syntax,
&self.syntax_set,
Expand All @@ -34,7 +43,7 @@ impl CssSyntectAdapter {
impl SyntaxHighlighterAdapter for CssSyntectAdapter {
fn write_highlighted(
&self,
output: &mut dyn std::io::Write,
output: &mut dyn Write,
lang: Option<&str>,
code: &str,
) -> std::io::Result<()> {
Expand All @@ -60,25 +69,71 @@ impl SyntaxHighlighterAdapter for CssSyntectAdapter {

fn write_pre_tag(
&self,
output: &mut dyn std::io::Write,
attributes: std::collections::HashMap<String, String>,
output: &mut dyn Write,
attributes: HashMap<String, String>,
) -> std::io::Result<()> {
html::write_opening_tag(output, "pre", attributes)
}

fn write_code_tag(
&self,
output: &mut dyn std::io::Write,
attributes: std::collections::HashMap<String, String>,
output: &mut dyn Write,
attributes: HashMap<String, String>,
) -> std::io::Result<()> {
html::write_opening_tag(output, "code", attributes)
}
}

// A simple adapter that defers highlighting job to the client side
pub struct JSHighlightSyntectAdapter;

impl SyntaxHighlighterAdapter for JSHighlightSyntectAdapter {
fn write_highlighted(
&self,
output: &mut dyn Write,
_lang: Option<&str>,
code: &str,
) -> std::io::Result<()> {
let code = escape_text(code);
output.write_all(code.as_bytes())
}

fn write_pre_tag(
&self,
output: &mut dyn Write,
mut attributes: HashMap<String, String>,
) -> std::io::Result<()> {
let classname = " q-need-highlight not-prose p-0";
if let Some(class) = attributes.get_mut("class") {
class.push_str(classname)
} else {
attributes.insert("class".to_string(), classname.to_string());
};
attributes.insert("x-data".to_string(), ALPINE_HIGHLIGHTING_APP.to_string());
attributes.insert("x-html".to_string(), "highlight()".to_string());
html::write_opening_tag(output, "pre", attributes)
}

fn write_code_tag(
&self,
output: &mut dyn Write,
mut attributes: HashMap<String, String>,
) -> std::io::Result<()> {
let classname = " q-code";
if let Some(class) = attributes.get_mut("class") {
class.push_str(classname)
} else {
attributes.insert("class".to_string(), classname.to_string());
};
attributes.insert("x-ref".to_string(), ALPINE_ORIG_CODE_ELM.to_string());
html::write_opening_tag(output, "code", attributes)
}
}

pub fn markdown_to_html(markdown: &str) -> String {
let options = ComrakOptions::default();
let mut plugins = ComrakPlugins::default();
let adapter = CssSyntectAdapter::new();
let adapter = JSHighlightSyntectAdapter;
plugins.render.codefence_syntax_highlighter = Some(&adapter);
markdown_to_html_with_plugins(markdown, &options, &plugins)
}
Expand Down
21 changes: 21 additions & 0 deletions static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,24 @@ article .entry-content.front h2 {
.asciicast:fullscreen {
width: 100%;
}

/* Code line numbering */
.q-need-highlight code {
counter-reset: step;
counter-increment: step 0;
}

.q-need-highlight code .line::before {
content: counter(step);
counter-increment: step;
width: 1rem;
margin-right: 1rem;
display: inline-block;
text-align: right;
color: rgba(115,138,148,.4)
}

pre.shiki {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
1 change: 1 addition & 0 deletions tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ module.exports = {
content: [
'./templates/**/*.jinja',
'./minijinja/**/*.jinja',
'./src/**/*.rs',
],
theme: {
extend: {
Expand Down

0 comments on commit 846305a

Please sign in to comment.