Skip to content

Commit a9ca67f

Browse files
committed
add a string version of the macro for inline string literals
1 parent fae61c3 commit a9ca67f

2 files changed

Lines changed: 132 additions & 31 deletions

File tree

dioxus-code-macro/src/lib.rs

Lines changed: 112 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ use syn::{Expr, LitStr, Token, parse_macro_input};
2222
/// [`CodeOptions::builder`] with [`CodeOptions::with_language`] to name the
2323
/// language explicitly; otherwise it is inferred from the file extension.
2424
///
25+
/// To highlight inline source instead of a file, use [`code_str!`].
26+
///
2527
/// [`CodeOptions::builder`]: https://docs.rs/dioxus-code/latest/dioxus_code/struct.CodeOptions.html#method.builder
2628
/// [`CodeOptions::with_language`]: https://docs.rs/dioxus-code/latest/dioxus_code/struct.CodeOptions.html#method.with_language
2729
#[proc_macro]
@@ -34,32 +36,74 @@ pub fn code(input: TokenStream) -> TokenStream {
3436
}
3537
}
3638

39+
/// Compile-time syntax highlighting of an inline source string.
40+
///
41+
/// Parses a string literal containing source code with [`arborium`] and
42+
/// expands to the resulting span tree. Pass the source as a string literal,
43+
/// `concat!(...)`, `include_str!(...)`, or `env!(...)`. The language must be
44+
/// supplied via [`CodeOptions::builder`] with [`CodeOptions::with_language`]
45+
/// since there is no file extension to infer from.
46+
///
47+
/// To highlight a file on disk instead, use [`code!`].
48+
///
49+
/// [`CodeOptions::builder`]: https://docs.rs/dioxus-code/latest/dioxus_code/struct.CodeOptions.html#method.builder
50+
/// [`CodeOptions::with_language`]: https://docs.rs/dioxus-code/latest/dioxus_code/struct.CodeOptions.html#method.with_language
51+
#[proc_macro]
52+
pub fn code_str(input: TokenStream) -> TokenStream {
53+
let input = parse_macro_input!(input as CodeStrInput);
54+
55+
match expand_code_str(input) {
56+
Ok(tokens) => tokens.into(),
57+
Err(error) => error.to_compile_error().into(),
58+
}
59+
}
60+
3761
struct CodeInput {
3862
path: String,
3963
options: Option<Expr>,
4064
}
4165

4266
impl Parse for CodeInput {
4367
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
44-
let MacroString(path) = input.parse()?;
45-
let mut options = None;
68+
let (path, options) = parse_string_and_options(input, "code macro")?;
69+
Ok(Self { path, options })
70+
}
71+
}
72+
73+
struct CodeStrInput {
74+
source: String,
75+
options: Option<Expr>,
76+
}
77+
78+
impl Parse for CodeStrInput {
79+
fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
80+
let (source, options) = parse_string_and_options(input, "code_str macro")?;
81+
Ok(Self { source, options })
82+
}
83+
}
4684

