1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! Code block rendering and line annotation output.
//!
//! Metadata parsing is handled by `html::code_annotations`; this module decides how the
//! resulting line states become `<pre><code>` attributes, wrapper spans, line numbers,
//! and compatibility classes.
use compact_str::CompactString;
use ox_content_ast::CodeBlock;
use smallvec::SmallVec;
use super::super::code_annotations::{
apply_annotation_numbers, apply_btree_annotations, normalize_code_block_info,
parse_code_annotations, parse_line_numbers, parse_vitepress_inline_annotations,
split_code_block_meta, CodeAnnotationKind, CodeBlockRenderState, CodeLineRenderState,
MetaTokenKind,
};
use super::HtmlRenderer;
impl HtmlRenderer {
/// Builds the normalized render state for one fenced code block.
///
/// The renderer only pays the expensive annotation parsers when annotation
/// output is enabled and the configured syntax can produce them. Plain code
/// blocks become a simple line list, while VitePress inline annotations and
/// meta annotations share the same `CodeLineRenderState` vector so later
/// rendering walks each line once.
pub(in crate::html::renderer) fn build_code_block_state(
&self,
code_block: &CodeBlock<'_>,
) -> CodeBlockRenderState {
let info = normalize_code_block_info(code_block.lang, code_block.meta);
let syntax = self.options.code_annotation_syntax;
let mut lines = if self.options.code_annotations && syntax.includes_vitepress() {
parse_vitepress_inline_annotations(code_block.value)
} else {
code_block
.value
.split('\n')
.map(|line| CodeLineRenderState {
value: line.to_string(),
annotations: SmallVec::new(),
})
.collect()
};
let mut title = None;
let mut line_numbers_start = if self.options.code_annotations
&& syntax.includes_vitepress()
&& self.options.code_annotation_default_line_numbers
{
Some(1)
} else {
None
};
if self.options.code_annotations && !info.meta.is_empty() {
if syntax.includes_attribute() {
let annotations = parse_code_annotations(
info.meta.as_str(),
&self.options.code_annotation_meta_key,
);
apply_btree_annotations(&mut lines, &annotations);
}
if syntax.includes_vitepress() {
for token in split_code_block_meta(info.meta.as_str()) {
match token.kind {
MetaTokenKind::Braces => {
let line_numbers = parse_line_numbers(token.value);
apply_annotation_numbers(
&mut lines,
&line_numbers,
CodeAnnotationKind::Highlight,
);
}
MetaTokenKind::Brackets => {
if title.is_none() && !token.value.trim().is_empty() {
title = Some(CompactString::from(token.value.trim()));
}
}
MetaTokenKind::Raw => {
if token.value == ":line-numbers" {
line_numbers_start = Some(1);
} else if let Some(start) =
token.value.strip_prefix(":line-numbers=").and_then(|value| {
value
.trim()
.parse::<usize>()
.ok()
.filter(|line_number| *line_number > 0)
})
{
line_numbers_start = Some(start);
} else if token.value == ":no-line-numbers" {
line_numbers_start = None;
}
}
}
}
}
}
CodeBlockRenderState { language: info.language, title, line_numbers_start, lines }
}
/// Emits annotated code lines from precomputed render state.
///
/// Class names use `SmallVec` because typical lines have only one or two
/// classes, but focus/highlight/diff combinations can add a few more. This
/// keeps the common case stack-backed while still preserving de-duplication
/// when multiple annotations imply the same class.
pub(in crate::html::renderer) fn write_code_lines(&mut self, state: &CodeBlockRenderState) {
let has_focus = state.has_focus();
for (index, line) in state.lines.iter().enumerate() {
let line_number = index + 1;
let mut class_names: SmallVec<[&str; 8]> = SmallVec::new();
class_names.push("line");
class_names.push("ox-code-line");
for annotation in &line.annotations {
let class_name = annotation.class_name();
if !class_names.contains(&class_name) {
class_names.push(class_name);
}
for extra_class_name in annotation.extra_class_names() {
if !class_names.contains(extra_class_name) {
class_names.push(extra_class_name);
}
}
}
if has_focus && !line.annotations.contains(&CodeAnnotationKind::Focus) {
class_names.push("ox-code-line--dimmed");
}
self.write("<span class=\"");
self.write(&class_names.join(" "));
self.write("\" data-line=\"");
self.write_display(line_number);
self.write("\"");
if let Some(start) = state.line_numbers_start {
self.write(" data-line-number=\"");
self.write_display(start + index);
self.write("\"");
}
self.write(">");
self.write_escaped(&line.value);
self.write("</span>");
if index + 1 < state.lines.len() {
self.write("\n");
}
}
}
}