1use std::collections::{HashMap, HashSet};
88
89#[cfg(test)]
90mod test;
91
92const DEFAULT_TAG_MARKERS: (&str, &str) = ("{%", "%}");
93
94pub fn split_path(path: &str) -> (&str, &str) {
109 if let Some((path, fragment)) = path.rsplit_once('#') {
110 (path.trim(), fragment.trim())
111 } else {
112 (path.trim(), "")
113 }
114}
115
116pub fn join_path(path: &str, fragment: &str) -> String {
129 let path = path.trim();
130 let fragment = fragment.trim();
131
132 if fragment.is_empty() {
133 path.to_string()
134 } else {
135 format!("{path}#{fragment}")
136 }
137}
138
139pub fn filter_template(src: &str, fragment: &str) -> Result<String, ErrorWithLine> {
169 let mut stack: FragmentStack<'_> = Default::default();
170 let mut res = String::new();
171 let mut last_line_idx = 0;
172
173 for (line_idx, line) in iterate_with_endings(src).enumerate() {
174 last_line_idx = line_idx;
175
176 match parse_fragment_tag(line, DEFAULT_TAG_MARKERS).map_err(|err| err.at(line_idx))? {
177 Some(Tag::Start(tag)) => stack.push(tag.fragments).map_err(|err| err.at(line_idx))?,
178 Some(Tag::End(_)) => {
179 stack.pop().map_err(|err| err.at(line_idx))?;
180 }
181 Some(Tag::StartBlock(tag)) => {
182 stack
183 .push(HashSet::from([tag.fragment]))
184 .map_err(|err| err.at(line_idx))?;
185 let line = format!(
186 "{}{{% block {} %}}{}",
187 tag.prefix,
188 tag.fragment,
189 get_ending(line)
190 );
191 if stack.is_active(fragment) {
192 res.push_str(&line);
193 }
194 }
195 Some(Tag::EndBlock(tag)) => {
196 let active = stack.pop().map_err(|err| err.at(line_idx))?;
197 let line = format!("{}{{% endblock %}}{}", tag.prefix, get_ending(line));
198 if active.contains(fragment) {
199 res.push_str(&line);
200 }
201 }
202 None => {
203 if stack.is_active(fragment) {
204 res.push_str(line);
205 }
206 }
207 }
208 }
209 stack.done().map_err(|err| err.at(last_line_idx))?;
210
211 Ok(res)
212}
213
214pub fn split_templates(src: &str) -> Result<HashMap<String, String>, ErrorWithLine> {
244 let mut stack: FragmentStack<'_> = Default::default();
245 let mut res: HashMap<String, String> = Default::default();
246 let mut last_line_idx = 0;
247
248 for (line_idx, line) in iterate_with_endings(src).enumerate() {
249 last_line_idx = line_idx;
250
251 match parse_fragment_tag(line, DEFAULT_TAG_MARKERS).map_err(|err| err.at(line_idx))? {
252 Some(Tag::Start(tag)) => stack.push(tag.fragments).map_err(|err| err.at(line_idx))?,
253 Some(Tag::End(_)) => {
254 stack.pop().map_err(|err| err.at(line_idx))?;
255 }
256 Some(Tag::StartBlock(tag)) => {
257 stack
258 .push(HashSet::from([tag.fragment]))
259 .map_err(|err| err.at(line_idx))?;
260 let line = format!(
261 "{}{{% block {} %}}{}",
262 tag.prefix,
263 tag.fragment,
264 get_ending(line)
265 );
266 for fragment in &stack.active_fragments {
267 push_line(&mut res, fragment, &line);
268 }
269 }
270 Some(Tag::EndBlock(tag)) => {
271 let fragments = stack.pop().map_err(|err| err.at(line_idx))?;
272 let line = format!("{}{{% endblock %}}{}", tag.prefix, get_ending(line));
273
274 for fragment in fragments {
275 push_line(&mut res, fragment, &line);
276 }
277 }
278 None => {
279 for fragment in &stack.active_fragments {
280 push_line(&mut res, fragment, line);
281 }
282 }
283 }
284 }
285 stack.done().map_err(|err| err.at(last_line_idx))?;
286
287 Ok(res)
288}
289
290fn push_line(res: &mut HashMap<String, String>, fragment: &str, line: &str) {
291 if let Some(target) = res.get_mut(fragment) {
292 target.push_str(line);
293 } else {
294 res.insert(fragment.to_owned(), line.to_owned());
295 }
296}
297
298fn get_ending(line: &str) -> &str {
299 if line.ends_with("\r\n") {
300 "\r\n"
301 } else if line.ends_with('\n') {
302 "\n"
303 } else {
304 ""
305 }
306}
307
308#[derive(Debug)]
309struct FragmentStack<'a> {
310 stack: Vec<HashSet<&'a str>>,
311 active_fragments: HashSet<&'a str>,
312}
313
314impl<'a> std::default::Default for FragmentStack<'a> {
315 fn default() -> Self {
316 Self {
317 stack: Vec::new(),
318 active_fragments: HashSet::from([""]),
319 }
320 }
321}
322
323impl<'a> FragmentStack<'a> {
324 fn push(&mut self, fragments: HashSet<&'a str>) -> Result<(), Error> {
326 let mut reentrant_fragments = Vec::new();
327
328 for &fragment in &fragments {
329 let not_seen = self.active_fragments.insert(fragment);
330 if !not_seen {
331 reentrant_fragments.push(fragment);
332 }
333 }
334 if !reentrant_fragments.is_empty() {
335 return Err(Error::ReentrantFragment(sorted_fragments(
336 reentrant_fragments,
337 )));
338 }
339
340 self.stack.push(fragments);
341 Ok(())
342 }
343
344 fn pop(&mut self) -> Result<HashSet<&'a str>, Error> {
347 let fragments = self.active_fragments.clone();
348 for fragment in self.stack.pop().ok_or(Error::UnbalancedEndTag)? {
349 self.active_fragments.remove(fragment);
350 }
351
352 Ok(fragments)
353 }
354
355 fn done(&self) -> Result<(), Error> {
356 if !self.stack.is_empty() {
357 let fragments: HashSet<&str> = self.stack.iter().flatten().copied().collect();
358 Err(Error::UnclosedTag(sorted_fragments(fragments)))
359 } else {
360 Ok(())
361 }
362 }
363
364 fn is_active(&self, fragment: &str) -> bool {
365 self.active_fragments.contains(fragment)
366 }
367}
368
369fn iterate_with_endings(mut s: &str) -> impl Iterator<Item = &str> {
370 std::iter::from_fn(move || {
371 let res;
372 match s.find('\n') {
373 Some(new_line_idx) => {
374 let split_idx = new_line_idx + '\n'.len_utf8();
375 res = Some(&s[..split_idx]);
376 s = &s[split_idx..];
377 }
378 None if !s.is_empty() => {
379 res = Some(s);
380 s = "";
381 }
382 None => {
383 res = None;
384 }
385 }
386 res
387 })
388}
389
390#[derive(Debug, Clone, PartialEq, Eq)]
391enum Tag<'a> {
392 Start(StartTag<'a>),
393 End(EndTag),
394 StartBlock(StartBlockTag<'a>),
395 EndBlock(EndBlockTag<'a>),
396}
397
398#[derive(Debug, Clone, PartialEq, Eq)]
399struct StartTag<'a> {
400 fragments: HashSet<&'a str>,
401}
402
403#[derive(Debug, Clone, PartialEq, Eq)]
404struct StartBlockTag<'a> {
405 prefix: &'a str,
406 fragment: &'a str,
407}
408
409#[derive(Debug, Clone, PartialEq, Eq)]
410struct EndBlockTag<'a> {
411 prefix: &'a str,
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
415struct EndTag;
416
417fn parse_fragment_tag<'l>(
418 line: &'l str,
419 tag_markers: (&str, &str),
420) -> Result<Option<Tag<'l>>, Error> {
421 let parts = match parse_base(line, tag_markers) {
422 Some(parts) => parts,
423 None => return Ok(None),
424 };
425
426 if !parts.head.trim().is_empty() {
427 return Err(Error::LeadingContent(parts.head.to_owned()));
428 }
429
430 if !parts.tail.trim().is_empty() {
431 return Err(Error::TrailingContent(parts.tail.to_owned()));
432 }
433
434 match parts.fragment_type {
435 FragmentType::Start | FragmentType::BlockStart => {
436 let data = parts.data.trim();
437 if data.is_empty() {
438 return Err(Error::StartTagWithoutData);
439 }
440
441 let block = matches!(parts.fragment_type, FragmentType::BlockStart);
442
443 let fragments: HashSet<&str> = data.split_whitespace().collect();
444
445 let mut invalid_fragments = Vec::new();
446 for &fragment in &fragments {
447 if !is_valid_fragment_name(fragment) {
448 invalid_fragments.push(fragment);
449 }
450 }
451 if !invalid_fragments.is_empty() {
452 return Err(Error::InvalidFragmentName(sorted_fragments(
453 invalid_fragments,
454 )));
455 }
456
457 if !block {
458 Ok(Some(Tag::Start(StartTag { fragments })))
459 } else {
460 if fragments.len() > 1 {
461 return Err(Error::MultipleNamesBlock(sorted_fragments(fragments)));
462 } else if fragments.is_empty() {
463 return Err(Error::UnnamedBlock);
464 }
465
466 let fragment = fragments.into_iter().next().unwrap();
467 Ok(Some(Tag::StartBlock(StartBlockTag {
468 prefix: parts.head,
469 fragment,
470 })))
471 }
472 }
473 FragmentType::End => {
474 if !parts.data.trim().is_empty() {
475 return Err(Error::EndTagWithData(parts.data.to_owned()));
476 }
477 Ok(Some(Tag::End(EndTag)))
478 }
479 FragmentType::BlockEnd => {
480 if !parts.data.trim().is_empty() {
481 return Err(Error::EndTagWithData(parts.data.to_owned()));
482 }
483 Ok(Some(Tag::EndBlock(EndBlockTag { prefix: parts.head })))
484 }
485 }
486}
487
488fn parse_base<'l>(line: &'l str, tag_markers: (&str, &str)) -> Option<LineParts<'l>> {
489 let (head, line) = line.split_once(tag_markers.0)?;
491 let line = line.strip_prefix(char::is_whitespace)?;
492
493 use FragmentType as T;
494
495 let (fragment_type, line) = None
497 .or_else(|| {
498 line.strip_prefix("fragment-block")
499 .map(|l| (T::BlockStart, l))
500 })
501 .or_else(|| {
502 line.strip_prefix("endfragment-block")
503 .map(|l| (T::BlockEnd, l))
504 })
505 .or_else(|| line.strip_prefix("fragment").map(|l| (T::Start, l)))
506 .or_else(|| line.strip_prefix("endfragment").map(|l| (T::End, l)))?;
507
508 let line = line.strip_prefix(char::is_whitespace)?;
509 let (data, line) = line.split_once(tag_markers.1)?;
510 let tail = line;
511
512 Some(LineParts {
513 head,
514 fragment_type,
515 data,
516 tail,
517 })
518}
519
520fn is_valid_fragment_name(name: &str) -> bool {
521 let is_reserved = matches!(name, "block");
522 let only_valid_chars = name
523 .chars()
524 .all(|c| c.is_alphanumeric() || matches!(c, '-' | '_'));
525
526 !is_reserved && only_valid_chars
527}
528
529#[derive(Debug, Clone, PartialEq, Eq)]
530struct LineParts<'a> {
531 head: &'a str,
532 fragment_type: FragmentType,
533 data: &'a str,
534 tail: &'a str,
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
538pub(crate) enum FragmentType {
539 Start,
540 End,
541 BlockStart,
542 BlockEnd,
543}
544
545fn sorted_fragments<'a, I: IntoIterator<Item = &'a str>>(fragments: I) -> String {
546 let mut fragments = fragments.into_iter().collect::<Vec<_>>();
547 fragments.sort();
548
549 let mut res = String::new();
550 for fragment in fragments {
551 push_join(&mut res, fragment);
552 }
553 res
554}
555
556#[derive(Debug, Clone, PartialEq, Eq)]
559pub enum Error {
560 LeadingContent(String),
562 TrailingContent(String),
564 EndTagWithData(String),
566 StartTagWithoutData,
568 ReentrantFragment(String),
570 UnclosedTag(String),
572 UnbalancedEndTag,
574 InvalidFragmentName(String),
576 UnnamedBlock,
578 MultipleNamesBlock(String),
580}
581
582impl Error {
583 pub fn at(self, line: usize) -> ErrorWithLine {
584 ErrorWithLine(line, self)
585 }
586}
587
588impl std::fmt::Display for Error {
589 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
590 match self {
591 Self::LeadingContent(content) => write!(f, "Error::LeadingContent({content:?})"),
592 Self::TrailingContent(content) => write!(f, "Error::TrailingContent({content:?})"),
593 Self::EndTagWithData(data) => write!(f, "Error::EndTagWithData({data:?})"),
594 Self::StartTagWithoutData => write!(f, "Error::StartTagWithoutData"),
595 Self::ReentrantFragment(fragments) => write!(f, "Error::ReentrantFragment({fragments}"),
596 Self::UnbalancedEndTag => write!(f, "Error::UnbalancedTags"),
597 Self::UnclosedTag(fragments) => write!(f, "Error::UnclosedTag({fragments})"),
598 Self::InvalidFragmentName(fragments) => {
599 write!(f, "Error::InvalidFragmentName({fragments}")
600 }
601 Self::UnnamedBlock => write!(f, "Error::UnnamedBlock"),
602 Self::MultipleNamesBlock(fragments) => {
603 write!(f, "Error::MultipleNamesBlock({fragments}")
604 }
605 }
606 }
607}
608
609impl std::error::Error for Error {}
610
611#[derive(Debug, Clone, PartialEq, Eq)]
614pub struct ErrorWithLine(pub usize, pub Error);
615
616impl std::fmt::Display for ErrorWithLine {
617 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
618 write!(f, "{} at line {}", self.1, self.0 + 1)
619 }
620}
621
622impl std::error::Error for ErrorWithLine {}
623
624fn push_join(s: &mut String, t: &str) {
625 if !s.is_empty() {
626 s.push_str(", ");
627 }
628 s.push_str(t);
629}