1use crate::{
9 core::{
10 assets,
11 attrs::{component_attributes, lang_attribute},
12 },
13 prelude::*,
14 theme::use_stylesheets,
15};
16const BEM: Bem = Bem::new("fc-code");
17const TOK: Bem = Bem::new("fc-code__tok");
18fc_modifier! { enum Layout for BEM { Wrap => None, Numbered => "numbered", Scroll => "scroll" } }
19fc_modifier! { enum Support for BEM { Highlighted => None, Unsupported => "unsupported" } }
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23#[non_exhaustive]
24pub enum SyntaxRole {
25 Fg,
27 Comment,
29 Keyword,
31 String,
33 Number,
35 Function,
37 Type,
39 Operator,
41 Punctuation,
43 Property,
45 Constant,
47 Tag,
49 Attribute,
51 Regexp,
53 Heading,
55 Link,
57 Invalid,
59}
60impl SyntaxRole {
61 pub const ALL: [Self; 17] = [
63 Self::Fg,
64 Self::Comment,
65 Self::Keyword,
66 Self::String,
67 Self::Number,
68 Self::Function,
69 Self::Type,
70 Self::Operator,
71 Self::Punctuation,
72 Self::Property,
73 Self::Constant,
74 Self::Tag,
75 Self::Attribute,
76 Self::Regexp,
77 Self::Heading,
78 Self::Link,
79 Self::Invalid,
80 ];
81 const fn suffix(self) -> &'static str {
82 match self {
83 Self::Fg => "fg",
84 Self::Comment => "comment",
85 Self::Keyword => "keyword",
86 Self::String => "string",
87 Self::Number => "number",
88 Self::Function => "function",
89 Self::Type => "type",
90 Self::Operator => "operator",
91 Self::Punctuation => "punctuation",
92 Self::Property => "property",
93 Self::Constant => "constant",
94 Self::Tag => "tag",
95 Self::Attribute => "attribute",
96 Self::Regexp => "regexp",
97 Self::Heading => "heading",
98 Self::Link => "link",
99 Self::Invalid => "invalid",
100 }
101 }
102}
103
104#[derive(Debug, Clone, PartialEq, Eq)]
106pub struct SyntaxSpan {
107 pub role: SyntaxRole,
109 pub text: String,
111}
112impl SyntaxSpan {
113 #[must_use]
115 pub fn new(role: SyntaxRole, text: impl Into<String>) -> Self {
116 Self {
117 role,
118 text: text.into(),
119 }
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum CodeSource {
126 Plain(String),
128 Spans(Vec<SyntaxSpan>),
130}
131impl CodeSource {
132 #[must_use]
135 pub fn highlighted(source: &str, lang: &str) -> Self {
136 if !Self::supports(lang) {
137 return Self::Plain(source.to_owned());
138 }
139 Self::Spans(
140 fc_highlight::highlight(source, lang)
141 .into_iter()
142 .map(|span| SyntaxSpan::new(role(span.role), span.text))
143 .collect(),
144 )
145 }
146
147 #[must_use]
150 pub fn supports(lang: &str) -> bool {
151 fc_highlight::Language::resolve(lang).is_some()
152 }
153
154 fn text(&self) -> String {
155 match self {
156 Self::Plain(text) => text.clone(),
157 Self::Spans(spans) => spans.iter().map(|s| s.text.as_str()).collect(),
158 }
159 }
160}
161
162const fn role(role: fc_highlight::Role) -> SyntaxRole {
164 use fc_highlight::Role as R;
165 match role {
166 R::Fg => SyntaxRole::Fg,
167 R::Comment => SyntaxRole::Comment,
168 R::Keyword => SyntaxRole::Keyword,
169 R::String => SyntaxRole::String,
170 R::Number => SyntaxRole::Number,
171 R::Function => SyntaxRole::Function,
172 R::Type => SyntaxRole::Type,
173 R::Operator => SyntaxRole::Operator,
174 R::Punctuation => SyntaxRole::Punctuation,
175 R::Property => SyntaxRole::Property,
176 R::Constant => SyntaxRole::Constant,
177 R::Tag => SyntaxRole::Tag,
178 R::Attribute => SyntaxRole::Attribute,
179 R::Regexp => SyntaxRole::Regexp,
180 R::Heading => SyntaxRole::Heading,
181 R::Link => SyntaxRole::Link,
182 R::Invalid => SyntaxRole::Invalid,
183 }
184}
185
186#[derive(Debug, Default, Clone, PartialEq, Eq)]
188pub enum CodeLayout {
189 #[default]
191 Wrap,
192 Numbered,
194 Scroll(AccessibleName),
196}
197
198#[component]
200pub fn CodeBlock(
201 source: CodeSource,
202 #[props(default)]
204 lang: Option<String>,
205 #[props(default)] layout: CodeLayout,
206 #[props(default)]
208 unsupported: bool,
209 #[props(default)] class: Option<String>,
210 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
211) -> Element {
212 let shape = match layout {
213 CodeLayout::Wrap => Layout::Wrap,
214 CodeLayout::Numbered => Layout::Numbered,
215 CodeLayout::Scroll(_) => Layout::Scroll,
216 };
217 let mut attributes = component_attributes(
218 attributes,
219 ClassList::new(BEM.block())
220 .with_modifier(BEM, shape)
221 .with_modifier(
222 BEM,
223 if unsupported {
224 Support::Unsupported
225 } else {
226 Support::Highlighted
227 },
228 )
229 .extend(class.as_deref()),
230 &["dir", "role", "aria-label", "tabindex"],
231 );
232 if let Some(lang) = &lang {
233 attributes.push(lang_attribute(lang));
234 }
235 if let CodeLayout::Scroll(label) = &layout {
236 attributes.push(Attribute::new("role", "region", None, false));
237 attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
238 attributes.push(Attribute::new("tabindex", "0", None, false));
239 }
240 let lines = source.text().split('\n').count();
241 rsx! {
242 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_CODE_CSS } }
243 div { dir: "ltr", ..attributes,
245 if unsupported {
246 div { class: BEM.element("bar"), span { class: BEM.element("lang"), "{lang.clone().unwrap_or_default()}" } }
247 }
248 if layout == CodeLayout::Numbered {
249 div { class: BEM.element("gutter"), aria_hidden: "true",
250 for number in 1..=lines { span { key: "{number}", "{number}" } }
251 }
252 }
253 code { class: BEM.element("source"), tabindex: "0",
254 match &source {
255 CodeSource::Plain(text) => rsx! { "{text}" },
256 CodeSource::Spans(spans) => rsx! {
257 for (index, span) in spans.iter().enumerate() {
258 span { key: "{index}", class: TOK.modifier_raw(span.role.suffix()), "{span.text}" }
259 }
260 },
261 }
262 }
263 }
264 }
265}