47-
if input.peek(Token![,]) {
48-
input.parse::<Token![,]>()?;
85+
fn parse_string_and_options(
86+
input: ParseStream<'_>,
87+
macro_label: &str,
88+
) -> syn::Result<(String, Option<Expr>)> {
89+
let MacroString(value) = input.parse()?;
90+
let mut options = None;
91+
92+
if input.peek(Token![,]) {
93+
input.parse::<Token![,]>()?;
94+
if !input.is_empty() {
95+
let expr: Expr = input.parse()?;
96+
if input.peek(Token![,]) {
97+
input.parse::<Token![,]>()?;
98+
}
4999
if !input.is_empty() {
50-
let expr: Expr = input.parse()?;
51-
if input.peek(Token![,]) {
52-
input.parse::<Token![,]>()?;
53-
}
54-
if !input.is_empty() {
55-
return Err(input.error("unexpected tokens after code macro options"));
56-
}
57-
options = Some(expr);
100+
return Err(input.error(format!("unexpected tokens after {macro_label} options")));
58101
}
102+
options = Some(expr);
59103
}
60-
61-
Ok(Self { path, options })
62104
}
105+
106+
Ok((value, options))
63107
}
64108

65109
fn try_extract_language(expr: &Expr) -> Option<String> {
@@ -238,13 +282,7 @@ fn expand_code(input: CodeInput) -> syn::Result<TokenStream2> {
238282
let absolute_path = resolve_manifest_path(&manifest_dir, &macro_path);
239283
let crate_path = dioxus_code_crate_path()?;
240284

241-
let options_check = input.options.as_ref().map(|expr| {
242-
quote_spanned! { expr.span() =>
243-
const _: fn() = || {
244-
let _: #crate_path::CodeOptions = #expr;
245-
};
246-
}
247-
});
285+
let options_check = options_check_tokens(&crate_path, input.options.as_ref());
248286

249287
let source = fs::read_to_string(&absolute_path).map_err(|error| {
250288
syn::Error::new(
@@ -268,21 +306,68 @@ fn expand_code(input: CodeInput) -> syn::Result<TokenStream2> {
268306
}});
269307
};
270308

309+
let absolute_lit = LitStr::new(&absolute_path.to_string_lossy(), Span::call_site());
310+
let source_decl = quote! { const SOURCE: &str = include_str!(#absolute_lit); };
311+
312+
expand_highlighted_source(&crate_path, options_check, source_decl, &language, &source)
313+
}
314+
315+
fn expand_code_str(input: CodeStrInput) -> syn::Result<TokenStream2> {
316+
let crate_path = dioxus_code_crate_path()?;
317+
let options_check = options_check_tokens(&crate_path, input.options.as_ref());
318+
319+
let Some(language) = input.options.as_ref().and_then(try_extract_language) else {
320+
let message =
321+
"could not determine language for `code_str!`; pass `CodeOptions::builder().with_language(Language::Rust)`";
322+
return Ok(quote! {{
323+
#options_check
324+
compile_error!(#message);
325+
}});
326+
};
327+
328+
let source_lit = LitStr::new(&input.source, Span::call_site());
329+
let source_decl = quote! { const SOURCE: &str = #source_lit; };
330+
331+
expand_highlighted_source(
332+
&crate_path,
333+
options_check,
334+
source_decl,
335+
&language,
336+
&input.source,
337+
)
338+
}
339+
340+
fn options_check_tokens(crate_path: &TokenStream2, options: Option<&Expr>) -> Option<TokenStream2> {
341+
options.map(|expr| {
342+
quote_spanned! { expr.span() =>
343+
const _: fn() = || {
344+
let _: #crate_path::CodeOptions = #expr;
345+
};
346+
}
347+
})
348+
}
349+
350+
fn expand_highlighted_source(
351+
crate_path: &TokenStream2,
352+
options_check: Option<TokenStream2>,
353+
source_decl: TokenStream2,
354+
language: &str,
355+
source: &str,
356+
) -> syn::Result<TokenStream2> {
271357
let mut highlighter = arborium::Highlighter::new();
272358
let spans = highlighter
273-
.highlight_spans(&language, &source)
359+
.highlight_spans(language, source)
274360
.map_err(|error| syn::Error::new(Span::call_site(), error.to_string()))?;
275361

276-
let Some(variant) = language_variant_for_slug(&language) else {
362+
let Some(variant) = language_variant_for_slug(language) else {
277363
let message = format!("language `{language}` has no `Language` variant");
278364
return Ok(quote! {{
279365
#options_check
280366
compile_error!(#message);
281367
}});
282368
};
283369
let variant_ident = Ident::new(variant, Span::call_site());
284-
let absolute_lit = LitStr::new(&absolute_path.to_string_lossy(), Span::call_site());
285-
let spans = normalize_spans(spans).into_iter().map(|span| {
370+
let span_tokens = normalize_spans(spans).into_iter().map(|span| {
286371
let start = span.start;
287372
let end = span.end;
288373
let tag = LitStr::new(span.tag, Span::call_site());
@@ -294,8 +379,8 @@ fn expand_code(input: CodeInput) -> syn::Result<TokenStream2> {
294379

295380
Ok(quote! {{
296381
#options_check
297-
const SOURCE: &str = include_str!(#absolute_lit);
298-
static SPANS: &[#crate_path::advanced::HighlightSpan] = &[#(#spans),*];
382+
#source_decl
383+
static SPANS: &[#crate_path::advanced::HighlightSpan] = &[#(#span_tokens),*];
299384
#crate_path::advanced::HighlightedSource::from_static_parts(
300385
SOURCE,
301386
#crate_path::Language::#variant_ident,

src/lib.rs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ const CODE_CSS: Asset = asset!("/assets/dioxus-code.css");
1515

1616
#[cfg(feature = "macro")]
1717
#[cfg_attr(docsrs, doc(cfg(feature = "macro")))]
18-
pub use dioxus_code_macro::code;
18+
pub use dioxus_code_macro::{code, code_str};
1919

20-
/// Compile-time options for the [`code!`] macro.
20+
/// Compile-time options for the [`code!`] and [`code_str!`] macros.
2121
///
22-
/// The [`code!`] macro reads this builder syntactically; pass
22+
/// Both macros read this builder syntactically; pass
2323
/// [`CodeOptions::builder`] with [`CodeOptions::with_language`] to override the
24-
/// language that would otherwise be inferred from the file extension.
24+
/// language that would otherwise be inferred from the file extension. For
25+
/// [`code_str!`] the language is required since there is no extension to
26+
/// infer from.
2527
///
2628
/// ```rust
2729
/// use dioxus_code::{CodeOptions, Language, code};
@@ -474,4 +476,18 @@ mod tests {
474476
span.tag() == "k" && &tree.source()[span.start() as usize..span.end() as usize] == "fn"
475477
}));
476478
}
479+
480+
#[cfg(feature = "macro")]
481+
#[test]
482+
fn code_str_macro_highlights_inline_source() {
483+
const TREE: advanced::HighlightedSource = code_str!(
484+
"fn main() {}",
485+
CodeOptions::builder().with_language(Language::Rust)
486+
);
487+
assert_eq!(TREE.language(), Language::Rust);
488+
assert_eq!(TREE.source(), "fn main() {}");
489+
assert!(TREE.spans().iter().any(|span| {
490+
span.tag() == "k" && &TREE.source()[span.start() as usize..span.end() as usize] == "fn"
491+
}));
492+
}
477493
}

0 commit comments

Comments
 (0)