1#![cfg_attr(feature = "better-internal-errors", feature(proc_macro_diagnostic))]
2#![cfg_attr(feature = "external-template-spans", feature(proc_macro_expand))]
3#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
4#![doc(issue_tracker_base_url = "https://github.com/0b10011/Oxiplate/issues/")]
5#![doc(test(no_crate_inject))]
6#![doc(test(attr(deny(warnings))))]
7#![doc = include_str!("../README.md")]
8
9mod config;
10mod parser;
11mod source;
12mod state;
13mod template;
14mod tokenizer;
15
16use std::collections::{HashMap, VecDeque};
17#[cfg(not(feature = "external-template-spans"))]
18use std::fs;
19use std::io;
20use std::path::{Path, PathBuf};
21
22use proc_macro::TokenStream;
23use proc_macro2::Span;
24use quote::{quote, quote_spanned};
25use syn::parse::Parse;
26use syn::spanned::Spanned;
27use syn::token::Colon;
28use syn::{
29 Attribute, Data, DeriveInput, Expr, ExprLit, Ident, Lit, LitStr, MetaList, MetaNameValue,
30};
31
32use crate::config::OptimizedRenderer;
33pub(crate) use crate::source::Source;
34use crate::source::SourceOwned;
35pub(crate) use crate::state::State;
36use crate::state::{LocalVariables, build_config};
37use crate::template::{TokenSlice, parse, tokens_and_eof};
38
39type BuiltTokens = (proc_macro2::TokenStream, usize);
40
41#[proc_macro_derive(
100 Oxiplate,
101 attributes(oxiplate, oxiplate_inline, oxiplate_extends, oxiplate_include)
102)]
103pub fn oxiplate(input: TokenStream) -> TokenStream {
104 #[cfg(feature = "_unreachable")]
105 let input = {
106 if input.to_string()
108 == r#"#[oxiplate_inline("hello world")] struct UnreachableUnparseableInput;"#
109 .to_string()
110 {
111 quote! { struct 19foo; }.into()
113 } else {
114 input
115 }
116 };
117
118 oxiplate_internal(input, &VecDeque::from([&HashMap::new()])).0
119}
120
121pub(crate) fn oxiplate_internal(
123 input: TokenStream,
124 blocks: &VecDeque<&HashMap<&str, (BuiltTokens, Option<BuiltTokens>)>>,
125) -> (TokenStream, usize) {
126 let input = match syn::parse(input) {
127 Ok(input) => input,
128 Err(err) => return (err.to_compile_error().into(), 0),
129 };
130 parse_input(&input, blocks)
131}
132
133fn parse_input(
137 input: &DeriveInput,
138 blocks: &VecDeque<&HashMap<&str, (BuiltTokens, Option<BuiltTokens>)>>,
139) -> (TokenStream, usize) {
140 let DeriveInput {
141 ident, generics, ..
142 } = &input;
143
144 let (template, estimated_length, template_type, optimized_renderer): (
145 proc_macro2::TokenStream,
146 usize,
147 TemplateType,
148 OptimizedRenderer,
149 ) = match parse_template_and_data(input, blocks) {
150 Ok(data) => data,
151 Err((err, template_type, optimized_renderer)) => (
152 err.to_compile_error(),
153 0,
154 template_type.unwrap_or(TemplateType::Inline),
155 optimized_renderer,
156 ),
157 };
158
159 if let TemplateType::Extends | TemplateType::Include = template_type {
161 return (template.into(), estimated_length);
162 }
163
164 let where_clause = &generics.where_clause;
165 let expanded = if *optimized_renderer {
166 #[cfg(not(feature = "_oxiplate"))]
167 quote! {
168 compile_error!(
169 "`optimized_renderer` config option specified in `/oxiplate.toml` is only available when using `oxiplate`. It looks like `oxiplate-derive` is being used directly instead."
170 );
171 impl #generics ::core::fmt::Display for #ident #generics #where_clause {
172 fn fmt(&self, oxiplate_formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
173 let string = {
174 extern crate alloc;
175
176 use ::core::fmt::Write as _;
177 let mut string = alloc::string::String::with_capacity(#estimated_length);
178 let oxiplate_formatter = &mut string;
179 #template
180 string
181 };
182 oxiplate_formatter.write_str(&string)
183 }
184 }
185 }
186
187 #[cfg(feature = "_oxiplate")]
188 quote! {
189 impl #generics ::core::fmt::Display for #ident #generics #where_clause {
190 fn fmt(&self, oxiplate_formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
191 ::oxiplate::Render::render_into(self, oxiplate_formatter)
192 }
193 }
194 impl #generics ::oxiplate::Render for #ident #generics #where_clause {
195 const ESTIMATED_LENGTH: usize = #estimated_length;
196
197 #[inline]
198 fn render_into<W: ::core::fmt::Write>(&self, oxiplate_formatter: &mut W) -> ::core::fmt::Result {
199 extern crate alloc;
200
201 use ::core::fmt::Write as _;
202 use ::oxiplate::{ToCowStr as _, UnescapedText as _};
203 #template
204 Ok(())
205 }
206 }
207 }
208 } else {
209 quote! {
210 impl #generics ::core::fmt::Display for #ident #generics #where_clause {
211 fn fmt(&self, oxiplate_formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
212 let string = {
213 extern crate alloc;
214
215 use ::core::fmt::Write as _;
216 let mut string = alloc::string::String::with_capacity(#estimated_length);
217 let oxiplate_formatter = &mut string;
218 #template
219 string
220 };
221 oxiplate_formatter.write_str(&string)
222 }
223 }
224 }
225 };
226
227 (TokenStream::from(expanded), estimated_length)
228}
229
230type ParsedTemplate = (
231 proc_macro2::TokenStream,
232 usize,
233 TemplateType,
234 OptimizedRenderer,
235);
236
237fn parse_template_and_data(
238 input: &DeriveInput,
239 blocks: &VecDeque<&HashMap<&str, (BuiltTokens, Option<BuiltTokens>)>>,
240) -> Result<ParsedTemplate, (syn::Error, Option<TemplateType>, OptimizedRenderer)> {
241 let config =
243 build_config(input).map_err(|(err, optimized_renderer)| (err, None, optimized_renderer))?;
244
245 let DeriveInput {
246 attrs, ident, data, ..
247 } = &input;
248
249 match data {
251 Data::Struct(_struct_item) => (),
252 _ => {
253 return Err((
254 syn::Error::new(input.span(), "Expected a struct"),
255 None,
256 config.optimized_renderer,
257 ));
258 }
259 }
260
261 let (attr, template_type) = parse_template_type(attrs, ident.span())
262 .map_err(|err: syn::Error| (err, None, config.optimized_renderer.clone()))?;
263
264 let optimized_renderer = config.optimized_renderer.clone();
265
266 let mut state = State {
267 local_variables: LocalVariables::new(),
268 inferred_escaper_group: None,
269 default_escaper_group: None,
270 failed_to_set_default_escaper_group: false,
271 config,
272 blocks,
273 has_content: false,
274 };
275
276 let parsed_tokens = parse_source_tokens(attr, &template_type, &mut state);
277 let (template, estimated_length): BuiltTokens = process_parsed_tokens(
278 parsed_tokens,
279 &mut state,
280 #[cfg(any(feature = "_oxiplate", feature = "external-template-spans"))]
281 &template_type,
282 )
283 .map_err(|err: syn::Error| (err, Some(template_type.clone()), optimized_renderer.clone()))?;
284
285 Ok((
286 template,
287 estimated_length,
288 template_type,
289 optimized_renderer,
290 ))
291}
292
293type ParsedTokens = Result<
294 (
295 Span,
296 proc_macro2::TokenStream,
297 Option<PathBuf>,
298 Option<String>,
299 ),
300 ParsedEscaperError,
301>;
302
303fn process_parsed_tokens<'a>(
304 parsed_tokens: ParsedTokens,
305 state: &'a mut State<'a>,
306 #[cfg(any(feature = "_oxiplate", feature = "external-template-spans"))]
307 template_type: &TemplateType,
308) -> Result<BuiltTokens, syn::Error> {
309 match parsed_tokens {
310 #[cfg(feature = "_oxiplate")]
311 Err(ParsedEscaperError::EscaperNotFound((escaper, span))) => {
312 let mut available_escaper_groups = state
313 .config
314 .escaper_groups
315 .keys()
316 .map(|key| &**key)
317 .collect::<Vec<&str>>();
318 available_escaper_groups.sort_unstable();
319 let available_escaper_groups = available_escaper_groups.join(", ");
320 let template = match template_type {
321 TemplateType::Path | TemplateType::Extends | TemplateType::Include => {
322 internal_error!(
323 span.unwrap(),
324 "Unregistered file extension causing `EscaperNotFound` error",
325 .help(format!("Extension found: {escaper}"))
326 .help(format!("Registered escaper groups: {available_escaper_groups}"))
327 );
328 }
329 TemplateType::Inline => {
330 let available_escaper_groups = LitStr::new(&available_escaper_groups, span);
331 quote_spanned! {span=> compile_error!(concat!("The specified escaper group `", #escaper, "` is not registered in `/oxiplate.toml`. Registered escaper groups: ", #available_escaper_groups)); }
332 }
333 };
334 Ok((template, 0))
335 }
336 Err(ParsedEscaperError::ParseError(compile_error)) => Ok((compile_error, 0)),
337 Ok((span, input, origin, inferred_escaper_group_name)) => {
338 let code = parse_code_literal(
339 &input.into(),
340 #[cfg(feature = "external-template-spans")]
341 template_type,
342 #[cfg(feature = "external-template-spans")]
343 span,
344 )?;
345
346 if let Some(inferred_escaper_group_name) = &inferred_escaper_group_name {
347 state.inferred_escaper_group = Some((
348 inferred_escaper_group_name.to_owned(),
349 state
350 .config
351 .escaper_groups
352 .get(inferred_escaper_group_name)
353 .expect("Escaper group should have already been checked for existence")
354 .clone(),
355 ));
356 }
357
358 let owned_source = SourceOwned::new(&code, span, origin);
360 let source = Source::new(&owned_source);
361 let (tokens, eof) = tokens_and_eof(source);
362 let tokens = TokenSlice::new(&tokens, &eof);
363
364 Ok(parse(state, tokens))
367 }
368 }
369}
370
371#[derive(Clone)]
372enum TemplateType {
373 Path,
374 Inline,
375 Extends,
376 Include,
377}
378
379fn parse_template_type(
381 attrs: &Vec<Attribute>,
382 span: Span,
383) -> Result<(&Attribute, TemplateType), syn::Error> {
384 for attr in attrs {
385 let path = attr.path();
386 let template_type = if path.is_ident("oxiplate_inline") {
387 TemplateType::Inline
388 } else if path.is_ident("oxiplate_extends") {
389 TemplateType::Extends
390 } else if path.is_ident("oxiplate_include") {
391 TemplateType::Include
392 } else if path.is_ident("oxiplate") {
393 TemplateType::Path
394 } else {
395 continue;
396 };
397
398 return Ok((attr, template_type));
399 }
400
401 Err(syn::Error::new(
402 span,
403 r#"Expected an attribute named `oxiplate_inline` or `oxiplate` to specify the template:
404External: #[oxiplate = "path/to/template/from/templates/directory.html.oxip"]
405Internal: #[oxiplate_inline(html: "{{ your_var }}")]"#,
406 ))
407}
408
409fn parse_code_literal(
410 input: &TokenStream,
411 #[cfg(feature = "external-template-spans")] template_type: &TemplateType,
412 #[cfg(feature = "external-template-spans")] span: Span,
413) -> Result<LitStr, syn::Error> {
414 #[cfg(feature = "external-template-spans")]
415 let input = {
416 let invalid_attribute_message = match template_type {
417 TemplateType::Path | TemplateType::Inline => {
418 r#"Must provide either an external or internal template:
419External: #[oxiplate = "path/to/template/from/templates/directory.html.oxip"]
420Internal: #[oxiplate_inline(html: "{{ your_var }}")]"#
421 }
422 TemplateType::Extends => {
423 r#"Must provide a path to a template that exists. E.g., `{% extends "path/to/template.html.oxip" %}`"#
424 }
425 TemplateType::Include => {
426 r#"Must provide a path to a template that exists. E.g., `{% include "path/to/template.html.oxip" %}`"#
427 }
428 };
429
430 let input = input.expand_expr();
432 if input.is_err() {
433 return Err(syn::Error::new(span, invalid_attribute_message));
434 }
435 input.unwrap()
436 };
437
438 #[cfg(not(feature = "external-template-spans"))]
439 let input = input.clone();
440
441 let parser = |input: syn::parse::ParseStream| input.parse::<LitStr>();
443 let code = syn::parse::Parser::parse(parser, input)?;
444 Ok(code)
445}
446
447fn parse_source_tokens(
448 attr: &Attribute,
449 template_type: &TemplateType,
450 #[cfg_attr(not(feature = "_oxiplate"), allow(unused_variables))] state: &mut State,
451) -> ParsedTokens {
452 match template_type {
453 TemplateType::Inline => parse_source_tokens_for_inline(attr, state),
454 TemplateType::Path | TemplateType::Extends | TemplateType::Include => {
455 parse_source_tokens_for_path(attr, state)
456 }
457 }
458}
459
460enum Template {
462 WithEscaper(TemplateWithEscaper),
463 WithoutEscaper(TemplateWithoutEscaper),
464}
465
466impl Parse for Template {
467 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
468 let lookahead = input.lookahead1();
469 if lookahead.peek(Ident) {
470 input.parse().map(Template::WithEscaper)
471 } else {
472 input.parse().map(Template::WithoutEscaper)
473 }
474 }
475}
476
477struct TemplateWithEscaper {
479 #[cfg_attr(not(feature = "_oxiplate"), allow(dead_code))]
480 escaper: Ident,
481 #[allow(dead_code)]
482 colon: Colon,
483 #[cfg_attr(not(feature = "_oxiplate"), allow(dead_code))]
484 template: Expr,
485}
486
487impl Parse for TemplateWithEscaper {
488 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
489 Ok(TemplateWithEscaper {
490 escaper: input.parse()?,
491 colon: input.parse()?,
492 template: input.parse()?,
493 })
494 }
495}
496
497struct TemplateWithoutEscaper {
499 template: Expr,
500}
501
502impl Parse for TemplateWithoutEscaper {
503 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
504 Ok(TemplateWithoutEscaper {
505 template: input.parse()?,
506 })
507 }
508}
509
510#[cfg_attr(not(feature = "external-template-spans"), derive(Debug))]
511enum ParsedEscaperError {
512 #[cfg(feature = "_oxiplate")]
513 EscaperNotFound((String, Span)),
514 ParseError(proc_macro2::TokenStream),
515}
516
517#[cfg_attr(not(feature = "_oxiplate"), allow(clippy::unnecessary_wraps))]
518fn parse_source_tokens_for_inline(
519 attr: &Attribute,
520 #[cfg_attr(not(feature = "_oxiplate"), allow(unused_variables))] state: &mut State,
521) -> ParsedTokens {
522 match &attr.meta {
523 syn::Meta::Path(path) => {
524 let span = path.span();
525 Err(ParsedEscaperError::ParseError(quote_spanned! {span=>
526 compile_error!(r#"Must provide either an external or internal template:
527External: #[oxiplate = "/path/to/template/from/templates/directory.txt.oxip"]
528Internal: #[oxiplate_inline(html: "{{ your_var }}")]"#);
529 }))
530 }
531 syn::Meta::List(MetaList {
532 path: _,
533 delimiter: _,
534 tokens,
535 }) => match syn::parse2::<Template>(tokens.clone()) {
536 #[cfg(not(feature = "_oxiplate"))]
537 Ok(Template::WithEscaper(template)) => {
538 let span = template.escaper.span();
539 Err(ParsedEscaperError::ParseError(quote_spanned! {span=>
540 compile_error!("Escaping requires the `oxiplate` library, but you appear to be using \
541 `oxiplate-derive` directly. Replacing `oxiplate-derive` with `oxiplate` in the \
542 dependencies should fix this issue, although you may need to turn off some \
543 default features if you want it to work the same way.");
544 }))
545 }
546 #[cfg(feature = "_oxiplate")]
547 Ok(Template::WithEscaper(TemplateWithEscaper {
548 escaper,
549 colon: _,
550 template,
551 })) => {
552 let span = template.span();
553
554 let escaper_name = escaper.to_string();
555 if !state.config.escaper_groups.contains_key(&escaper_name) {
556 return Err(ParsedEscaperError::EscaperNotFound((
557 escaper_name,
558 escaper.span(),
559 )));
560 }
561
562 Ok((
563 span,
564 quote::quote_spanned!(span=> #template),
565 None,
566 Some(escaper_name),
567 ))
568 }
569 Ok(Template::WithoutEscaper(TemplateWithoutEscaper { template })) => {
570 let span = template.span();
571 Ok((span, quote::quote_spanned!(span=> #template), None, None))
572 }
573 Err(error) => {
574 let span = error.span();
575 let compile_error = error.to_compile_error();
576 Err(ParsedEscaperError::ParseError(quote_spanned! {span=>
577 compile_error!("Failed to parse inline template. Should look something like:\n#[oxiplate_inline(html: \"{{ your_var }}\")]");
578 #compile_error
579 }))
580 }
581 },
582 syn::Meta::NameValue(meta) => {
583 let span = meta.span();
584 Err(ParsedEscaperError::ParseError(quote_spanned! {span=>
585 compile_error!("Incorrect syntax for inline template. Should look something like:\n#[oxiplate_inline(html: \"{{ your_var }}\")]");
586 }))
587 }
588 }
589}
590
591fn templates_dir(span: Span) -> Result<PathBuf, ParsedEscaperError> {
594 let default_template_dir = String::from("templates");
595
596 let (specified_templates_dir, using_default_template_dir) =
597 if let Ok(templates_dir) = ::std::env::var("OXIP_TEMPLATE_DIR") {
598 (templates_dir, false)
599 } else {
600 (default_template_dir, true)
601 };
602 let root = PathBuf::from(
603 ::std::env::var("CARGO_MANIFEST_DIR_OVERRIDE")
604 .or(::std::env::var("CARGO_MANIFEST_DIR"))
605 .expect("`CARGO_MANIFEST_DIR` should be present"),
606 );
607
608 root
610 .append_path(&specified_templates_dir, false)
611 .map_err(|err| -> ParsedEscaperError {
612 match err {
613 AppendPathError::DoesNotExist(path_buf) => {
614 let path_buf = path_buf.to_string_lossy();
615 ParsedEscaperError::ParseError(quote_spanned! {span=>
616 compile_error!(concat!("Template directory `", #path_buf, "` not found."));
617 })
618 },
619 AppendPathError::IsSymlink(path_buf) => {
620 let path_buf = path_buf.to_string_lossy();
621 ParsedEscaperError::ParseError(quote_spanned! {span=>
622 compile_error!(concat!("Template directory `", #path_buf, "` cannot be a symlink."));
623 })
624 },
625 AppendPathError::CanonicalizeError(path_buf, error) => {
626 if using_default_template_dir {
627 unreachable!(
628 "Failed to normalize default template directory. Original error: {error}",
629 );
630 } else {
631 let path_buf = path_buf.to_string_lossy();
632 let error = error.to_string();
633 ParsedEscaperError::ParseError(quote_spanned! {span=>
634 compile_error!(concat!("Failed to normalize `", #path_buf, "`. Original error: ", #error));
635 })
636 }
637 },
638 AppendPathError::PrefixNotPresent { prefix, final_path } => {
639 if using_default_template_dir {
640 let _ = prefix;
641 let _ = final_path;
642 unreachable!(
643 "`default_template_dir` variable in `oxiplate-derive` code must be a relative \
644 path; example: 'templates' instead of '/templates'. Provided: {specified_templates_dir}",
645 );
646 } else {
647 let prefix = prefix.to_string_lossy();
648 let final_path = final_path.to_string_lossy();
649 ParsedEscaperError::ParseError(quote_spanned! {span=>
650 compile_error!(concat!(
651 "`OXIP_TEMPLATE_DIR` environment variable must be a relative path that resolves under `",
652 #prefix,
653 "`; example: 'templates' instead of '/templates'. Provided: ",
654 #final_path
655 ));
656 })
657 }
658 },
659 AppendPathError::NotDirectory(path_buf) => {
660 let path_buf = path_buf.to_string_lossy();
661 ParsedEscaperError::ParseError(quote_spanned! {span=>
662 compile_error!(concat!("Template directory `", #path_buf, "` was not a directory."));
663 })
664 },
665 AppendPathError::NotFile(_path_buf) => unreachable!("Directory is expected, not a file"),
666 }
667 })
668}
669
670fn template_path(path: &LitStr, attr_span: Span) -> Result<PathBuf, ParsedEscaperError> {
672 let templates_dir = templates_dir(attr_span)?;
673
674 let span = path.span();
676
677 templates_dir
678 .append_path(path.value(), true)
679 .map_err(|err| -> ParsedEscaperError {
680 match err {
681 AppendPathError::DoesNotExist(path_buf) => {
682 let path_buf = path_buf.to_string_lossy();
683 ParsedEscaperError::ParseError(quote_spanned! {span=>
684 compile_error!(concat!("Path does not exist: `", #path_buf, "`"));
685 })
686 },
687 AppendPathError::IsSymlink(path_buf) => {
688 let path_buf = path_buf.to_string_lossy();
689 ParsedEscaperError::ParseError(quote_spanned! {span=>
690 compile_error!(concat!("Symlinks are not allowed for template paths: `", #path_buf, "`"));
691 })
692 },
693 AppendPathError::CanonicalizeError(path_buf, error) => {
694 let path_buf = path_buf.to_string_lossy();
695 let error = error.to_string();
696 ParsedEscaperError::ParseError(quote_spanned! {span=>
697 compile_error!(concat!("Failed to canonicalize path: `", #path_buf, "`. Original error: ", #error));
698 })
699 },
700 AppendPathError::PrefixNotPresent { prefix, final_path } => {
701 let prefix = prefix.to_string_lossy();
702 let final_path = final_path.to_string_lossy();
703 ParsedEscaperError::ParseError(quote_spanned! {span=>
704 compile_error!(concat!("Template path `", #final_path, "` not within template directory `", #prefix, "`"));
705 })
706 },
707 AppendPathError::NotDirectory(_path_buf) => unreachable!("File is expected, not a directory"),
708 AppendPathError::NotFile(path_buf) => {
709 let path_buf = path_buf.to_string_lossy();
710 ParsedEscaperError::ParseError(quote_spanned! {span=>
711 compile_error!(concat!("Path is not a file: `", #path_buf, "`"));
712 })
713 },
714 }
715 })
716}
717
718fn parse_source_tokens_for_path(
719 attr: &Attribute,
720 #[cfg_attr(not(feature = "_oxiplate"), allow(unused_variables))] state: &mut State,
721) -> ParsedTokens {
722 let syn::Meta::NameValue(MetaNameValue {
723 path: _,
724 eq_token: _,
725 value: Expr::Lit(ExprLit {
726 attrs: _,
727 lit: Lit::Str(path),
728 }),
729 }) = &attr.meta
730 else {
731 let span = attr.span();
732 return Err(ParsedEscaperError::ParseError(quote_spanned! {span=>
733 compile_error!("Incorrect syntax for external template. Should look something like:\n#[oxiplate = \"/path/to/template/from/templates/directory.txt.oxip\"]");
734 }));
735 };
736
737 let full_path = template_path(path, attr.span())?;
738
739 let span = path.span();
740
741 #[cfg(feature = "_oxiplate")]
742 let mut escaper_name: Option<String> = None;
743
744 #[cfg(feature = "_oxiplate")]
747 if *state.config.infer_escaper_group_from_file_extension {
748 let path_value = full_path.to_string_lossy();
751 let mut extensions = path_value.split('.');
752 let mut extension = extensions.next_back();
753 if extension == Some("oxip") {
754 extension = extensions.next_back();
755 }
756
757 if extension == Some("raw") {
759 extension = None;
760 }
761
762 if let Some(extension) = extension {
764 if state.config.escaper_groups.contains_key(extension) {
765 escaper_name = Some(extension.to_owned());
766 } else {
767 #[cfg(feature = "_unreachable")]
770 return Err(ParsedEscaperError::EscaperNotFound((
771 extension.to_string(),
772 span,
773 )));
774 }
775 }
776 }
777
778 #[cfg(feature = "external-template-spans")]
779 let tokens = {
780 let path = syn::LitStr::new(&full_path.to_string_lossy(), span);
781 quote::quote_spanned!(span=> include_str!(#path))
782 };
783
784 #[cfg(not(feature = "external-template-spans"))]
785 let tokens = {
786 let template_string = fs::read_to_string(&full_path)
787 .expect("Template has already been checked to exist; perhaps a permissions issue?");
788
789 quote_spanned! {span=> #template_string }
790 };
791
792 #[cfg(feature = "_oxiplate")]
793 return Ok((span, tokens, Some(full_path), escaper_name));
794
795 #[cfg(not(feature = "_oxiplate"))]
796 Ok((span, tokens, Some(full_path), None))
797}
798
799enum AppendPathError {
801 DoesNotExist(PathBuf),
803 IsSymlink(PathBuf),
805 CanonicalizeError(PathBuf, io::Error),
808 PrefixNotPresent {
811 prefix: PathBuf,
812 final_path: PathBuf,
813 },
814 NotDirectory(PathBuf),
816 NotFile(PathBuf),
818}
819
820trait AppendPath<P: AsRef<Path>> {
822 fn append_path(&self, suffix: P, expecting_file: bool) -> Result<Self, AppendPathError>
825 where
826 Self: Sized;
827}
828impl<P: AsRef<Path>> AppendPath<P> for PathBuf {
829 fn append_path(&self, suffix: P, expecting_file: bool) -> Result<Self, AppendPathError> {
830 let new_path = self.join(suffix);
832
833 if !new_path.starts_with(self) {
836 return Err(AppendPathError::PrefixNotPresent {
837 prefix: self.clone(),
838 final_path: new_path,
839 });
840 } else if !new_path.exists() {
841 return Err(AppendPathError::DoesNotExist(new_path));
842 } else if new_path.is_symlink() {
843 return Err(AppendPathError::IsSymlink(new_path));
844 }
845
846 let new_path = new_path
848 .canonicalize()
849 .map_err(|error| AppendPathError::CanonicalizeError(new_path, error))?;
850
851 if !new_path.starts_with(self) {
854 return Err(AppendPathError::PrefixNotPresent {
855 prefix: self.clone(),
856 final_path: new_path,
857 });
858 } else if !expecting_file && !new_path.is_dir() {
859 return Err(AppendPathError::NotDirectory(new_path));
860 } else if expecting_file && !new_path.is_file() {
861 return Err(AppendPathError::NotFile(new_path));
862 }
863
864 Ok(new_path)
865 }
866}
867
868macro_rules! internal_error {
869 ($span:expr, $message:expr) => {{
870 internal_error!($span, $message, );
871 }};
872 ($span:expr, $message:expr, $($help:tt)*) => {{
873 let message = $message;
874
875 #[cfg(not(feature = "better-internal-errors"))]
876 unreachable!(
877 "Internal Oxiplate error. Enable `better-internal-errors` feature for an \
878 easier-to-debug error message. Error: {}",
879 message
880 );
881
882 #[cfg(feature = "better-internal-errors")]
883 {
884 let url = format!(
885 "https://github.com/0b10011/oxiplate/issues/new?title={}&labels=internal+error&body={}",
886 crate::encode_query_value(&message),
887 crate::encode_query_value(
888 r"Error:
889
890```text
891PASTE_FULL_ERROR_HERE
892```
893
894Template:
895
896```
897PASTE_TEMPLATE_HERE
898```"
899 ),
900 );
901
902 ::proc_macro::Diagnostic::spanned(
903 $span,
904 ::proc_macro::Level::Error,
905 format!("Internal Oxiplate error: {}", &message),
906 )
907 .help(format!("Please open an issue: {url}"))
908 $($help)*
909 .emit();
910
911 unreachable!("Internal Oxiplate error. See previous error for more information.");
912 }
913 }};
914}
915
916#[cfg(feature = "better-internal-errors")]
919fn encode_query_value(input: &str) -> String {
920 let mut output = String::with_capacity(input.len());
921
922 for byte in input.as_bytes() {
923 match byte {
924 b' ' => output.push('+'),
925 b'*' | b'-' | b'.' | b'0'..=b'9' | b'A'..=b'Z' | b'_' | b'a'..=b'z' => output.push_str(
926 ::std::str::from_utf8(&[*byte]).expect("Error messages should always be UTF8-safe"),
927 ),
928 _ => output.push_str(percent_encode_byte(*byte)),
929 }
930 }
931
932 output
933}
934
935#[inline]
938#[cfg(feature = "better-internal-errors")]
939fn percent_encode_byte(byte: u8) -> &'static str {
940 static ENC_TABLE: &[u8; 768] = b"\
941 %00%01%02%03%04%05%06%07%08%09%0A%0B%0C%0D%0E%0F\
942 %10%11%12%13%14%15%16%17%18%19%1A%1B%1C%1D%1E%1F\
943 %20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2D%2E%2F\
944 %30%31%32%33%34%35%36%37%38%39%3A%3B%3C%3D%3E%3F\
945 %40%41%42%43%44%45%46%47%48%49%4A%4B%4C%4D%4E%4F\
946 %50%51%52%53%54%55%56%57%58%59%5A%5B%5C%5D%5E%5F\
947 %60%61%62%63%64%65%66%67%68%69%6A%6B%6C%6D%6E%6F\
948 %70%71%72%73%74%75%76%77%78%79%7A%7B%7C%7D%7E%7F\
949 %80%81%82%83%84%85%86%87%88%89%8A%8B%8C%8D%8E%8F\
950 %90%91%92%93%94%95%96%97%98%99%9A%9B%9C%9D%9E%9F\
951 %A0%A1%A2%A3%A4%A5%A6%A7%A8%A9%AA%AB%AC%AD%AE%AF\
952 %B0%B1%B2%B3%B4%B5%B6%B7%B8%B9%BA%BB%BC%BD%BE%BF\
953 %C0%C1%C2%C3%C4%C5%C6%C7%C8%C9%CA%CB%CC%CD%CE%CF\
954 %D0%D1%D2%D3%D4%D5%D6%D7%D8%D9%DA%DB%DC%DD%DE%DF\
955 %E0%E1%E2%E3%E4%E5%E6%E7%E8%E9%EA%EB%EC%ED%EE%EF\
956 %F0%F1%F2%F3%F4%F5%F6%F7%F8%F9%FA%FB%FC%FD%FE%FF\
957 ";
958
959 let index = usize::from(byte) * 3;
960 unsafe { str::from_utf8_unchecked(&ENC_TABLE[index..index + 3]) }
963}
964
965pub(crate) use internal_error;