Skip to content

Commit fac775c

Browse files
committed
[Feat] enhance code block highlight
1 parent f9fbe88 commit fac775c

File tree

9 files changed

+204
-79
lines changed

9 files changed

+204
-79
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,4 @@ once_cell = "1.19"
5050
toml = "0.8"
5151
syntect = { version = "5", default-features = false, features = ["default-fancy", "html"] }
5252
katex = { version = "0.4.6", default-features = false, features = ["wasm-js"] }
53+
gloo-worker = { version = "0.4", features = ["futures"] }

app/index.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
<title>zzhack</title>
66
<link data-trunk rel="tailwind-css" href="src/styles/global.css"/>
77
<link data-trunk rel="copy-dir" href="../data" />
8+
<link data-trunk rel="rust" data-bin="zzhack-v6" data-type="main" />
9+
<link data-trunk rel="rust" data-bin="highlight_worker" data-type="worker" />
810
</head>
911
<body></body>
1012
</html>

app/src/bin/highlight_worker.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
use zzhack_v6::highlight_service::worker;
2+
3+
fn main() {
4+
worker::register();
5+
}

app/src/components/markdown_renderer/code_block.rs

Lines changed: 64 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use std::{cell::Cell, rc::Rc};
2+
3+
use wasm_bindgen_futures::spawn_local;
14
use yew::prelude::*;
25

