use crate::{
core::{
assets,
attrs::{component_attributes, lang_attribute},
},
prelude::*,
theme::use_stylesheets,
};
const BEM: Bem = Bem::new("fc-code");
const TOK: Bem = Bem::new("fc-code__tok");
fc_modifier! { enum Layout for BEM { Wrap => None, Numbered => "numbered", Scroll => "scroll" } }
fc_modifier! { enum Support for BEM { Highlighted => None, Unsupported => "unsupported" } }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum SyntaxRole {
Fg,
Comment,
Keyword,
String,
Number,
Function,
Type,
Operator,
Punctuation,
Property,
Constant,
Tag,
Attribute,
Regexp,
Heading,
Link,
Invalid,
}
impl SyntaxRole {
pub const ALL: [Self; 17] = [
Self::Fg,
Self::Comment,
Self::Keyword,
Self::String,
Self::Number,
Self::Function,
Self::Type,
Self::Operator,
Self::Punctuation,
Self::Property,
Self::Constant,
Self::Tag,
Self::Attribute,
Self::Regexp,
Self::Heading,
Self::Link,
Self::Invalid,
];
const fn suffix(self) -> &'static str {
match self {
Self::Fg => "fg",
Self::Comment => "comment",
Self::Keyword => "keyword",
Self::String => "string",
Self::Number => "number",
Self::Function => "function",
Self::Type => "type",
Self::Operator => "operator",
Self::Punctuation => "punctuation",
Self::Property => "property",
Self::Constant => "constant",
Self::Tag => "tag",
Self::Attribute => "attribute",
Self::Regexp => "regexp",
Self::Heading => "heading",
Self::Link => "link",
Self::Invalid => "invalid",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyntaxSpan {
pub role: SyntaxRole,
pub text: String,
}
impl SyntaxSpan {
#[must_use]
pub fn new(role: SyntaxRole, text: impl Into<String>) -> Self {
Self {
role,
text: text.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CodeSource {
Plain(String),
Spans(Vec<SyntaxSpan>),
}
impl CodeSource {
#[must_use]
pub fn highlighted(source: &str, lang: &str) -> Self {
if !Self::supports(lang) {
return Self::Plain(source.to_owned());
}
Self::Spans(
fc_highlight::highlight(source, lang)
.into_iter()
.map(|span| SyntaxSpan::new(role(span.role), span.text))
.collect(),
)
}
#[must_use]
pub fn supports(lang: &str) -> bool {
fc_highlight::Language::resolve(lang).is_some()
}
fn text(&self) -> String {
match self {
Self::Plain(text) => text.clone(),
Self::Spans(spans) => spans.iter().map(|s| s.text.as_str()).collect(),
}
}
}
const fn role(role: fc_highlight::Role) -> SyntaxRole {
use fc_highlight::Role as R;
match role {
R::Fg => SyntaxRole::Fg,
R::Comment => SyntaxRole::Comment,
R::Keyword => SyntaxRole::Keyword,
R::String => SyntaxRole::String,
R::Number => SyntaxRole::Number,
R::Function => SyntaxRole::Function,
R::Type => SyntaxRole::Type,
R::Operator => SyntaxRole::Operator,
R::Punctuation => SyntaxRole::Punctuation,
R::Property => SyntaxRole::Property,
R::Constant => SyntaxRole::Constant,
R::Tag => SyntaxRole::Tag,
R::Attribute => SyntaxRole::Attribute,
R::Regexp => SyntaxRole::Regexp,
R::Heading => SyntaxRole::Heading,
R::Link => SyntaxRole::Link,
R::Invalid => SyntaxRole::Invalid,
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum CodeLayout {
#[default]
Wrap,
Numbered,
Scroll(AccessibleName),
}
#[component]
pub fn CodeBlock(
source: CodeSource,
#[props(default)]
lang: Option<String>,
#[props(default)] layout: CodeLayout,
#[props(default)]
unsupported: bool,
#[props(default)] class: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let shape = match layout {
CodeLayout::Wrap => Layout::Wrap,
CodeLayout::Numbered => Layout::Numbered,
CodeLayout::Scroll(_) => Layout::Scroll,
};
let mut attributes = component_attributes(
attributes,
ClassList::new(BEM.block())
.with_modifier(BEM, shape)
.with_modifier(
BEM,
if unsupported {
Support::Unsupported
} else {
Support::Highlighted
},
)
.extend(class.as_deref()),
&["dir", "role", "aria-label", "tabindex"],
);
if let Some(lang) = &lang {
attributes.push(lang_attribute(lang));
}
if let CodeLayout::Scroll(label) = &layout {
attributes.push(Attribute::new("role", "region", None, false));
attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
attributes.push(Attribute::new("tabindex", "0", None, false));
}
let lines = source.text().split('\n').count();
rsx! {
if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_CODE_CSS } }
div { dir: "ltr", ..attributes,
if unsupported {
div { class: BEM.element("bar"), span { class: BEM.element("lang"), "{lang.clone().unwrap_or_default()}" } }
}
if layout == CodeLayout::Numbered {
div { class: BEM.element("gutter"), aria_hidden: "true",
for number in 1..=lines { span { key: "{number}", "{number}" } }
}
}
code { class: BEM.element("source"), tabindex: "0",
match &source {
CodeSource::Plain(text) => rsx! { "{text}" },
CodeSource::Spans(spans) => rsx! {
for (index, span) in spans.iter().enumerate() {
span { key: "{index}", class: TOK.modifier_raw(span.role.suffix()), "{span.text}" }
}
},
}
}
}
}
}