Skip to content

Commit

Permalink
Let opt-in showing line numbers.
Browse files Browse the repository at this point in the history
  • Loading branch information
hongquan committed Mar 29, 2024
1 parent 846305a commit e985b56
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 34 deletions.
63 changes: 63 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ rust-embed = { version = "8.0.0", features = ["axum", "mime-guess", "include-exc
serde = { version = "1.0.164", features = ["serde_derive"] }
serde-value = "0.7.0"
serde_json = "1.0.99"
serde_json5 = "0.1.0"
serde_with = "3.0.0"
smart-default = "0.7.1"
str-macro = "1.0.0"
Expand Down
34 changes: 32 additions & 2 deletions minijinja/base.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -82,21 +82,51 @@
</script>
<script type='module'>
import { codeToHtml } from 'https://esm.sh/shiki@1.0.0'
// Our old <code> element will be replaced by the one created by Shiki,
// we need to keep old class names and copy to the new one.
function getShikiOpt(lang, classes) {
return {
lang,
theme: 'one-dark-pro',
// Ref: https://shiki.style/guide/transformers#transformer-hooks
transformers: [
{
code(node) {
classes.forEach((c) => this.addClassToHast(node, c))
},
pre(node) {
if (!classes.includes('q-with-lineno')) {
this.addClassToHast(node, 'p-4')
}
},
}
]
}
}
document.addEventListener('alpine:init', () => {
Alpine.data('need_highlight', () => ({
code: '',
classes: [],
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-'))
let classes = Array.from(codeElm.classList.values())
let className = classes.find((c) => c.startsWith('language-'))
if (className) {
this.lang = className.split('-')[1]
}
this.classes = classes
},
async highlight() {
const html = await codeToHtml(this.code, { lang: this.lang, theme: 'one-dark-pro' })
const lang = this.lang
const classes = this.classes
const opts = getShikiOpt(lang, classes)
const html = await codeToHtml(this.code, opts)
return html
}
}))
Expand Down
2 changes: 2 additions & 0 deletions src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ 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";
// Given by comrak
pub const ATTR_CODEFENCE_EXTRA: &str = "data-meta";
8 changes: 8 additions & 0 deletions src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,11 @@ pub fn create_shape_element<N: ToString>(name: N, cardinality: Cardinality) -> S
flag_implicit: false,
}
}

#[derive(Debug, Deserialize, SmartDefault)]
#[serde(default)]
pub struct CodeFenceOptions {
pub lines: bool,
#[default = 1]
pub start_line: u8,
}
96 changes: 71 additions & 25 deletions src/utils/markdown.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
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 comrak::{
markdown_to_html_with_plugins, ExtensionOptionsBuilder, Options, PluginsBuilder,
RenderOptionsBuilder, RenderPluginsBuilder,
};
use htmlize::escape_text;
use serde_json5;
use syntect::html::ClassedHTMLGenerator;
use syntect::parsing::{SyntaxReference, SyntaxSet};
use syntect::util::LinesWithEndings;

use crate::consts::{SYNTECT_CLASS_STYLE, ALPINE_HIGHLIGHTING_APP, ALPINE_ORIG_CODE_ELM};
use crate::consts::{
ALPINE_HIGHLIGHTING_APP, ALPINE_ORIG_CODE_ELM, ATTR_CODEFENCE_EXTRA, SYNTECT_CLASS_STYLE,
};
use crate::types::CodeFenceOptions;

pub struct CssSyntectAdapter {
syntax_set: SyntaxSet,
Expand Down Expand Up @@ -85,24 +92,24 @@ impl SyntaxHighlighterAdapter for CssSyntectAdapter {
}

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

impl SyntaxHighlighterAdapter for JSHighlightSyntectAdapter {
impl SyntaxHighlighterAdapter for JSHighlightAdapter {
fn write_highlighted(
&self,
output: &mut dyn Write,
_lang: Option<&str>,
code: &str,
) -> std::io::Result<()> {
&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<()> {
&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)
Expand All @@ -115,26 +122,65 @@ impl SyntaxHighlighterAdapter for JSHighlightSyntectAdapter {
}

fn write_code_tag(
&self,
output: &mut dyn Write,
mut attributes: HashMap<String, String>,
) -> std::io::Result<()> {
let classname = " q-code";
&self,
output: &mut dyn Write,
mut attributes: HashMap<String, String>,
) -> std::io::Result<()> {
tracing::info!("Attributes for code: {:?}", attributes);
let mut class_names = vec!["q-code"];
let mut styles = vec![];
if let Some(info_string) = attributes.get(ATTR_CODEFENCE_EXTRA) {
tracing::info!("Attempt to parse: {}", info_string);
let codefence_opts: CodeFenceOptions = serde_json5::from_str(info_string.as_str())
.inspect_err(|e| tracing::warn!("Failed to parse codefence extra. {e}"))
.unwrap_or_default();
if codefence_opts.lines {
class_names.push("q-with-lineno")
}
styles.push(format!("--line-start={}", codefence_opts.start_line));
};
let extra_class = format!(" {}", class_names.join(" "));
if let Some(class) = attributes.get_mut("class") {
class.push_str(classname)
class.push_str(&extra_class)
} else {
attributes.insert("class".to_string(), classname.to_string());
attributes.insert("class".to_string(), extra_class);
};
if let Some(style) = attributes.get("style") {
styles.extend(style.split(';').map(String::from));
}
if !styles.is_empty() {
let style_s = styles.join(" ");
attributes.insert("style".to_string(), style_s);
}
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 = JSHighlightSyntectAdapter;
plugins.render.codefence_syntax_highlighter = Some(&adapter);
let extension = ExtensionOptionsBuilder::default()
.table(true)
.autolink(true)
.build()
.unwrap_or_default();
let render = RenderOptionsBuilder::default()
.full_info_string(true)
.build()
.unwrap_or_default();
let options = Options {
extension,
render,
..Default::default()
};
let adapter = JSHighlightAdapter;
let render = RenderPluginsBuilder::default()
.codefence_syntax_highlighter(Some(&adapter))
.build()
.unwrap_or_default();
let plugins = PluginsBuilder::default()
.render(render)
.build()
.unwrap_or_default();
markdown_to_html_with_plugins(markdown, &options, &plugins)
}

Expand Down
9 changes: 2 additions & 7 deletions static/css/custom.css
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,12 @@ article .entry-content.front h2 {
}

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

.q-need-highlight code .line::before {
.q-need-highlight code.q-with-lineno .line::before {
content: counter(step);
counter-increment: step;
width: 1rem;
Expand All @@ -81,8 +81,3 @@ article .entry-content.front h2 {
text-align: right;
color: rgba(115,138,148,.4)
}

pre.shiki {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}

0 comments on commit e985b56

Please sign in to comment.