36
use crate::highlight_service::HighlightService;
@@ -11,22 +14,72 @@ pub struct CodeBlockProps {
1114

1215
#[function_component(CodeBlock)]
1316
pub fn code_block(props: &CodeBlockProps) -> Html {
14-
let lang = props.language.as_deref();
15-
let highlighted = HighlightService::highlight_lines_html(&props.code, lang);
16-
let lang_class = lang.map(|l| format!("language-{}", l));
17+
let highlighted_lines = use_state(|| None::<Vec<String>>);
18+
let lang_class = props.language.as_deref().map(|l| format!("language-{}", l));
19+
20+
{
21+
let highlighted_lines = highlighted_lines.clone();
22+
use_effect_with((props.code.clone(), props.language.clone()), move |deps| {
23+
let (code, language) = deps;
24+
let highlighted_lines = highlighted_lines.clone();
25+
let code_owned = code.to_string();
26+
let language_owned = language.clone().map(|value| value.to_string());
27+
let is_cancelled = Rc::new(Cell::new(false));
28+
let cancel_flag = is_cancelled.clone();
29+
30+
highlighted_lines.set(None);
31+
32+
spawn_local(async move {
33+
let language_ref = language_owned.as_deref();
34+
let lines = HighlightService::highlight_lines_html(&code_owned, language_ref).await;
35+
36+
if !cancel_flag.get() {
37+
highlighted_lines.set(Some(lines));
38+
}
39+
});
40+
41+
move || {
42+
is_cancelled.set(true);
43+
}
44+
});
45+
}
46+
47+
let render_line = |index: usize, content: Html| {
48+
html! {
49+
<span class="grid grid-cols-[auto,1fr] gap-4">
50+
<span class="w-12 select-none text-right pr-2 text-slate-500 tabular-nums" aria-hidden="true">{ index + 1 }</span>
51+
<span class="block whitespace-pre">
52+
{ content }
53+
</span>
54+
</span>
55+
}
56+
};
1757

1858
html! {
1959
<pre class="my-4 overflow-x-auto rounded-lg bg-black/60 text-sm text-slate-100">
2060
<code class={classes!(lang_class, "block", "font-mono", "leading-6")}>
2161
{
22-
for highlighted.iter().enumerate().map(|(index, line)| html! {
23-
<span class="grid grid-cols-[auto,1fr] gap-4">
24-
<span class="w-12 select-none text-right pr-2 text-slate-500 tabular-nums" aria-hidden="true">{ index + 1 }</span>
25-
<span class="block whitespace-pre">
26-
{ Html::from_html_unchecked(AttrValue::from(line.clone())) }
27-
</span>
28-
</span>
29-
})
62+
if let Some(lines) = &*highlighted_lines {
63+
html! {
64+
<>
65+
{
66+
for lines.iter().enumerate().map(|(index, line)| {
67+
render_line(index, Html::from_html_unchecked(AttrValue::from(line.clone())))
68+
})
69+
}
70+
</>
71+
}
72+
} else {
73+
html! {
74+
<>
75+
{
76+
for props.code.as_ref().split_inclusive('\n').enumerate().map(|(index, line)| {
77+
render_line(index, html! { { line } })
78+
})
79+
}
80+
</>
81+
}
82+
}
3083
}
3184
</code>
3285
</pre>

app/src/highlight_engine.rs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
use once_cell::sync::Lazy;
2+
use syntect::{
3+
easy::HighlightLines,
4+
highlighting::{Style, Theme, ThemeSet},
5+
html::{append_highlighted_html_for_styled_line, IncludeBackground},
6+
parsing::SyntaxSet,
7+
util::LinesWithEndings,
8+
};
9+
10+
pub struct HighlightEngine;
11+
12+
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(|| SyntaxSet::load_defaults_newlines());
13+
static THEME: Lazy<Theme> = Lazy::new(|| {
14+
ThemeSet::load_defaults()
15+
.themes
16+
.get("base16-ocean.dark")
17+
.cloned()
18+
.unwrap_or_else(|| ThemeSet::load_defaults().themes["base16-eighties.dark"].clone())
19+
});
20+
21+
impl HighlightEngine {
22+
pub fn highlight_html(code: &str, language: Option<&str>) -> String {
23+
Self::highlight_lines(code, language).join("\n")
24+
}
25+
26+
pub fn highlight_lines(code: &str, language: Option<&str>) -> Vec<String> {
27+
let syntax = language
28+
.and_then(|lang| SYNTAX_SET.find_syntax_by_token(lang))
29+
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
30+
31+
let mut highlighter = HighlightLines::new(syntax, &THEME);
32+
let mut output = Vec::new();
33+
34+
for line in LinesWithEndings::from(code) {
35+
let mut highlighted_line = String::new();
36+
if let Ok(ranges) = highlighter.highlight_line(line, &SYNTAX_SET) {
37+
let _ = append_highlighted_html_for_styled_line(
38+
&ranges,
39+
IncludeBackground::No,
40+
&mut highlighted_line,
41+
);
42+
} else {
43+
let _ = append_highlighted_html_for_styled_line(
44+
&[(Style::default(), line)],
45+
IncludeBackground::No,
46+
&mut highlighted_line,
47+
);
48+
}
49+
50+
let highlighted_line = highlighted_line
51+
.strip_suffix('\n')
52+
.unwrap_or(&highlighted_line)
53+
.to_owned();
54+
output.push(highlighted_line);
55+
}
56+
57+
output
58+
}
59+
}

app/src/highlight_service.rs

Lines changed: 53 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,59 +1,64 @@
1-
use once_cell::sync::Lazy;
2-
use syntect::{
3-
easy::HighlightLines,
4-
highlighting::{Style, Theme, ThemeSet},
5-
html::{append_highlighted_html_for_styled_line, IncludeBackground},
6-
parsing::SyntaxSet,
7-
util::LinesWithEndings,
8-
};
1+
use gloo_worker::{oneshot::OneshotBridge, Spawnable};
2+
use std::cell::RefCell;
3+
4+
use worker::{HighlightRequest, HighlightWorker};
95

106
pub struct HighlightService;
117

12-
static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(|| SyntaxSet::load_defaults_newlines());
13-
static THEME: Lazy<Theme> = Lazy::new(|| {
14-
ThemeSet::load_defaults()
15-
.themes
16-
.get("base16-ocean.dark")
17-
.cloned()
18-
.unwrap_or_else(|| ThemeSet::load_defaults().themes["base16-eighties.dark"].clone())
19-
});
8+
const WORKER_ENTRYPOINT: &str = "/highlight_worker.js";
9+
10+
thread_local! {
11+
static HIGHLIGHT_WORKER: RefCell<Option<OneshotBridge<HighlightWorker>>> = RefCell::new(None);
12+
}
2013

2114
impl HighlightService {
22-
pub fn highlight_html(code: &str, language: Option<&str>) -> String {
23-
Self::highlight_lines_html(code, language).join("\n")
15+
pub async fn highlight_html(code: &str, language: Option<&str>) -> String {
16+
Self::highlight_lines_html(code, language).await.join("\n")
17+
}
18+
19+
pub async fn highlight_lines_html(code: &str, language: Option<&str>) -> Vec<String> {
20+
let request = HighlightRequest {
21+
code: code.to_owned(),
22+
language: language.map(str::to_owned),
23+
};
24+
25+
run_worker(request).await
2426
}
27+
}
2528

26-
pub fn highlight_lines_html(code: &str, language: Option<&str>) -> Vec<String> {
27-
let syntax = language
28-
.and_then(|lang| SYNTAX_SET.find_syntax_by_token(lang))
29-
.unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
30-
31-
let mut highlighter = HighlightLines::new(syntax, &THEME);
32-
let mut output = Vec::new();
33-
34-
for line in LinesWithEndings::from(code) {
35-
let mut highlighted_line = String::new();
36-
if let Ok(ranges) = highlighter.highlight_line(line, &SYNTAX_SET) {
37-
let _ = append_highlighted_html_for_styled_line(
38-
&ranges,
39-
IncludeBackground::No,
40-
&mut highlighted_line,
41-
);
42-
} else {
43-
let _ = append_highlighted_html_for_styled_line(
44-
&[(Style::default(), line)],
45-
IncludeBackground::No,
46-
&mut highlighted_line,
47-
);
48-
}
49-
50-
let highlighted_line = highlighted_line
51-
.strip_suffix('\n')
52-
.unwrap_or(&highlighted_line)
53-
.to_owned();
54-
output.push(highlighted_line);
29+
async fn run_worker(request: HighlightRequest) -> Vec<String> {
30+
let mut bridge = HIGHLIGHT_WORKER.with(|cell| {
31+
let mut worker = cell.borrow_mut();
32+
if worker.is_none() {
33+
let base = HighlightWorker::spawner().spawn(WORKER_ENTRYPOINT);
34+
*worker = Some(base);
5535
}
5636

57-
output
37+
worker.as_ref().expect("worker initialized above").fork()
38+
});
39+
40+
bridge.run(request).await
41+
}
42+
43+
pub mod worker {
44+
use gloo_worker::{oneshot::oneshot, Registrable};
45+
use serde::{Deserialize, Serialize};
46+
47+
#[derive(Debug, Clone, Serialize, Deserialize)]
48+
pub struct HighlightRequest {
49+
pub code: String,
50+
pub language: Option<String>,
51+
}
52+
53+
#[oneshot(HighlightWorker)]
54+
pub(crate) async fn highlight_worker(request: HighlightRequest) -> Vec<String> {
55+
crate::highlight_engine::HighlightEngine::highlight_lines(
56+
&request.code,
57+
request.language.as_deref(),
58+
)
59+
}
60+
61+
pub fn register() {
62+
HighlightWorker::registrar().register();
5863
}
5964
}

app/src/lib.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
pub mod app;
2+
pub mod cache_service;
3+
pub mod commands;
4+
pub mod commands_history_service;
5+
pub mod components;
6+
pub mod config_service;
7+
pub mod highlight_engine;
8+
pub mod highlight_service;
9+
pub mod markdown_renderer;
10+
pub mod router;
11+
pub mod terminal;
12+
pub mod terminal_state;
13+
pub mod types;
14+
pub mod utils;
15+
pub mod vfs_data;

app/src/main.rs

Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,15 @@
1-
mod app;
2-
mod cache_service;
3-
mod commands;
4-
mod commands_history_service;
5-
mod components;
6-
mod config_service;
7-
mod highlight_service;
8-
mod markdown_renderer;
9-
mod router;
10-
mod terminal;
11-
mod terminal_state;
12-
mod types;
13-
mod utils;
14-
mod vfs_data;
15-
16-
use app::App;
171
use tracing_subscriber::{filter::Targets, prelude::*};
182
use tracing_web::MakeWebConsoleWriter;
3+
use zzhack_v6::app::App;
194

205
fn main() {
21-
// Configure tracing to output to browser console
226
let fmt_layer = tracing_subscriber::fmt::layer()
23-
.with_ansi(true) // This might not work in all browsers
24-
.without_time() // std::time is not available in browsers
7+
.with_ansi(true)
8+
.without_time()
259
.with_writer(MakeWebConsoleWriter::new())
2610
.with_filter(
2711
Targets::new()
28-
.with_target("yew", tracing::Level::DEBUG) // yew trace can be verbose
12+
.with_target("yew", tracing::Level::DEBUG)
2913
.with_default(tracing::Level::TRACE),
3014
);
3115

0 commit comments

Comments
 (0)