1use std::collections::BTreeMap;
18use std::rc::Rc;
19
20use carta_ast::{
21 Alignment, Attr, Block, Caption, Cell, Citation, CitationMode, ColSpec, ColWidth, Document,
22 Format, Inline, ListAttributes, ListNumberDelim, ListNumberStyle, MathType, MetaValue,
23 QuoteType, Row, Table, TableBody, TableFoot, TableHead, Target, slug, slug_gfm, to_plain_text,
24};
25use carta_core::{Extension, Extensions, Reader, ReaderOptions, Result};
26
27use crate::heading_ids::{IdRegistry, IdScheme};
28
29#[derive(Debug, Default, Clone, Copy)]
31pub struct LatexReader;
32
33impl Reader for LatexReader {
34 fn read(&self, input: &str, options: &ReaderOptions) -> Result<Document> {
35 let ext = options.extensions;
36 let (preamble, body) = split_document(input);
37
38 let base_level = base_section_level(body);
41
42 let mut parser = Parser {
43 frames: Vec::new(),
44 ext,
45 smart: ext.contains(Extension::Smart),
46 meta: BTreeMap::new(),
47 macros: Rc::new(BTreeMap::new()),
48 ids: IdRegistry::default(),
49 base_level,
50 in_figure: false,
51 in_float: false,
52 expand_depth: 0,
53 total_expansions: 0,
54 last_ws_had_newline: false,
55 };
56
57 if let Some(preamble) = preamble {
59 parser.set_source(preamble);
60 let _ = parser.parse_blocks(&Stop::Eof);
61 }
62
63 parser.set_source(body);
64 let blocks = parser.parse_blocks(&Stop::Env("document"));
65
66 Ok(Document {
67 meta: parser
68 .meta
69 .into_iter()
70 .map(|(k, v)| (k.into(), v))
71 .collect(),
72 blocks,
73 ..Document::default()
74 })
75 }
76}
77
78fn split_document(input: &str) -> (Option<&str>, &str) {
82 if let Some(begin) = input.find("\\begin{document}") {
85 let after = &input[begin + "\\begin{document}".len()..];
86 let body = match after.find("\\end{document}") {
87 Some(end) => &after[..end],
88 None => after,
89 };
90 (Some(&input[..begin]), body)
91 } else {
92 (None, input)
93 }
94}
95
96fn base_section_level(body: &str) -> i32 {
99 if has_command(body, "part") {
100 -1
101 } else {
102 i32::from(!has_command(body, "chapter"))
103 }
104}
105
106fn has_command(text: &str, name: &str) -> bool {
109 let bytes = text.as_bytes();
110 let pat = format!("\\{name}");
111 let mut from = 0;
112 while let Some(rel) = text[from..].find(&pat) {
113 let start = from + rel;
114 let after = start + pat.len();
115 let next = bytes.get(after).copied();
116 match next {
117 Some(b) if b.is_ascii_alphabetic() => {}
118 _ => return true,
119 }
120 from = after;
121 }
122 false
123}
124
125enum Stop<'a> {
127 Eof,
129 Env(&'a str),
131 Item(&'a str),
133}
134
135#[derive(Clone)]
139struct Macro {
140 args: usize,
141 optional_default: Option<String>,
142 body: String,
143}
144
145struct Frame {
149 chars: Vec<char>,
150 pos: usize,
151}
152
153#[allow(clippy::struct_excessive_bools)]
155struct Parser {
156 frames: Vec<Frame>,
159 ext: Extensions,
160 smart: bool,
161 meta: BTreeMap<String, MetaValue>,
162 macros: Rc<BTreeMap<String, Macro>>,
165 ids: IdRegistry,
166 base_level: i32,
168 in_figure: bool,
170 in_float: bool,
173 expand_depth: u32,
176 total_expansions: u32,
179 last_ws_had_newline: bool,
182}
183
184#[derive(Clone, Copy, PartialEq, Eq)]
186enum InlineStop {
187 Group,
189 Bracket,
191 Paragraph,
193 QuoteSingle,
195 QuoteDouble,
197}
198
199const MAX_EXPAND_DEPTH: u32 = 200;
201
202const MAX_TOTAL_EXPANSIONS: u32 = 100_000;
204
205impl Parser {
206 fn set_source(&mut self, source: &str) {
209 self.frames = vec![Frame {
210 chars: source.chars().collect(),
211 pos: 0,
212 }];
213 self.expand_depth = 0;
214 }
215
216 fn drop_exhausted_frames(&mut self) {
219 while self.frames.len() > 1 {
220 match self.frames.last() {
221 Some(frame) if frame.pos >= frame.chars.len() => {
222 self.frames.pop();
223 self.expand_depth = self.expand_depth.saturating_sub(1);
224 }
225 _ => break,
226 }
227 }
228 }
229
230 fn cur(&self) -> Option<char> {
231 self.frames
232 .iter()
233 .rev()
234 .find_map(|frame| frame.chars.get(frame.pos).copied())
235 }
236
237 fn at(&self, offset: usize) -> Option<char> {
238 let mut remaining = offset;
239 for frame in self.frames.iter().rev() {
240 let available = frame.chars.len().saturating_sub(frame.pos);
241 if remaining < available {
242 return frame.chars.get(frame.pos + remaining).copied();
243 }
244 remaining -= available;
245 }
246 None
247 }
248
249 fn bump(&mut self) -> Option<char> {
250 self.drop_exhausted_frames();
251 let frame = self.frames.last_mut()?;
252 let c = frame.chars.get(frame.pos).copied();
253 if c.is_some() {
254 frame.pos += 1;
255 }
256 c
257 }
258
259 fn advance_chars(&mut self, n: usize) {
261 for _ in 0..n {
262 self.bump();
263 }
264 }
265
266 fn eof(&self) -> bool {
267 self.cur().is_none()
268 }
269
270 fn looking_at(&self, s: &str) -> bool {
272 s.chars().enumerate().all(|(i, c)| self.at(i) == Some(c))
273 }
274
275 fn eat(&mut self, s: &str) -> bool {
277 if self.looking_at(s) {
278 self.advance_chars(s.chars().count());
279 true
280 } else {
281 false
282 }
283 }
284
285 fn parse_blocks(&mut self, stop: &Stop) -> Vec<Block> {
288 let mut blocks = Vec::new();
289 loop {
290 self.skip_block_ws();
291 if self.at_stop(stop) {
292 break;
293 }
294 if let Some(mut produced) = self.parse_block_construct() {
295 blocks.append(&mut produced);
296 } else {
297 let para = self.parse_paragraph();
298 if !para.is_empty() {
299 blocks.push(Block::Para(para));
300 } else if !self.advance_over_stray() {
301 break;
302 }
303 }
304 }
305 blocks
306 }
307
308 fn at_stop(&self, stop: &Stop) -> bool {
310 match stop {
311 Stop::Eof => self.eof(),
312 Stop::Env(name) => self.eof() || self.at_end_env(name),
313 Stop::Item(name) => self.eof() || self.at_end_env(name) || self.at_control_word("item"),
314 }
315 }
316
317 fn at_end_env(&self, name: &str) -> bool {
319 match self.peek_env_after("\\end") {
320 Some(env) => env == name,
321 None => false,
322 }
323 }
324
325 fn at_control_word(&self, name: &str) -> bool {
327 if self.cur() != Some('\\') {
328 return false;
329 }
330 for (i, c) in name.chars().enumerate() {
331 if self.at(1 + i) != Some(c) {
332 return false;
333 }
334 }
335 match self.at(1 + name.chars().count()) {
336 Some(c) => !c.is_ascii_alphabetic(),
337 None => true,
338 }
339 }
340
341 fn peek_env_after(&self, prefix: &str) -> Option<String> {
344 let mut i = 0;
345 for c in prefix.chars() {
346 if self.at(i) != Some(c) {
347 return None;
348 }
349 i += 1;
350 }
351 while self.at(i) == Some(' ') {
352 i += 1;
353 }
354 if self.at(i) != Some('{') {
355 return None;
356 }
357 i += 1;
358 let mut name = String::new();
359 while let Some(c) = self.at(i) {
360 if c == '}' {
361 return Some(name);
362 }
363 name.push(c);
364 i += 1;
365 }
366 None
367 }
368
369 fn advance_over_stray(&mut self) -> bool {
371 if self.looking_at("\\end")
373 && let Some(env) = self.peek_env_after("\\end")
374 {
375 self.consume_env_marker("\\end");
376 let _ = env;
377 return true;
378 }
379 if self.cur() == Some('\\') && self.at(1).is_some_and(|c| c.is_ascii_alphabetic()) {
382 self.bump();
383 while self.cur().is_some_and(|c| c.is_ascii_alphabetic()) {
384 self.bump();
385 }
386 let _ = self.read_optional_raw();
387 return true;
388 }
389 self.bump().is_some()
390 }
391
392 fn skip_block_ws(&mut self) {
394 loop {
395 match self.cur() {
396 Some(c) if c.is_whitespace() => {
397 self.bump();
398 }
399 Some('%') => self.skip_comment(),
400 _ => break,
401 }
402 }
403 }
404
405 fn skip_comment(&mut self) {
407 while let Some(c) = self.cur() {
408 if c == '\n' {
409 break;
410 }
411 self.bump();
412 }
413 }
414
415 fn parse_block_construct(&mut self) -> Option<Vec<Block>> {
418 if self.cur() != Some('\\') {
419 return None;
420 }
421 if let Some(env) = self.peek_env_after("\\begin") {
423 if math_env(&env) {
424 return None;
425 }
426 return Some(self.parse_environment(&env));
427 }
428 if self.try_expand_macro() {
430 return self.parse_block_construct();
431 }
432 let name = self.peek_control_word()?;
433 if let Some(level) = section_intrinsic(&name) {
434 return Some(vec![self.parse_section(level)]);
435 }
436 match name.as_str() {
437 "title" | "author" | "date" | "subtitle" => {
438 self.consume_control_word();
439 self.capture_meta(&name);
440 Some(Vec::new())
441 }
442 "newcommand"
443 | "renewcommand"
444 | "providecommand"
445 | "DeclareRobustCommand"
446 | "def"
447 | "let" => Some(self.parse_macro_definition(&name)),
448 "par" | "maketitle" | "tableofcontents" | "listoffigures" | "listoftables"
451 | "frontmatter" | "mainmatter" | "backmatter" | "appendix" | "clearpage"
452 | "cleardoublepage" | "newpage" | "pagebreak" | "noindent" | "bigskip" | "medskip"
453 | "smallskip" | "centering" | "raggedright" | "raggedleft" | "printindex"
454 | "printbibliography" | "documentclass" | "usepackage" | "RequirePackage"
455 | "pagestyle" | "thispagestyle" | "pagenumbering" | "setlength" | "setcounter"
456 | "addtocounter" | "geometry" | "hypersetup" | "bibliographystyle" | "include"
457 | "input" | "graphicspath" | "definecolor" | "newtheorem" | "theoremstyle"
458 | "captionsetup" | "bibliography" => {
459 self.consume_control_word();
460 self.skip_command_args(&name);
461 Some(Vec::new())
462 }
463 _ => None,
464 }
465 }
466
467 fn peek_control_word(&self) -> Option<String> {
470 if self.cur() != Some('\\') {
471 return None;
472 }
473 let mut i = 1;
474 let mut name = String::new();
475 while let Some(c) = self.at(i) {
476 if c.is_ascii_alphabetic() {
477 name.push(c);
478 i += 1;
479 } else {
480 break;
481 }
482 }
483 if name.is_empty() { None } else { Some(name) }
484 }
485
486 fn consume_control_word(&mut self) -> String {
488 self.bump(); let mut name = String::new();
490 while let Some(c) = self.cur() {
491 if c.is_ascii_alphabetic() {
492 name.push(c);
493 self.bump();
494 } else {
495 break;
496 }
497 }
498 while matches!(self.cur(), Some(' ' | '\t')) {
500 self.bump();
501 }
502 name
503 }
504
505 fn consume_env_marker(&mut self, prefix: &str) {
507 self.eat(prefix);
508 while self.cur() == Some(' ') {
509 self.bump();
510 }
511 if self.cur() == Some('{') {
512 self.bump();
513 while let Some(c) = self.cur() {
514 self.bump();
515 if c == '}' {
516 break;
517 }
518 }
519 }
520 }
521
522 fn parse_section(&mut self, intrinsic: i32) -> Block {
525 self.consume_control_word();
526 let starred = self.cur() == Some('*');
527 if starred {
528 self.bump();
529 while matches!(self.cur(), Some(' ' | '\t')) {
530 self.bump();
531 }
532 }
533 let _ = self.read_optional_raw();
535 let mut label = None;
536 let title = self.parse_group_inlines_capturing_label(&mut label);
537 self.skip_block_ws();
539 if let Some(id) = self.peek_env_arg_after_label() {
540 label = Some(id);
541 }
542
543 let level = (intrinsic - self.base_level + 1).max(1);
544 let id = match label {
545 Some(id) => {
546 self.ids.reserve_native(&id);
547 id
548 }
549 None => self.assign_id(&to_plain_text(&title)),
550 };
551 let mut classes = Vec::new();
552 if starred {
553 classes.push("unnumbered".into());
554 }
555 Block::Header(
556 level,
557 Box::new(Attr {
558 id: id.into(),
559 classes,
560 attributes: Vec::new(),
561 }),
562 title,
563 )
564 }
565
566 fn peek_env_arg_after_label(&mut self) -> Option<String> {
568 if self.at_control_word("label") {
569 self.consume_control_word();
570 return self.read_group_raw();
571 }
572 None
573 }
574
575 fn parse_group_inlines_capturing_label(&mut self, label: &mut Option<String>) -> Vec<Inline> {
577 if self.cur() != Some('{') {
578 return Vec::new();
579 }
580 self.bump();
581 let inlines = self.parse_inlines(InlineStop::Group);
582 if self.cur() == Some('}') {
583 self.bump();
584 }
585 let mut kept = Vec::new();
588 for inline in inlines {
589 if let Inline::Span(attr, content) = &inline
590 && content.is_empty()
591 && attr.attributes.iter().any(|(k, _)| k == "label")
592 {
593 *label = Some(attr.id.to_string());
594 continue;
595 }
596 kept.push(inline);
597 }
598 kept
599 }
600
601 fn assign_id(&mut self, text: &str) -> String {
605 let Some(scheme) = IdScheme::select(self.ext, false) else {
606 return String::new();
607 };
608 let base = match scheme {
609 IdScheme::Plain => slug(text),
610 IdScheme::Gfm => slug_gfm(text),
611 };
612 self.ids.assign_native(base)
613 }
614
615 fn parse_environment(&mut self, env: &str) -> Vec<Block> {
618 self.consume_env_marker("\\begin");
619 match env {
621 "itemize" | "enumerate" => {
622 let _ = self.read_optional_raw();
623 vec![self.parse_list(env)]
624 }
625 "description" => {
626 let _ = self.read_optional_raw();
627 vec![self.parse_description()]
628 }
629 "quote" | "quotation" | "verse" => {
630 let inner = self.parse_blocks(&Stop::Env(env));
631 self.consume_env_marker("\\end");
632 vec![Block::BlockQuote(inner)]
633 }
634 "center" | "flushleft" | "flushright" => {
635 let inner = self.parse_blocks(&Stop::Env(env));
636 self.consume_env_marker("\\end");
637 vec![Block::Div(
638 Box::new(Attr {
639 id: carta_ast::Text::default(),
640 classes: vec![env.into()],
641 attributes: Vec::new(),
642 }),
643 inner,
644 )]
645 }
646 "minipage" => {
647 while self.read_optional_raw().is_some() {}
649 let _ = self.read_group_raw();
650 let inner = self.parse_blocks(&Stop::Env(env));
651 self.consume_env_marker("\\end");
652 vec![Block::Div(
653 Box::new(Attr {
654 id: carta_ast::Text::default(),
655 classes: vec!["minipage".into()],
656 attributes: Vec::new(),
657 }),
658 inner,
659 )]
660 }
661 "verbatim" | "verbatim*" | "Verbatim" | "lstlisting" | "minted" | "alltt"
662 | "lstinputlisting" => self.parse_verbatim_env(env),
663 "comment" => {
664 self.skip_to_end_env(env);
665 Vec::new()
666 }
667 "figure" | "figure*" | "wrapfigure" | "SCfigure" | "marginfigure" => {
668 vec![self.parse_figure(env)]
669 }
670 "table" | "table*" => self.parse_table_float(env),
671 "tabular" | "tabular*" | "tabularx" | "array" | "longtable" | "supertabular"
672 | "tabulary" => {
673 vec![self.parse_tabular(env)]
674 }
675 "abstract" => {
676 let inner = self.parse_blocks(&Stop::Env(env));
677 self.consume_env_marker("\\end");
678 self.meta
679 .insert("abstract".to_owned(), MetaValue::MetaBlocks(inner));
680 Vec::new()
681 }
682 "document" => {
683 let inner = self.parse_blocks(&Stop::Env(env));
684 self.consume_env_marker("\\end");
685 inner
686 }
687 _ => {
688 if self.ext.contains(Extension::RawTex) {
689 self.parse_raw_env(env)
690 } else {
691 let inner = self.parse_blocks(&Stop::Env(env));
692 self.consume_env_marker("\\end");
693 vec![Block::Div(
694 Box::new(Attr {
695 id: carta_ast::Text::default(),
696 classes: vec![env.into()],
697 attributes: Vec::new(),
698 }),
699 inner,
700 )]
701 }
702 }
703 }
704 }
705
706 fn parse_raw_env(&mut self, env: &str) -> Vec<Block> {
708 let mut raw = format!("\\begin{{{env}}}");
709 while !self.eof() {
710 if self.at_end_env(env) {
711 break;
712 }
713 if let Some(c) = self.bump() {
714 raw.push(c);
715 }
716 }
717 raw.push_str("\\end{");
719 raw.push_str(env);
720 raw.push('}');
721 self.consume_env_marker("\\end");
722 vec![Block::RawBlock(Format("latex".into()), raw.into())]
723 }
724
725 fn skip_to_end_env(&mut self, env: &str) {
727 while !self.eof() {
728 if self.at_end_env(env) {
729 break;
730 }
731 self.bump();
732 }
733 self.consume_env_marker("\\end");
734 }
735
736 fn parse_list(&mut self, env: &str) -> Block {
739 let items = self.parse_items(env);
740 if env == "enumerate" {
741 Block::OrderedList(
742 ListAttributes {
743 start: 1,
744 style: ListNumberStyle::DefaultStyle,
745 delim: ListNumberDelim::DefaultDelim,
746 },
747 items,
748 )
749 } else {
750 Block::BulletList(items)
751 }
752 }
753
754 fn parse_items(&mut self, env: &str) -> Vec<Vec<Block>> {
756 let mut items: Vec<Vec<Block>> = Vec::new();
757 loop {
758 self.skip_block_ws();
759 if self.eof() || self.at_end_env(env) {
760 break;
761 }
762 if self.at_control_word("item") {
763 self.consume_control_word();
764 let _ = self.read_optional_raw(); let blocks = self.parse_blocks(&Stop::Item(env));
766 items.push(blocks);
767 } else if !self.advance_over_stray() {
768 break;
769 }
770 }
771 self.consume_env_marker("\\end");
772 items
773 }
774
775 fn parse_description(&mut self) -> Block {
776 let mut entries: Vec<(Vec<Inline>, Vec<Vec<Block>>)> = Vec::new();
777 loop {
778 self.skip_block_ws();
779 if self.eof() || self.at_end_env("description") {
780 break;
781 }
782 if self.at_control_word("item") {
783 self.consume_control_word();
784 let term = self.read_optional_inlines().unwrap_or_default();
785 let blocks = self.parse_blocks(&Stop::Item("description"));
786 entries.push((term, vec![blocks]));
787 } else if !self.advance_over_stray() {
788 break;
789 }
790 }
791 self.consume_env_marker("\\end");
792 Block::DefinitionList(entries)
793 }
794
795 fn parse_verbatim_env(&mut self, env: &str) -> Vec<Block> {
798 let mut classes = Vec::new();
799 let mut attributes = Vec::new();
800 if matches!(env, "lstlisting" | "Verbatim" | "lstinputlisting") {
802 if let Some(opts) = self.read_optional_raw() {
803 for (k, v) in parse_key_values(&opts) {
804 if k == "language" && !v.is_empty() {
805 classes.push(v.to_lowercase().into());
806 }
807 attributes.push((k, v));
808 }
809 }
810 } else if env == "minted" {
811 let _ = self.read_optional_raw();
812 if let Some(lang) = self.read_group_raw()
813 && !lang.is_empty()
814 {
815 classes.push(lang.to_lowercase().into());
816 }
817 }
818 let content = self.read_verbatim_body(env);
819 vec![Block::CodeBlock(
820 Box::new(Attr {
821 id: carta_ast::Text::default(),
822 classes,
823 attributes: attributes
824 .into_iter()
825 .map(|(k, v)| (k.into(), v.into()))
826 .collect(),
827 }),
828 content.into(),
829 )]
830 }
831
832 fn read_verbatim_body(&mut self, env: &str) -> String {
834 let closing = format!("\\end{{{env}}}");
835 let mut body = String::new();
836 while !self.eof() {
837 if self.looking_at(&closing) {
838 break;
839 }
840 if let Some(c) = self.bump() {
841 body.push(c);
842 }
843 }
844 self.consume_env_marker("\\end");
845 body.trim_matches('\n').to_owned()
846 }
847
848 fn parse_figure(&mut self, env: &str) -> Block {
851 let _ = self.read_optional_raw(); if env == "wrapfigure" {
853 let _ = self.read_group_raw(); let _ = self.read_group_raw(); }
856 let was_in_figure = self.in_figure;
857 self.in_figure = true;
858 let (blocks, caption, id) = self.collect_float(env);
859 self.in_figure = was_in_figure;
860 Block::Figure(
861 Box::new(Attr {
862 id: id.unwrap_or_default().into(),
863 classes: Vec::new(),
864 attributes: Vec::new(),
865 }),
866 Box::new(Caption {
867 short: None,
868 long: caption,
869 }),
870 blocks.into_iter().map(demote_image_para).collect(),
871 )
872 }
873
874 fn parse_table_float(&mut self, env: &str) -> Vec<Block> {
875 let _ = self.read_optional_raw();
876 let (mut blocks, caption, id) = self.collect_float(env);
877 if !caption.is_empty()
878 && let Some(Block::Table(table)) =
879 blocks.iter_mut().find(|b| matches!(b, Block::Table(_)))
880 {
881 table.caption = Caption {
882 short: None,
883 long: caption,
884 };
885 if let Some(id) = id {
886 table.attr.id = id.into();
887 }
888 }
889 blocks
890 }
891
892 fn collect_float(&mut self, env: &str) -> (Vec<Block>, Vec<Block>, Option<String>) {
894 let mut blocks = Vec::new();
895 let mut caption = Vec::new();
896 let mut id = None;
897 let was_in_float = self.in_float;
898 self.in_float = true;
899 loop {
900 self.skip_block_ws();
901 if self.eof() || self.at_end_env(env) {
902 break;
903 }
904 if self.at_control_word("caption") {
905 self.consume_control_word();
906 let _ = self.read_optional_raw();
907 let inlines = self.parse_group_inlines_capturing_label(&mut id);
908 caption = vec![Block::Plain(inlines)];
909 continue;
910 }
911 if self.at_control_word("centering")
912 || self.at_control_word("small")
913 || self.at_control_word("footnotesize")
914 {
915 self.consume_control_word();
916 continue;
917 }
918 if self.at_control_word("label") {
919 self.consume_control_word();
920 id = self.read_group_raw();
921 continue;
922 }
923 if let Some(mut produced) = self.parse_block_construct() {
924 blocks.append(&mut produced);
925 } else {
926 let para = self.parse_paragraph();
927 if !para.is_empty() {
928 blocks.push(Block::Para(para));
929 } else if !self.advance_over_stray() {
930 break;
931 }
932 }
933 }
934 self.consume_env_marker("\\end");
935 self.in_float = was_in_float;
936 (blocks, caption, id)
937 }
938
939 fn parse_tabular(&mut self, env: &str) -> Block {
940 if env == "tabular*" || env == "tabularx" || env == "tabulary" {
941 let _ = self.read_group_raw(); }
943 let _ = self.read_optional_raw(); let spec = self.read_group_raw().unwrap_or_default();
945 let aligns = parse_column_spec(&spec);
946 let body = self.read_environment_source(env);
947 self.consume_env_marker("\\end");
948 build_table(self, &aligns, &body)
949 }
950
951 fn read_math_environment(&mut self, env: &str) -> Inline {
954 self.consume_env_marker("\\begin");
955 let body = self.read_environment_source(env);
956 self.consume_env_marker("\\end");
957 match env {
958 "math" => Inline::Math(MathType::InlineMath, body.trim().into()),
959 "displaymath" => Inline::Math(MathType::DisplayMath, body.trim().into()),
960 _ => {
961 let content = body.trim_end().trim_start_matches(['\n', '\r']);
964 Inline::Math(
965 MathType::DisplayMath,
966 format!("\\begin{{{env}}}\n{content}\n\\end{{{env}}}").into(),
967 )
968 }
969 }
970 }
971
972 fn read_environment_source(&mut self, env: &str) -> String {
974 let closing = format!("\\end{{{env}}}");
975 let mut out = String::new();
976 while !self.eof() {
977 if self.looking_at(&closing) {
978 break;
979 }
980 if let Some(c) = self.bump() {
981 out.push(c);
982 }
983 }
984 out
985 }
986
987 fn parse_paragraph(&mut self) -> Vec<Inline> {
990 let inlines = self.parse_inlines(InlineStop::Paragraph);
991 trim_inlines(inlines)
992 }
993
994 fn parse_inlines(&mut self, stop: InlineStop) -> Vec<Inline> {
995 let mut out = Vec::new();
996 let mut buf = String::new();
997 loop {
998 let Some(c) = self.cur() else {
999 break;
1000 };
1001 match stop {
1002 InlineStop::Group if c == '}' => break,
1003 InlineStop::Bracket if c == ']' => break,
1004 InlineStop::QuoteSingle if c == '\'' && self.quote_closes(1) => break,
1005 InlineStop::QuoteDouble
1006 if c == '\'' && self.at(1) == Some('\'') && self.quote_closes(2) =>
1007 {
1008 break;
1009 }
1010 _ => {}
1011 }
1012 match c {
1013 ' ' | '\t' | '\n' | '\r' | '%' => {
1014 let had_blank = self.consume_inline_ws();
1015 if had_blank && matches!(stop, InlineStop::Paragraph) {
1016 break;
1017 }
1018 flush_buf(&mut buf, &mut out);
1019 if !self.eof() {
1020 let ws = if had_blank || self.last_ws_had_newline {
1021 Inline::SoftBreak
1022 } else {
1023 Inline::Space
1024 };
1025 push_whitespace(&mut out, ws);
1026 }
1027 }
1028 '\\' => {
1029 if matches!(stop, InlineStop::Paragraph) && self.inline_break_ahead() {
1030 break;
1031 }
1032 if let Some(env) = self.peek_env_after("\\begin")
1033 && math_env(&env)
1034 {
1035 let math = self.read_math_environment(&env);
1036 emit(&mut out, &mut buf, math);
1037 continue;
1038 }
1039 if self.try_expand_macro() {
1040 continue;
1041 }
1042 if let Some(word) = self.peek_control_word()
1044 && let Some(switch) = switch_kind(&word)
1045 {
1046 self.apply_switch(switch, stop, &mut out, &mut buf);
1047 break;
1048 }
1049 self.exec_control(&mut out, &mut buf);
1052 }
1053 '{' => {
1054 self.bump();
1055 let inner = self.parse_inlines(InlineStop::Group);
1056 if self.cur() == Some('}') {
1057 self.bump();
1058 }
1059 if let Some(span) = group_span(inner) {
1062 emit(&mut out, &mut buf, span);
1063 }
1064 }
1065 '}' => {
1066 buf.push('}');
1068 self.bump();
1069 }
1070 '$' => {
1071 flush_buf(&mut buf, &mut out);
1072 let math = self.read_dollar_math();
1073 out.push(math);
1074 }
1075 '~' => {
1076 buf.push('\u{a0}');
1077 self.bump();
1078 }
1079 '-' => {
1080 self.read_dashes(&mut buf);
1081 }
1082 '`' if self.smart => {
1083 flush_buf(&mut buf, &mut out);
1084 self.read_open_quote(&mut out);
1085 }
1086 '\'' => {
1087 self.read_apostrophe(&mut buf);
1088 }
1089 _ => {
1090 buf.push(c);
1091 self.bump();
1092 }
1093 }
1094 }
1095 flush_buf(&mut buf, &mut out);
1096 out
1097 }
1098
1099 fn inline_break_ahead(&self) -> bool {
1101 if let Some(env) = self.peek_env_after("\\begin") {
1102 return !math_env(&env);
1103 }
1104 if self.looking_at("\\end") && self.peek_env_after("\\end").is_some() {
1105 return true;
1106 }
1107 if let Some(word) = self.peek_control_word() {
1108 if section_intrinsic(&word).is_some() {
1109 return true;
1110 }
1111 if self.in_float && word == "caption" {
1112 return true;
1113 }
1114 return matches!(word.as_str(), "item" | "par");
1115 }
1116 false
1117 }
1118
1119 fn quote_closes(&self, offset: usize) -> bool {
1122 match self.at(offset) {
1123 Some(c) => !c.is_alphanumeric(),
1124 None => true,
1125 }
1126 }
1127
1128 fn consume_inline_ws(&mut self) -> bool {
1131 let mut newlines = 0u32;
1132 loop {
1133 match self.cur() {
1134 Some('\n') => {
1135 newlines += 1;
1136 self.bump();
1137 }
1138 Some(' ' | '\t' | '\r') => {
1139 self.bump();
1140 }
1141 Some('%') => self.skip_comment(),
1142 _ => break,
1143 }
1144 }
1145 self.last_ws_had_newline = newlines > 0;
1146 newlines >= 2
1147 }
1148
1149 fn read_dashes(&mut self, buf: &mut String) {
1150 let mut count = 0;
1151 while self.cur() == Some('-') {
1152 count += 1;
1153 self.bump();
1154 }
1155 while count >= 3 {
1156 buf.push('\u{2014}');
1157 count -= 3;
1158 }
1159 if count == 2 {
1160 buf.push('\u{2013}');
1161 } else if count == 1 {
1162 buf.push('-');
1163 }
1164 }
1165
1166 fn read_apostrophe(&mut self, buf: &mut String) {
1167 if self.cur() == Some('\'') && self.at(1) == Some('\'') {
1168 buf.push('\u{201d}');
1169 self.bump();
1170 self.bump();
1171 } else {
1172 buf.push('\u{2019}');
1173 self.bump();
1174 }
1175 }
1176
1177 fn read_open_quote(&mut self, out: &mut Vec<Inline>) {
1179 if self.at(1) == Some('`') {
1180 self.bump();
1181 self.bump();
1182 let inner = self.parse_inlines(InlineStop::QuoteDouble);
1183 if self.cur() == Some('\'') && self.at(1) == Some('\'') {
1184 self.bump();
1185 self.bump();
1186 out.push(Inline::Quoted(QuoteType::DoubleQuote, inner));
1187 } else {
1188 out.push(Inline::Str("\u{201c}".into()));
1189 out.extend(inner);
1190 }
1191 } else {
1192 self.bump();
1193 let inner = self.parse_inlines(InlineStop::QuoteSingle);
1194 if self.cur() == Some('\'') {
1195 self.bump();
1196 out.push(Inline::Quoted(QuoteType::SingleQuote, inner));
1197 } else {
1198 out.push(Inline::Str("\u{2018}".into()));
1199 out.extend(inner);
1200 }
1201 }
1202 }
1203
1204 fn exec_control(&mut self, out: &mut Vec<Inline>, buf: &mut String) {
1208 if self.at(1).is_some_and(|c| !c.is_ascii_alphabetic()) {
1210 self.exec_control_symbol(out, buf);
1211 return;
1212 }
1213 let name = self.consume_control_word();
1214 self.exec_named(&name, out, buf);
1215 }
1216
1217 fn exec_control_symbol(&mut self, out: &mut Vec<Inline>, buf: &mut String) {
1218 self.bump(); let Some(c) = self.bump() else {
1220 return;
1221 };
1222 match c {
1223 '\\' => {
1224 if self.cur() == Some('*') {
1227 self.bump();
1228 }
1229 let _ = self.read_optional_raw();
1230 flush_buf(buf, out);
1231 while matches!(out.last(), Some(Inline::Space | Inline::SoftBreak)) {
1232 out.pop();
1233 }
1234 out.push(Inline::LineBreak);
1235 }
1236 '[' => {
1237 let text = self.read_math_body("\\]");
1238 emit(out, buf, Inline::Math(MathType::DisplayMath, text.into()));
1239 }
1240 '(' => {
1241 let text = self.read_math_body("\\)");
1242 emit(out, buf, Inline::Math(MathType::InlineMath, text.into()));
1243 }
1244 ' ' | '\n' | '\t' => buf.push('\u{a0}'),
1246 ',' => buf.push('\u{2006}'),
1248 '&' | '%' | '#' | '$' | '_' | '{' | '}' => buf.push(c),
1249 '~' => self.read_accent_symbol(Accent::Tilde, buf),
1250 '^' => self.read_accent_symbol(Accent::Circumflex, buf),
1251 '\'' => self.read_accent_symbol(Accent::Acute, buf),
1252 '`' => self.read_accent_symbol(Accent::Grave, buf),
1253 '"' => self.read_accent_symbol(Accent::Diaeresis, buf),
1254 '=' => self.read_accent_symbol(Accent::Macron, buf),
1255 '.' => self.read_accent_symbol(Accent::DotAbove, buf),
1256 '-' | '/' | ';' | ':' | '!' | '@' | ')' | ']' => {}
1258 other => buf.push(other),
1259 }
1260 }
1261
1262 fn apply_switch(
1264 &mut self,
1265 switch: Switch,
1266 stop: InlineStop,
1267 out: &mut Vec<Inline>,
1268 buf: &mut String,
1269 ) {
1270 self.consume_control_word();
1271 flush_buf(buf, out);
1272 let rest = self.parse_inlines(stop);
1273 if matches!(switch, Switch::Code) {
1274 out.push(switch.wrap(rest));
1275 } else {
1276 out.extend(extract_spaces(rest, |i| switch.wrap(i)));
1277 }
1278 }
1279
1280 #[allow(clippy::too_many_lines)]
1281 fn exec_named(&mut self, name: &str, out: &mut Vec<Inline>, buf: &mut String) {
1282 if let Some(wrap) = inline_wrapper(name) {
1284 let inner = self.parse_group_inlines();
1285 if matches!(name, "underline" | "uline") {
1286 emit(out, buf, wrap(inner));
1287 } else {
1288 emit_all(out, buf, extract_spaces(inner, wrap));
1289 }
1290 return;
1291 }
1292 if let Some(accent) = word_accent(name) {
1294 self.read_accent_symbol(accent, buf);
1295 return;
1296 }
1297 if let Some(class) = font_span_class(name) {
1299 let inner = self.parse_group_inlines();
1300 emit_all(out, buf, extract_spaces(inner, |i| span_class(i, class)));
1301 return;
1302 }
1303 match name {
1304 "textcolor" | "colorbox" => {
1305 let color = self.read_group_raw().unwrap_or_default();
1306 let inner = self.parse_group_inlines();
1307 let property = if name == "colorbox" {
1308 "background-color"
1309 } else {
1310 "color"
1311 };
1312 let attr = Attr {
1313 id: carta_ast::Text::default(),
1314 classes: Vec::new(),
1315 attributes: vec![(
1316 "style".into(),
1317 format!("{property}: {}", color.trim()).into(),
1318 )],
1319 };
1320 emit(out, buf, Inline::Span(Box::new(attr), inner));
1321 }
1322 "texttt" | "lstinline" => {
1323 if name == "lstinline" {
1324 let _ = self.read_optional_raw();
1325 }
1326 let inner = self.parse_group_inlines();
1327 emit(
1328 out,
1329 buf,
1330 Inline::Code(Box::default(), to_plain_text(&inner).into()),
1331 );
1332 }
1333 "verb" => {
1334 if let Some(code) = self.read_verb() {
1335 emit(out, buf, Inline::Code(Box::default(), code.into()));
1336 }
1337 }
1338 "footnote" | "footnotetext" | "thanks" => {
1339 let _ = self.read_optional_raw();
1340 let blocks = self.parse_group_blocks();
1341 emit(out, buf, Inline::Note(blocks));
1342 }
1343 "url" | "nolinkurl" => {
1344 if let Some(url) = self.read_group_raw() {
1345 let url = unescape_url(&url);
1346 emit(
1347 out,
1348 buf,
1349 Inline::Link(
1350 Box::new(Attr {
1351 id: carta_ast::Text::default(),
1352 classes: vec!["uri".into()],
1353 attributes: Vec::new(),
1354 }),
1355 vec![Inline::Str(url.clone().into())],
1356 Box::new(Target {
1357 url: url.into(),
1358 title: carta_ast::Text::default(),
1359 }),
1360 ),
1361 );
1362 }
1363 }
1364 "href" => {
1365 let url = self
1366 .read_group_raw()
1367 .map(|u| unescape_url(&u))
1368 .unwrap_or_default();
1369 let text = self.parse_group_inlines();
1370 emit(
1371 out,
1372 buf,
1373 Inline::Link(
1374 Box::default(),
1375 text,
1376 Box::new(Target {
1377 url: url.into(),
1378 title: carta_ast::Text::default(),
1379 }),
1380 ),
1381 );
1382 }
1383 "includegraphics" => {
1384 let opts = self.read_optional_raw().unwrap_or_default();
1385 let path = self.read_group_raw().unwrap_or_default();
1386 let attributes = image_attributes(&opts);
1387 let alt = if self.in_figure {
1388 Vec::new()
1389 } else {
1390 vec![Inline::Str("image".into())]
1391 };
1392 emit(
1393 out,
1394 buf,
1395 Inline::Image(
1396 Box::new(Attr {
1397 id: carta_ast::Text::default(),
1398 classes: Vec::new(),
1399 attributes: attributes
1400 .into_iter()
1401 .map(|(k, v)| (k.into(), v.into()))
1402 .collect(),
1403 }),
1404 alt,
1405 Box::new(Target {
1406 url: path.into(),
1407 title: carta_ast::Text::default(),
1408 }),
1409 ),
1410 );
1411 }
1412 "label" => {
1413 if let Some(id) = self.read_group_raw() {
1414 emit(
1415 out,
1416 buf,
1417 Inline::Span(
1418 Box::new(Attr {
1419 id: id.clone().into(),
1420 classes: Vec::new(),
1421 attributes: vec![("label".into(), id.into())],
1422 }),
1423 Vec::new(),
1424 ),
1425 );
1426 }
1427 }
1428 "ref" | "eqref" | "autoref" | "cref" | "Cref" => {
1429 if let Some(target) = self.read_group_raw() {
1430 emit(out, buf, reference_link(name, &target));
1431 }
1432 }
1433 "cite" | "citep" | "citet" | "citealp" | "citealt" | "citeauthor" | "citeyear"
1434 | "parencite" | "textcite" | "footcite" | "autocite" => {
1435 flush_buf(buf, out);
1436 self.read_citation(name, out);
1437 }
1438 "textsuperscript" | "textsubscript" => {
1439 let inner = self.parse_group_inlines();
1440 let wrap: fn(Vec<Inline>) -> Inline = if name == "textsubscript" {
1441 Inline::Subscript
1442 } else {
1443 Inline::Superscript
1444 };
1445 emit_all(out, buf, extract_spaces(inner, wrap));
1446 }
1447 "mbox" | "hbox" => {
1448 let inner = self.parse_group_inlines();
1449 emit_all(out, buf, inner);
1450 }
1451 "ensuremath" => {
1452 let body = self.read_group_raw().unwrap_or_default();
1453 emit(
1454 out,
1455 buf,
1456 Inline::Math(MathType::InlineMath, body.trim().into()),
1457 );
1458 }
1459 "footnotemark" | "protect" | "noindent" | "indent" | "bigskip" | "medskip"
1460 | "smallskip" | "centering" | "hfill" | "hrulefill" | "dotfill" | "par"
1461 | "displaystyle" | "scriptsize" | "small" | "footnotesize" | "large" | "Large"
1462 | "LARGE" | "huge" | "Huge" | "normalsize" | "rmfamily" | "sffamily" | "ttfamily"
1463 | "mdseries" | "upshape" | "normalfont" | "sc" | "rm" | "sf" | "boldmath"
1464 | "unboldmath" | "clearpage" | "newpage" | "nolinebreak" | "sloppy" | "raggedright"
1465 | "item" => {
1466 }
1469 "linebreak" => {
1470 let _ = self.read_optional_raw();
1471 emit(out, buf, Inline::LineBreak);
1472 }
1473 "newline" => emit(out, buf, Inline::LineBreak),
1474 "hspace" | "vspace" | "hskip" | "vskip" | "setlength" | "vphantom" | "hphantom"
1475 | "phantom" | "rule" | "settowidth" => {
1476 self.skip_command_args(name);
1477 }
1478 _ => {
1479 if let Some(text) = symbol_text(name) {
1480 buf.push_str(text);
1481 } else if self.ext.contains(Extension::RawTex) {
1482 let raw = self.reconstruct_command(name);
1483 emit(
1484 out,
1485 buf,
1486 Inline::RawInline(Format("latex".into()), raw.into()),
1487 );
1488 } else {
1489 self.skip_adjacent_arguments();
1491 }
1492 }
1493 }
1494 }
1495
1496 fn reconstruct_command(&mut self, name: &str) -> String {
1499 let mut raw = format!("\\{name}");
1500 loop {
1501 match self.cur() {
1502 Some('[') => {
1503 if let Some(opt) = self.read_optional_raw() {
1504 raw.push('[');
1505 raw.push_str(&opt);
1506 raw.push(']');
1507 } else {
1508 break;
1509 }
1510 }
1511 Some('{') => {
1512 if let Some(arg) = self.read_group_raw() {
1513 raw.push('{');
1514 raw.push_str(&arg);
1515 raw.push('}');
1516 } else {
1517 break;
1518 }
1519 }
1520 _ => break,
1521 }
1522 }
1523 raw
1524 }
1525
1526 fn read_citation(&mut self, name: &str, out: &mut Vec<Inline>) {
1527 let opt1 = self.read_optional_raw();
1530 let opt2 = self.read_optional_raw();
1531 let keys_raw = self.read_group_raw().unwrap_or_default();
1532 let (prefix_raw, suffix_raw) = match (&opt1, &opt2) {
1533 (Some(pre), Some(post)) => (Some(pre.as_str()), Some(post.as_str())),
1534 (Some(post), None) => (None, Some(post.as_str())),
1535 _ => (None, None),
1536 };
1537 let prefix = prefix_raw
1538 .map(|s| self.parse_fragment(s))
1539 .unwrap_or_default();
1540 let suffix = suffix_raw
1541 .map(|s| self.parse_fragment(s))
1542 .unwrap_or_default();
1543 let mode = if matches!(name, "citet" | "textcite" | "citeauthor") {
1544 CitationMode::AuthorInText
1545 } else {
1546 CitationMode::NormalCitation
1547 };
1548 let mut citations = Vec::new();
1549 for key in keys_raw.split(',') {
1550 let key = key.trim();
1551 if key.is_empty() {
1552 continue;
1553 }
1554 citations.push(Citation {
1555 id: key.into(),
1556 prefix: Vec::new(),
1557 suffix: Vec::new(),
1558 mode: mode.clone(),
1559 note_num: 0,
1560 hash: 0,
1561 });
1562 }
1563 if citations.is_empty() {
1564 return;
1565 }
1566 if let Some(first) = citations.first_mut() {
1567 first.prefix = prefix;
1568 }
1569 if let Some(last) = citations.last_mut() {
1570 last.suffix = suffix;
1571 }
1572 let mut raw = format!("\\{name}");
1573 for opt in [&opt1, &opt2].into_iter().flatten() {
1574 raw.push('[');
1575 raw.push_str(opt);
1576 raw.push(']');
1577 }
1578 raw.push('{');
1579 raw.push_str(&keys_raw);
1580 raw.push('}');
1581 out.push(Inline::Cite(
1582 citations,
1583 vec![Inline::RawInline(Format("latex".into()), raw.into())],
1584 ));
1585 }
1586
1587 fn read_verb(&mut self) -> Option<String> {
1589 if self.cur() == Some('*') {
1590 self.bump();
1591 }
1592 let delim = self.bump()?;
1593 let mut code = String::new();
1594 while let Some(c) = self.cur() {
1595 self.bump();
1596 if c == delim {
1597 break;
1598 }
1599 code.push(c);
1600 }
1601 Some(code)
1602 }
1603
1604 fn child(&self, source: &str, in_figure: bool) -> Parser {
1609 Parser {
1610 frames: vec![Frame {
1611 chars: source.chars().collect(),
1612 pos: 0,
1613 }],
1614 ext: self.ext,
1615 smart: self.smart,
1616 meta: BTreeMap::new(),
1617 macros: Rc::clone(&self.macros),
1618 ids: IdRegistry::default(),
1619 base_level: self.base_level,
1620 in_figure,
1621 in_float: false,
1622 expand_depth: self.expand_depth,
1623 total_expansions: 0,
1624 last_ws_had_newline: false,
1625 }
1626 }
1627
1628 fn parse_group_blocks(&mut self) -> Vec<Block> {
1629 if self.cur() != Some('{') {
1630 return Vec::new();
1631 }
1632 let source = self.read_group_raw().unwrap_or_default();
1634 let mut sub = self.child(&source, self.in_figure);
1635 sub.parse_blocks(&Stop::Eof)
1636 }
1637
1638 fn parse_group_inlines(&mut self) -> Vec<Inline> {
1639 if self.cur() != Some('{') {
1640 return Vec::new();
1641 }
1642 self.bump();
1643 let inner = self.parse_inlines(InlineStop::Group);
1644 if self.cur() == Some('}') {
1645 self.bump();
1646 }
1647 inner
1648 }
1649
1650 fn read_dollar_math(&mut self) -> Inline {
1653 if self.cur() == Some('$') && self.at(1) == Some('$') {
1654 self.bump();
1655 self.bump();
1656 Inline::Math(MathType::DisplayMath, self.read_math_body("$$").into())
1657 } else {
1658 self.bump();
1659 Inline::Math(MathType::InlineMath, self.read_math_body("$").into())
1660 }
1661 }
1662
1663 fn read_math_body(&mut self, close: &str) -> String {
1665 let mut text = String::new();
1666 while !self.eof() {
1667 if self.looking_at(close) {
1668 self.advance_chars(close.chars().count());
1669 break;
1670 }
1671 if self.cur() == Some('\\') {
1673 if let Some(c) = self.bump() {
1674 text.push(c);
1675 }
1676 if let Some(c) = self.bump() {
1677 text.push(c);
1678 }
1679 continue;
1680 }
1681 if let Some(c) = self.bump() {
1682 text.push(c);
1683 }
1684 }
1685 text.trim().to_owned()
1686 }
1687
1688 fn read_accent_symbol(&mut self, accent: Accent, buf: &mut String) {
1691 let base = self.read_accent_argument();
1692 buf.push_str(&apply_accent(accent, base.as_deref()));
1693 }
1694
1695 fn read_accent_argument(&mut self) -> Option<String> {
1698 while matches!(self.cur(), Some(' ' | '\t')) {
1699 self.bump();
1700 }
1701 match self.cur() {
1702 Some('{') => {
1703 let raw = self.read_group_raw().unwrap_or_default();
1704 Some(resolve_accent_base(raw.trim()))
1705 }
1706 Some('\\') if self.at(1).is_some_and(|c| c.is_ascii_alphabetic()) => {
1707 let word = self.consume_control_word();
1708 Some(symbol_text(&word).map_or(word.clone(), str::to_owned))
1709 }
1710 Some('\\') => {
1711 self.bump();
1712 self.bump().map(|c| c.to_string())
1713 }
1714 _ => self.bump().map(|c| c.to_string()),
1715 }
1716 }
1717
1718 fn read_group_raw(&mut self) -> Option<String> {
1719 while matches!(self.cur(), Some(' ' | '\t')) {
1720 self.bump();
1721 }
1722 if self.cur() != Some('{') {
1723 return None;
1724 }
1725 self.bump();
1726 let mut depth = 1;
1727 let mut s = String::new();
1728 while let Some(c) = self.cur() {
1729 match c {
1730 '{' => {
1731 depth += 1;
1732 s.push(c);
1733 self.bump();
1734 }
1735 '}' => {
1736 depth -= 1;
1737 self.bump();
1738 if depth == 0 {
1739 break;
1740 }
1741 s.push('}');
1742 }
1743 '\\' => {
1744 s.push(c);
1745 self.bump();
1746 if let Some(n) = self.cur() {
1747 s.push(n);
1748 self.bump();
1749 }
1750 }
1751 _ => {
1752 s.push(c);
1753 self.bump();
1754 }
1755 }
1756 }
1757 Some(s)
1758 }
1759
1760 fn read_optional_raw(&mut self) -> Option<String> {
1761 if self.cur() != Some('[') {
1762 return None;
1763 }
1764 self.bump();
1765 let mut depth = 0;
1766 let mut s = String::new();
1767 while let Some(c) = self.cur() {
1768 match c {
1769 '{' => {
1770 depth += 1;
1771 s.push(c);
1772 self.bump();
1773 }
1774 '}' => {
1775 if depth > 0 {
1776 depth -= 1;
1777 }
1778 s.push(c);
1779 self.bump();
1780 }
1781 ']' if depth == 0 => {
1782 self.bump();
1783 break;
1784 }
1785 _ => {
1786 s.push(c);
1787 self.bump();
1788 }
1789 }
1790 }
1791 Some(s)
1792 }
1793
1794 fn read_optional_inlines(&mut self) -> Option<Vec<Inline>> {
1795 if self.cur() != Some('[') {
1796 return None;
1797 }
1798 self.bump();
1799 let inner = self.parse_inlines(InlineStop::Bracket);
1800 if self.cur() == Some(']') {
1801 self.bump();
1802 }
1803 Some(inner)
1804 }
1805
1806 fn skip_command_args(&mut self, name: &str) {
1809 while self.cur() == Some('[') {
1810 let _ = self.read_optional_raw();
1811 }
1812 for _ in 0..command_arg_count(name) {
1813 while self.cur() == Some('[') {
1814 let _ = self.read_optional_raw();
1815 }
1816 if self.read_group_raw().is_none() {
1817 break;
1818 }
1819 }
1820 }
1821
1822 fn skip_adjacent_arguments(&mut self) {
1825 loop {
1826 match self.cur() {
1827 Some('[') => {
1828 if self.read_optional_raw().is_none() {
1829 break;
1830 }
1831 }
1832 Some('{') => {
1833 if self.read_group_raw().is_none() {
1834 break;
1835 }
1836 }
1837 _ => break,
1838 }
1839 }
1840 }
1841
1842 fn capture_meta(&mut self, name: &str) {
1844 let _ = self.read_optional_raw();
1845 if name == "author" {
1846 let raw = self.read_group_raw().unwrap_or_default();
1848 let authors: Vec<MetaValue> = split_on_command(&raw, "and")
1849 .into_iter()
1850 .filter(|part| !part.trim().is_empty())
1851 .map(|part| MetaValue::MetaInlines(self.parse_fragment(part.trim())))
1852 .collect();
1853 self.meta
1854 .insert("author".to_owned(), MetaValue::MetaList(authors));
1855 return;
1856 }
1857 let inlines = self.parse_group_inlines();
1858 self.meta
1859 .insert(name.to_owned(), MetaValue::MetaInlines(inlines));
1860 }
1861
1862 fn parse_fragment(&self, source: &str) -> Vec<Inline> {
1864 parse_cell_inlines(self, source)
1865 }
1866
1867 fn parse_macro_definition(&mut self, name: &str) -> Vec<Block> {
1873 let start = self.frames.last().map_or(0, |frame| frame.pos);
1877 self.consume_control_word();
1878 if self.cur() == Some('*') {
1879 self.bump();
1880 }
1881 if name == "def" {
1882 self.parse_def();
1883 } else if name == "let" {
1884 let _ = self.take_defined_name();
1887 if self.cur() == Some('=') {
1888 self.bump();
1889 }
1890 if self.peek_control_word().is_some() {
1891 self.consume_control_word();
1892 }
1893 } else if let Some(macro_name) = self.take_defined_name() {
1894 let args = self
1895 .read_optional_raw()
1896 .and_then(|s| s.trim().parse::<usize>().ok())
1897 .unwrap_or(0);
1898 let optional_default = self.read_optional_raw();
1899 let body = self.read_group_raw().unwrap_or_default();
1900 Rc::make_mut(&mut self.macros).insert(
1901 macro_name,
1902 Macro {
1903 args,
1904 optional_default,
1905 body,
1906 },
1907 );
1908 }
1909 if self.ext.contains(Extension::LatexMacros) {
1910 return Vec::new();
1911 }
1912 let raw: String = self
1913 .frames
1914 .last()
1915 .and_then(|frame| frame.chars.get(start..frame.pos))
1916 .unwrap_or_default()
1917 .iter()
1918 .collect();
1919 vec![Block::RawBlock(Format("latex".into()), raw.into())]
1920 }
1921
1922 fn take_defined_name(&mut self) -> Option<String> {
1924 while matches!(self.cur(), Some(' ' | '\t')) {
1925 self.bump();
1926 }
1927 if self.cur() == Some('{') {
1928 let raw = self.read_group_raw()?;
1929 Some(raw.trim().trim_start_matches('\\').to_owned())
1930 } else if self.cur() == Some('\\') {
1931 Some(self.consume_control_word())
1932 } else {
1933 None
1934 }
1935 }
1936
1937 fn parse_def(&mut self) {
1938 let Some(macro_name) = self.take_defined_name() else {
1939 return;
1940 };
1941 let mut args = 0usize;
1943 while let Some(c) = self.cur() {
1944 if c == '{' {
1945 break;
1946 }
1947 if c == '#' {
1948 args += 1;
1949 self.bump();
1950 self.bump();
1951 } else {
1952 self.bump();
1953 }
1954 }
1955 let body = self.read_group_raw().unwrap_or_default();
1956 Rc::make_mut(&mut self.macros).insert(
1957 macro_name,
1958 Macro {
1959 args,
1960 optional_default: None,
1961 body,
1962 },
1963 );
1964 }
1965
1966 fn try_expand_macro(&mut self) -> bool {
1969 if !self.ext.contains(Extension::LatexMacros)
1970 || self.expand_depth >= MAX_EXPAND_DEPTH
1971 || self.total_expansions >= MAX_TOTAL_EXPANSIONS
1972 {
1973 return false;
1974 }
1975 let Some(name) = self.peek_control_word() else {
1976 return false;
1977 };
1978 let macros = Rc::clone(&self.macros);
1979 let Some(mac) = macros.get(&name) else {
1980 return false;
1981 };
1982 self.consume_control_word();
1983 let mut args = Vec::new();
1984 let mut mandatory = mac.args;
1985 if let Some(default) = &mac.optional_default {
1986 let first = if self.cur() == Some('[') {
1987 self.read_optional_raw().unwrap_or_default()
1988 } else {
1989 default.clone()
1990 };
1991 args.push(first);
1992 mandatory = mandatory.saturating_sub(1);
1993 }
1994 for _ in 0..mandatory {
1995 match self.read_macro_arg() {
1996 Some(a) => args.push(a),
1997 None => args.push(String::new()),
1998 }
1999 }
2000 let expanded = substitute_macro(&mac.body, &args);
2004 if !expanded.is_empty() {
2005 self.frames.push(Frame {
2006 chars: expanded.chars().collect(),
2007 pos: 0,
2008 });
2009 self.expand_depth += 1;
2010 }
2011 self.total_expansions += 1;
2012 true
2013 }
2014
2015 fn read_macro_arg(&mut self) -> Option<String> {
2017 while matches!(self.cur(), Some(' ' | '\t' | '\n')) {
2018 self.bump();
2019 }
2020 if self.cur() == Some('{') {
2021 self.read_group_raw()
2022 } else if self.cur() == Some('\\') {
2023 Some(format!("\\{}", self.consume_control_word()))
2024 } else {
2025 self.bump().map(|c| c.to_string())
2026 }
2027 }
2028}
2029
2030fn flush_buf(buf: &mut String, out: &mut Vec<Inline>) {
2034 if !buf.is_empty() {
2035 out.push(Inline::Str(std::mem::take(buf).into()));
2036 }
2037}
2038
2039fn emit(out: &mut Vec<Inline>, buf: &mut String, inline: Inline) {
2041 flush_buf(buf, out);
2042 out.push(inline);
2043}
2044
2045fn emit_all(out: &mut Vec<Inline>, buf: &mut String, inlines: Vec<Inline>) {
2047 flush_buf(buf, out);
2048 out.extend(inlines);
2049}
2050
2051fn push_whitespace(out: &mut Vec<Inline>, ws: Inline) {
2054 match out.last() {
2055 Some(Inline::Space) if matches!(ws, Inline::SoftBreak) => {
2057 if let Some(last) = out.last_mut() {
2058 *last = Inline::SoftBreak;
2059 }
2060 }
2061 Some(Inline::LineBreak | Inline::SoftBreak | Inline::Space) => {}
2063 _ => out.push(ws),
2064 }
2065}
2066
2067fn trim_inlines(mut inlines: Vec<Inline>) -> Vec<Inline> {
2069 while matches!(inlines.first(), Some(Inline::Space | Inline::SoftBreak)) {
2070 inlines.remove(0);
2071 }
2072 while matches!(inlines.last(), Some(Inline::Space | Inline::SoftBreak)) {
2073 inlines.pop();
2074 }
2075 inlines
2076}
2077
2078fn section_intrinsic(name: &str) -> Option<i32> {
2081 match name {
2082 "part" => Some(-1),
2083 "chapter" => Some(0),
2084 "section" => Some(1),
2085 "subsection" => Some(2),
2086 "subsubsection" => Some(3),
2087 "paragraph" => Some(4),
2088 "subparagraph" => Some(5),
2089 _ => None,
2090 }
2091}
2092
2093fn math_env(env: &str) -> bool {
2095 matches!(
2096 env,
2097 "math"
2098 | "displaymath"
2099 | "equation"
2100 | "equation*"
2101 | "align"
2102 | "align*"
2103 | "alignat"
2104 | "alignat*"
2105 | "gather"
2106 | "gather*"
2107 | "multline"
2108 | "multline*"
2109 | "flalign"
2110 | "flalign*"
2111 | "eqnarray"
2112 | "eqnarray*"
2113 | "split"
2114 | "cases"
2115 )
2116}
2117
2118fn inline_wrapper(name: &str) -> Option<fn(Vec<Inline>) -> Inline> {
2120 match name {
2121 "emph" | "textit" | "textsl" | "italic" | "emphasize" => Some(Inline::Emph),
2122 "textbf" | "strong" => Some(Inline::Strong),
2123 "underline" | "uline" => Some(Inline::Underline),
2124 "textsc" => Some(Inline::SmallCaps),
2125 "sout" | "st" => Some(Inline::Strikeout),
2126 _ => None,
2127 }
2128}
2129
2130fn font_span_class(name: &str) -> Option<&'static str> {
2133 match name {
2134 "textrm" | "textnormal" => Some("roman"),
2135 "textsf" => Some("sans-serif"),
2136 "textup" => Some("upright"),
2137 "textmd" => Some("medium"),
2138 _ => None,
2139 }
2140}
2141
2142fn extract_spaces<F: FnOnce(Vec<Inline>) -> Inline>(
2145 mut inner: Vec<Inline>,
2146 wrap: F,
2147) -> Vec<Inline> {
2148 let leading =
2149 matches!(inner.first(), Some(Inline::Space | Inline::SoftBreak)).then(|| inner.remove(0));
2150 let trailing = if matches!(inner.last(), Some(Inline::Space | Inline::SoftBreak)) {
2151 inner.pop()
2152 } else {
2153 None
2154 };
2155 let mut result = Vec::new();
2156 result.extend(leading);
2157 result.push(wrap(inner));
2158 result.extend(trailing);
2159 result
2160}
2161
2162fn span_class(inlines: Vec<Inline>, class: &str) -> Inline {
2164 Inline::Span(
2165 Box::new(Attr {
2166 id: carta_ast::Text::default(),
2167 classes: vec![class.into()],
2168 attributes: Vec::new(),
2169 }),
2170 inlines,
2171 )
2172}
2173
2174fn reference_link(name: &str, target: &str) -> Inline {
2176 let kind = match name {
2177 "autoref" | "cref" => "ref+label",
2178 "Cref" => "ref+Label",
2179 other => other,
2180 };
2181 Inline::Link(
2182 Box::new(Attr {
2183 id: carta_ast::Text::default(),
2184 classes: Vec::new(),
2185 attributes: vec![
2186 ("reference-type".into(), kind.into()),
2187 ("reference".into(), target.into()),
2188 ],
2189 }),
2190 vec![Inline::Str(format!("[{target}]").into())],
2191 Box::new(Target {
2192 url: format!("#{target}").into(),
2193 title: carta_ast::Text::default(),
2194 }),
2195 )
2196}
2197
2198#[derive(Clone, Copy)]
2200enum Switch {
2201 Strong,
2202 Emph,
2203 SmallCaps,
2204 Code,
2205}
2206
2207impl Switch {
2208 fn wrap(self, inner: Vec<Inline>) -> Inline {
2209 match self {
2210 Switch::Strong => Inline::Strong(inner),
2211 Switch::Emph => Inline::Emph(inner),
2212 Switch::SmallCaps => Inline::SmallCaps(inner),
2213 Switch::Code => Inline::Code(Box::default(), to_plain_text(&inner).into()),
2214 }
2215 }
2216}
2217
2218fn switch_kind(name: &str) -> Option<Switch> {
2219 Some(match name {
2220 "bf" | "bfseries" => Switch::Strong,
2221 "it" | "itshape" | "em" | "sl" | "slshape" => Switch::Emph,
2222 "scshape" => Switch::SmallCaps,
2223 "tt" => Switch::Code,
2224 _ => return None,
2225 })
2226}
2227
2228fn group_span(inner: Vec<Inline>) -> Option<Inline> {
2231 match inner.first() {
2232 None => None,
2233 Some(Inline::Span(attr, _))
2234 if inner.len() == 1
2235 && attr.id.is_empty()
2236 && attr.classes.is_empty()
2237 && attr.attributes.is_empty() =>
2238 {
2239 inner.into_iter().next()
2240 }
2241 _ => Some(Inline::Span(Box::default(), inner)),
2242 }
2243}
2244
2245#[derive(Clone, Copy)]
2247enum Accent {
2248 Acute,
2249 Grave,
2250 Circumflex,
2251 Tilde,
2252 Diaeresis,
2253 Macron,
2254 DotAbove,
2255 Cedilla,
2256 Caron,
2257 Breve,
2258 DoubleAcute,
2259 Ring,
2260 Ogonek,
2261}
2262
2263fn word_accent(name: &str) -> Option<Accent> {
2266 match name {
2267 "c" => Some(Accent::Cedilla),
2268 "v" => Some(Accent::Caron),
2269 "u" => Some(Accent::Breve),
2270 "H" => Some(Accent::DoubleAcute),
2271 "r" => Some(Accent::Ring),
2272 "k" => Some(Accent::Ogonek),
2273 _ => None,
2274 }
2275}
2276
2277fn combining_mark(accent: Accent) -> char {
2279 match accent {
2280 Accent::Acute => '\u{301}',
2281 Accent::Grave => '\u{300}',
2282 Accent::Circumflex => '\u{302}',
2283 Accent::Tilde => '\u{303}',
2284 Accent::Diaeresis => '\u{308}',
2285 Accent::Macron => '\u{304}',
2286 Accent::DotAbove => '\u{307}',
2287 Accent::Cedilla => '\u{327}',
2288 Accent::Caron => '\u{30c}',
2289 Accent::Breve => '\u{306}',
2290 Accent::DoubleAcute => '\u{30b}',
2291 Accent::Ring => '\u{30a}',
2292 Accent::Ogonek => '\u{328}',
2293 }
2294}
2295
2296fn resolve_accent_base(raw: &str) -> String {
2299 if let Some(rest) = raw.strip_prefix('\\') {
2300 let word: String = rest.chars().take_while(char::is_ascii_alphabetic).collect();
2301 if !word.is_empty() {
2302 return symbol_text(&word).map_or(word, str::to_owned);
2303 }
2304 }
2305 raw.to_owned()
2306}
2307
2308fn apply_accent(accent: Accent, base: Option<&str>) -> String {
2311 let base = base.unwrap_or("");
2312 let mut chars = base.chars();
2313 let Some(first) = chars.next() else {
2314 return String::new();
2315 };
2316 let rest: String = chars.collect();
2317 match combine_accent(accent, first) {
2318 Some(composed) => format!("{composed}{rest}"),
2319 None => format!("{first}{}{rest}", combining_mark(accent)),
2320 }
2321}
2322
2323#[allow(clippy::too_many_lines)]
2325fn combine_accent(accent: Accent, base: char) -> Option<char> {
2326 let table: &[(char, char)] = match accent {
2327 Accent::Acute => &[
2328 ('a', 'á'),
2329 ('e', 'é'),
2330 ('i', 'í'),
2331 ('o', 'ó'),
2332 ('u', 'ú'),
2333 ('y', 'ý'),
2334 ('c', 'ć'),
2335 ('n', 'ń'),
2336 ('s', 'ś'),
2337 ('z', 'ź'),
2338 ('r', 'ŕ'),
2339 ('l', 'ĺ'),
2340 ('A', 'Á'),
2341 ('E', 'É'),
2342 ('I', 'Í'),
2343 ('O', 'Ó'),
2344 ('U', 'Ú'),
2345 ('Y', 'Ý'),
2346 ('C', 'Ć'),
2347 ('N', 'Ń'),
2348 ('S', 'Ś'),
2349 ('Z', 'Ź'),
2350 ],
2351 Accent::Grave => &[
2352 ('a', 'à'),
2353 ('e', 'è'),
2354 ('i', 'ì'),
2355 ('o', 'ò'),
2356 ('u', 'ù'),
2357 ('A', 'À'),
2358 ('E', 'È'),
2359 ('I', 'Ì'),
2360 ('O', 'Ò'),
2361 ('U', 'Ù'),
2362 ],
2363 Accent::Circumflex => &[
2364 ('a', 'â'),
2365 ('e', 'ê'),
2366 ('i', 'î'),
2367 ('o', 'ô'),
2368 ('u', 'û'),
2369 ('A', 'Â'),
2370 ('E', 'Ê'),
2371 ('I', 'Î'),
2372 ('O', 'Ô'),
2373 ('U', 'Û'),
2374 ],
2375 Accent::Tilde => &[
2376 ('a', 'ã'),
2377 ('o', 'õ'),
2378 ('n', 'ñ'),
2379 ('A', 'Ã'),
2380 ('O', 'Õ'),
2381 ('N', 'Ñ'),
2382 ],
2383 Accent::Diaeresis => &[
2384 ('a', 'ä'),
2385 ('e', 'ë'),
2386 ('i', 'ï'),
2387 ('o', 'ö'),
2388 ('u', 'ü'),
2389 ('y', 'ÿ'),
2390 ('A', 'Ä'),
2391 ('E', 'Ë'),
2392 ('I', 'Ï'),
2393 ('O', 'Ö'),
2394 ('U', 'Ü'),
2395 ],
2396 Accent::Macron => &[
2397 ('a', 'ā'),
2398 ('e', 'ē'),
2399 ('i', 'ī'),
2400 ('o', 'ō'),
2401 ('u', 'ū'),
2402 ('A', 'Ā'),
2403 ('E', 'Ē'),
2404 ('I', 'Ī'),
2405 ('O', 'Ō'),
2406 ('U', 'Ū'),
2407 ],
2408 Accent::DotAbove => &[('e', 'ė'), ('z', 'ż'), ('E', 'Ė'), ('Z', 'Ż')],
2409 Accent::Cedilla => &[
2410 ('c', 'ç'),
2411 ('s', 'ş'),
2412 ('t', 'ţ'),
2413 ('g', 'ģ'),
2414 ('C', 'Ç'),
2415 ('S', 'Ş'),
2416 ('T', 'Ţ'),
2417 ],
2418 Accent::Caron => &[
2419 ('c', 'č'),
2420 ('s', 'š'),
2421 ('z', 'ž'),
2422 ('r', 'ř'),
2423 ('e', 'ě'),
2424 ('n', 'ň'),
2425 ('d', 'ď'),
2426 ('t', 'ť'),
2427 ('l', 'ľ'),
2428 ('C', 'Č'),
2429 ('S', 'Š'),
2430 ('Z', 'Ž'),
2431 ('R', 'Ř'),
2432 ('E', 'Ě'),
2433 ('N', 'Ň'),
2434 ],
2435 Accent::Breve => &[
2436 ('a', 'ă'),
2437 ('e', 'ĕ'),
2438 ('g', 'ğ'),
2439 ('i', 'ĭ'),
2440 ('o', 'ŏ'),
2441 ('u', 'ŭ'),
2442 ('A', 'Ă'),
2443 ('G', 'Ğ'),
2444 ],
2445 Accent::DoubleAcute => &[('o', 'ő'), ('u', 'ű'), ('O', 'Ő'), ('U', 'Ű')],
2446 Accent::Ring => &[('a', 'å'), ('u', 'ů'), ('A', 'Å'), ('U', 'Ů')],
2447 Accent::Ogonek => &[
2448 ('a', 'ą'),
2449 ('e', 'ę'),
2450 ('i', 'į'),
2451 ('u', 'ų'),
2452 ('A', 'Ą'),
2453 ('E', 'Ę'),
2454 ],
2455 };
2456 table.iter().find(|(b, _)| *b == base).map(|(_, c)| *c)
2457}
2458
2459fn symbol_text(name: &str) -> Option<&'static str> {
2461 let text = match name {
2462 "LaTeX" => "LaTeX",
2463 "TeX" => "TeX",
2464 "ldots" | "dots" => "\u{2026}",
2465 "textbackslash" => "\\",
2466 "textasciitilde" => "~",
2467 "textasciicircum" => "^",
2468 "textless" => "<",
2469 "textgreater" => ">",
2470 "textbullet" => "\u{2022}",
2471 "textquoteright" => "\u{2019}",
2472 "textquoteleft" => "\u{2018}",
2473 "textquotedblright" => "\u{201d}",
2474 "textquotedblleft" => "\u{201c}",
2475 "textregistered" => "\u{ae}",
2476 "textcopyright" | "copyright" => "\u{a9}",
2477 "textdegree" => "\u{b0}",
2478 "textdagger" => "\u{2020}",
2479 "S" | "textsection" => "\u{a7}",
2480 "P" | "textparagraph" => "\u{b6}",
2481 "pounds" | "textsterling" => "\u{a3}",
2482 "euro" => "\u{20ac}",
2483 "textyen" => "\u{a5}",
2484 "guillemotleft" => "\u{ab}",
2485 "guillemotright" => "\u{bb}",
2486 "aa" => "\u{e5}",
2487 "AA" => "\u{c5}",
2488 "ae" => "\u{e6}",
2489 "AE" => "\u{c6}",
2490 "oe" => "\u{153}",
2491 "OE" => "\u{152}",
2492 "o" => "\u{f8}",
2493 "O" => "\u{d8}",
2494 "ss" => "\u{df}",
2495 "l" => "\u{142}",
2496 "L" => "\u{141}",
2497 "i" => "\u{131}",
2498 "j" => "\u{237}",
2499 "textquotesingle" => "'",
2500 "textquotedbl" => "\"",
2501 "slash" => "/",
2502 "&" => "&",
2503 _ => return None,
2504 };
2505 Some(text)
2506}
2507
2508fn command_arg_count(name: &str) -> usize {
2510 match name {
2511 "setlength" | "addtolength" | "setcounter" | "addtocounter" | "settowidth"
2512 | "definecolor" | "rule" | "newtheorem" => 2,
2513 "hspace" | "vspace" | "hskip" | "vskip" | "vphantom" | "hphantom" | "phantom"
2514 | "raisebox" | "pagestyle" | "thispagestyle" | "pagenumbering" | "documentclass"
2515 | "usepackage" | "RequirePackage" | "geometry" | "hypersetup" | "bibliographystyle"
2516 | "include" | "input" | "graphicspath" | "theoremstyle" | "captionsetup"
2517 | "bibliography" => 1,
2518 _ => 0,
2519 }
2520}
2521
2522fn split_on_command(raw: &str, name: &str) -> Vec<String> {
2524 let marker: Vec<char> = format!("\\{name}").chars().collect();
2525 let chars: Vec<char> = raw.chars().collect();
2526 let mut parts = Vec::new();
2527 let mut current = String::new();
2528 let mut depth = 0i32;
2529 let mut i = 0;
2530 while let Some(&c) = chars.get(i) {
2531 match c {
2532 '{' => depth += 1,
2533 '}' => {
2534 if depth > 0 {
2535 depth -= 1;
2536 }
2537 }
2538 '\\' if depth == 0 => {
2539 let matches_marker = marker
2540 .iter()
2541 .enumerate()
2542 .all(|(k, m)| chars.get(i + k) == Some(m));
2543 let after = chars.get(i + marker.len());
2544 let is_word_boundary = after.is_none_or(|c| !c.is_ascii_alphabetic());
2545 if matches_marker && is_word_boundary {
2546 parts.push(std::mem::take(&mut current));
2547 i += marker.len();
2548 continue;
2549 }
2550 }
2551 _ => {}
2552 }
2553 current.push(c);
2554 i += 1;
2555 }
2556 parts.push(current);
2557 parts
2558}
2559
2560fn demote_image_para(block: Block) -> Block {
2563 if let Block::Para(inlines) = &block {
2564 let has_image = inlines.iter().any(|i| matches!(i, Inline::Image(..)));
2565 let only_images = inlines.iter().all(|i| {
2566 matches!(
2567 i,
2568 Inline::Image(..) | Inline::Space | Inline::SoftBreak | Inline::LineBreak
2569 )
2570 });
2571 if has_image
2572 && only_images
2573 && let Block::Para(inlines) = block
2574 {
2575 return Block::Plain(inlines);
2576 }
2577 }
2578 block
2579}
2580
2581fn substitute_macro(body: &str, args: &[String]) -> String {
2583 let mut out = String::new();
2584 let mut chars = body.chars().peekable();
2585 while let Some(c) = chars.next() {
2586 if c == '#' {
2587 match chars.peek() {
2588 Some(d) if matches!(d, '1'..='9') => {
2589 let idx = (*d as usize) - ('1' as usize);
2590 chars.next();
2591 if let Some(arg) = args.get(idx) {
2592 out.push_str(arg);
2593 }
2594 continue;
2595 }
2596 _ => {}
2597 }
2598 }
2599 out.push(c);
2600 }
2601 out
2602}
2603
2604fn unescape_url(url: &str) -> String {
2606 let mut out = String::new();
2607 let mut chars = url.chars();
2608 while let Some(c) = chars.next() {
2609 if c == '\\' {
2610 match chars.clone().next() {
2611 Some(n) if matches!(n, '%' | '#' | '_' | '&' | '{' | '}' | '$' | '~') => {
2612 out.push(n);
2613 chars.next();
2614 }
2615 _ => out.push('\\'),
2616 }
2617 } else {
2618 out.push(c);
2619 }
2620 }
2621 out
2622}
2623
2624fn parse_key_values(text: &str) -> Vec<(String, String)> {
2627 let mut pairs = Vec::new();
2628 for part in split_top_level(text, ',') {
2629 let part = part.trim();
2630 if part.is_empty() {
2631 continue;
2632 }
2633 match part.split_once('=') {
2634 Some((k, v)) => pairs.push((k.trim().to_owned(), v.trim().to_owned())),
2635 None => pairs.push((part.to_owned(), String::new())),
2636 }
2637 }
2638 pairs
2639}
2640
2641fn image_attributes(opts: &str) -> Vec<(String, String)> {
2644 let mut attrs = Vec::new();
2645 for (key, value) in parse_key_values(opts) {
2646 if key != "width" && key != "height" {
2647 continue;
2648 }
2649 let value = latex_length_to_percent(&value).unwrap_or(value);
2650 attrs.push((key, value));
2651 }
2652 attrs
2653}
2654
2655fn latex_length_to_percent(value: &str) -> Option<String> {
2658 let value = value.trim();
2659 let number = ["\\textwidth", "\\linewidth", "\\textheight"]
2660 .into_iter()
2661 .find_map(|unit| value.strip_suffix(unit))?
2662 .trim();
2663 if !number.starts_with(|c: char| c.is_ascii_digit()) {
2664 return None;
2665 }
2666 let (int_part, frac_part) = number.split_once('.').unwrap_or((number, ""));
2667 if int_part.is_empty()
2668 || !int_part.bytes().all(|b| b.is_ascii_digit())
2669 || !frac_part.bytes().all(|b| b.is_ascii_digit())
2670 {
2671 return None;
2672 }
2673 let mut digits = format!("{int_part}{frac_part}");
2675 let point = int_part.len() + 2;
2676 while digits.len() < point {
2677 digits.push('0');
2678 }
2679 let (whole, frac) = digits.split_at_checked(point)?;
2680 let whole = whole.trim_start_matches('0');
2681 let whole = if whole.is_empty() { "0" } else { whole };
2682 let frac = frac.trim_end_matches('0');
2683 Some(if frac.is_empty() {
2684 format!("{whole}%")
2685 } else {
2686 format!("{whole}.{frac}%")
2687 })
2688}
2689
2690fn split_top_level(text: &str, sep: char) -> Vec<String> {
2692 let mut parts = Vec::new();
2693 let mut depth = 0i32;
2694 let mut current = String::new();
2695 for c in text.chars() {
2696 match c {
2697 '{' => {
2698 depth += 1;
2699 current.push(c);
2700 }
2701 '}' => {
2702 if depth > 0 {
2703 depth -= 1;
2704 }
2705 current.push(c);
2706 }
2707 _ if c == sep && depth == 0 => {
2708 parts.push(std::mem::take(&mut current));
2709 }
2710 _ => current.push(c),
2711 }
2712 }
2713 parts.push(current);
2714 parts
2715}
2716
2717fn parse_column_spec(spec: &str) -> Vec<Alignment> {
2720 let mut aligns = Vec::new();
2721 let chars: Vec<char> = spec.chars().collect();
2722 let mut i = 0;
2723 while let Some(&c) = chars.get(i) {
2724 match c {
2725 'l' => aligns.push(Alignment::AlignLeft),
2726 'r' => aligns.push(Alignment::AlignRight),
2727 'c' => aligns.push(Alignment::AlignCenter),
2728 'p' | 'm' | 'b' | 'X' => {
2729 aligns.push(Alignment::AlignLeft);
2730 i = skip_brace_group(&chars, i + 1);
2731 continue;
2732 }
2733 '@' | '!' | '>' | '<' => {
2734 i = skip_brace_group(&chars, i + 1);
2735 continue;
2736 }
2737 '*' => {
2738 i = skip_brace_group(&chars, i + 1);
2740 i = skip_brace_group(&chars, i);
2741 continue;
2742 }
2743 _ => {}
2744 }
2745 i += 1;
2746 }
2747 aligns
2748}
2749
2750fn skip_brace_group(chars: &[char], start: usize) -> usize {
2752 let mut i = start;
2753 while matches!(chars.get(i), Some(' ')) {
2754 i += 1;
2755 }
2756 if chars.get(i) != Some(&'{') {
2757 return i;
2758 }
2759 let mut depth = 0i32;
2760 while let Some(&c) = chars.get(i) {
2761 match c {
2762 '{' => depth += 1,
2763 '}' => {
2764 depth -= 1;
2765 if depth == 0 {
2766 return i + 1;
2767 }
2768 }
2769 _ => {}
2770 }
2771 i += 1;
2772 }
2773 i
2774}
2775
2776fn build_table(parser: &Parser, aligns: &[Alignment], body: &str) -> Block {
2779 let col_specs: Vec<ColSpec> = aligns
2780 .iter()
2781 .map(|a| ColSpec {
2782 align: a.clone(),
2783 width: ColWidth::ColWidthDefault,
2784 })
2785 .collect();
2786 let ncols = col_specs.len();
2787
2788 let mut rows: Vec<Row> = Vec::new();
2790 let mut rule_pending = false;
2791 let mut first_row_ruled = false;
2792 for chunk in split_top_level(body, '\n').join(" ").split("\\\\") {
2793 let (leading_rule, content) = strip_leading_rules(chunk);
2794 if content.trim().is_empty() {
2795 rule_pending |= leading_rule;
2796 continue;
2797 }
2798 rule_pending |= leading_rule;
2799 if rows.len() == 1 && rule_pending {
2800 first_row_ruled = true;
2801 }
2802 rule_pending = false;
2803 rows.push(build_row(parser, &strip_rules(&content), ncols));
2804 }
2805 if rows.len() == 1 && rule_pending {
2807 first_row_ruled = true;
2808 }
2809
2810 let (head_rows, body_rows) = if first_row_ruled && !rows.is_empty() {
2811 let body = rows.split_off(1);
2812 (rows, body)
2813 } else {
2814 (Vec::new(), rows)
2815 };
2816
2817 let table = Table {
2818 attr: Attr::default(),
2819 caption: Caption::default(),
2820 col_specs,
2821 head: TableHead {
2822 attr: Attr::default(),
2823 rows: head_rows,
2824 },
2825 bodies: vec![TableBody {
2826 attr: Attr::default(),
2827 row_head_columns: 0,
2828 head: Vec::new(),
2829 body: body_rows,
2830 }],
2831 foot: TableFoot::default(),
2832 };
2833 Block::Table(Box::new(table))
2834}
2835
2836fn build_row(parser: &Parser, source: &str, ncols: usize) -> Row {
2839 let mut cells = Vec::new();
2840 let mut span_total: i32 = 0;
2841 for cell_src in split_top_level(source, '&') {
2842 let trimmed = cell_src.trim();
2843 let (align, col_span, content_src) = match parse_multicolumn(trimmed) {
2844 Some((n, align, content)) => (align, n, content),
2845 None => (Alignment::AlignDefault, 1, trimmed.to_owned()),
2846 };
2847 let inlines = parse_cell_inlines(parser, content_src.trim());
2848 let content = if inlines.is_empty() {
2849 Vec::new()
2850 } else {
2851 vec![Block::Plain(inlines)]
2852 };
2853 cells.push(Cell {
2854 attr: Attr::default(),
2855 align,
2856 row_span: 1,
2857 col_span,
2858 content,
2859 });
2860 span_total += col_span.max(1);
2861 }
2862 while span_total < i32::try_from(ncols).unwrap_or(i32::MAX) {
2863 cells.push(Cell {
2864 attr: Attr::default(),
2865 align: Alignment::AlignDefault,
2866 row_span: 1,
2867 col_span: 1,
2868 content: Vec::new(),
2869 });
2870 span_total += 1;
2871 }
2872 Row {
2873 attr: Attr::default(),
2874 cells,
2875 }
2876}
2877
2878fn parse_multicolumn(src: &str) -> Option<(i32, Alignment, String)> {
2881 let rest = src.strip_prefix("\\multicolumn")?;
2882 let chars: Vec<char> = rest.chars().collect();
2883 let (span, next) = read_brace_group(&chars, 0)?;
2884 let (align_spec, next) = read_brace_group(&chars, next)?;
2885 let (content, _) = read_brace_group(&chars, next)?;
2886 let span: i32 = span.trim().parse().ok()?;
2887 if span < 1 {
2888 return None;
2889 }
2890 let align = parse_column_spec(&align_spec)
2891 .into_iter()
2892 .next()
2893 .unwrap_or(Alignment::AlignDefault);
2894 Some((span, align, content))
2895}
2896
2897fn read_brace_group(chars: &[char], start: usize) -> Option<(String, usize)> {
2900 let mut i = start;
2901 while matches!(chars.get(i), Some(' ')) {
2902 i += 1;
2903 }
2904 if chars.get(i) != Some(&'{') {
2905 return None;
2906 }
2907 let mut depth = 0i32;
2908 let mut content = String::new();
2909 while let Some(&c) = chars.get(i) {
2910 match c {
2911 '{' => {
2912 depth += 1;
2913 if depth > 1 {
2914 content.push(c);
2915 }
2916 }
2917 '}' => {
2918 depth -= 1;
2919 if depth == 0 {
2920 return Some((content, i + 1));
2921 }
2922 content.push(c);
2923 }
2924 _ => content.push(c),
2925 }
2926 i += 1;
2927 }
2928 None
2929}
2930
2931fn strip_leading_rules(chunk: &str) -> (bool, String) {
2934 let chars: Vec<char> = chunk.chars().collect();
2935 let mut i = 0;
2936 let mut header_boundary = false;
2937 loop {
2938 while matches!(chars.get(i), Some(c) if c.is_whitespace()) {
2939 i += 1;
2940 }
2941 match rule_command_at(&chars, i) {
2942 Some((end, header)) => {
2943 header_boundary |= header;
2944 i = end;
2945 }
2946 None => break,
2947 }
2948 }
2949 (
2950 header_boundary,
2951 chars.get(i..).unwrap_or(&[]).iter().collect(),
2952 )
2953}
2954
2955fn rule_command_at(chars: &[char], start: usize) -> Option<(usize, bool)> {
2960 if chars.get(start) != Some(&'\\') {
2961 return None;
2962 }
2963 let mut j = start + 1;
2964 let mut name = String::new();
2965 while let Some(&d) = chars.get(j) {
2966 if d.is_ascii_alphabetic() {
2967 name.push(d);
2968 j += 1;
2969 } else {
2970 break;
2971 }
2972 }
2973 if !is_rule_command(&name) {
2974 return None;
2975 }
2976 let header_boundary = !matches!(name.as_str(), "hdashline" | "specialrule");
2977 while matches!(chars.get(j), Some('{' | '[' | '(')) {
2978 j = skip_rule_argument(chars, j);
2979 }
2980 Some((j, header_boundary))
2981}
2982
2983fn is_rule_command(name: &str) -> bool {
2984 matches!(
2985 name,
2986 "hline"
2987 | "toprule"
2988 | "midrule"
2989 | "bottomrule"
2990 | "cmidrule"
2991 | "cline"
2992 | "hdashline"
2993 | "specialrule"
2994 )
2995}
2996
2997fn skip_rule_argument(chars: &[char], start: usize) -> usize {
2999 let close = match chars.get(start) {
3000 Some('{') => '}',
3001 Some('[') => ']',
3002 Some('(') => ')',
3003 _ => return start,
3004 };
3005 let mut i = start + 1;
3006 while let Some(&c) = chars.get(i) {
3007 i += 1;
3008 if c == close {
3009 break;
3010 }
3011 }
3012 i
3013}
3014
3015fn parse_cell_inlines(parser: &Parser, source: &str) -> Vec<Inline> {
3017 let mut sub = parser.child(source, false);
3018 trim_inlines(sub.parse_inlines(InlineStop::Paragraph))
3019}
3020
3021fn strip_rules(row: &str) -> String {
3023 let mut out = String::new();
3024 let chars: Vec<char> = row.chars().collect();
3025 let mut i = 0;
3026 while let Some(&c) = chars.get(i) {
3027 if let Some((end, _)) = rule_command_at(&chars, i) {
3028 i = end;
3029 continue;
3030 }
3031 out.push(c);
3032 i += 1;
3033 }
3034 out
3035}
3036
3037#[cfg(test)]
3038mod tests {
3039 use super::*;
3040
3041 fn latex_defaults() -> Extensions {
3045 Extensions::from_list(&[
3046 Extension::Smart,
3047 Extension::AutoIdentifiers,
3048 Extension::LatexMacros,
3049 ])
3050 }
3051
3052 fn parse(input: &str) -> Vec<Block> {
3053 parse_ext(input, latex_defaults())
3054 }
3055
3056 fn parse_ext(input: &str, extensions: Extensions) -> Vec<Block> {
3057 let mut options = ReaderOptions::default();
3058 options.extensions = extensions;
3059 LatexReader
3060 .read(input, &options)
3061 .expect("latex reader does not fail")
3062 .blocks
3063 }
3064
3065 fn attr(attributes: Vec<(String, String)>) -> Attr {
3066 Attr {
3067 id: carta_ast::Text::default(),
3068 classes: Vec::new(),
3069 attributes: attributes
3070 .into_iter()
3071 .map(|(k, v)| (k.into(), v.into()))
3072 .collect(),
3073 }
3074 }
3075
3076 #[test]
3079 fn substitute_macro_preserves_non_parameter_hash_zero() {
3080 let expanded = substitute_macro("a#0b", &["Y".to_owned()]);
3081 assert_eq!(expanded, "a#0b");
3082 }
3083
3084 #[test]
3087 fn equation_environment_is_display_math_with_verbatim_body() {
3088 let blocks = parse("\\begin{equation}\n f(x) = x + 1\n\\end{equation}\n");
3089 assert_eq!(
3090 blocks,
3091 vec![Block::Para(vec![Inline::Math(
3092 MathType::DisplayMath,
3093 "\\begin{equation}\n f(x) = x + 1\n\\end{equation}"
3094 .to_owned()
3095 .into(),
3096 )])],
3097 );
3098 }
3099
3100 #[test]
3104 fn ref_becomes_tagged_link_with_bracketed_label() {
3105 let blocks = parse("See Section~\\ref{sec:intro}.\n");
3106 assert_eq!(
3107 blocks,
3108 vec![Block::Para(vec![
3109 Inline::Str("See".to_owned().into()),
3110 Inline::Space,
3111 Inline::Str("Section\u{a0}".to_owned().into()),
3112 Inline::Link(
3113 Box::new(attr(vec![
3114 ("reference-type".to_owned(), "ref".to_owned()),
3115 ("reference".to_owned(), "sec:intro".to_owned()),
3116 ])),
3117 vec![Inline::Str("[sec:intro]".to_owned().into())],
3118 Box::new(Target {
3119 url: "#sec:intro".to_owned().into(),
3120 title: carta_ast::Text::default(),
3121 }),
3122 ),
3123 Inline::Str(".".to_owned().into()),
3124 ])],
3125 );
3126 }
3127
3128 #[test]
3131 fn cref_variants_carry_their_reference_kind() {
3132 let kinds = |input: &str| match parse(input).into_iter().next() {
3133 Some(Block::Para(inlines)) => inlines
3134 .into_iter()
3135 .filter_map(|inline| match inline {
3136 Inline::Link(attr, _, _) => attr
3137 .attributes
3138 .into_iter()
3139 .find(|(k, _)| k == "reference-type")
3140 .map(|(_, v)| v),
3141 _ => None,
3142 })
3143 .collect::<Vec<_>>(),
3144 _ => Vec::new(),
3145 };
3146 assert_eq!(kinds("\\cref{a}"), vec!["ref+label".to_owned()]);
3147 assert_eq!(kinds("\\autoref{a}"), vec!["ref+label".to_owned()]);
3148 assert_eq!(kinds("\\Cref{a}"), vec!["ref+Label".to_owned()]);
3149 }
3150
3151 #[test]
3154 fn includegraphics_textwidth_fraction_becomes_percent_width() {
3155 let blocks = parse("x \\includegraphics[width=0.5\\textwidth]{diagram.png} y\n");
3156 assert_eq!(
3157 blocks,
3158 vec![Block::Para(vec![
3159 Inline::Str("x".to_owned().into()),
3160 Inline::Space,
3161 Inline::Image(
3162 Box::new(attr(vec![("width".to_owned(), "50%".to_owned())])),
3163 vec![Inline::Str("image".to_owned().into())],
3164 Box::new(Target {
3165 url: "diagram.png".to_owned().into(),
3166 title: carta_ast::Text::default(),
3167 }),
3168 ),
3169 Inline::Space,
3170 Inline::Str("y".to_owned().into()),
3171 ])],
3172 );
3173 }
3174
3175 #[test]
3178 fn macro_definition_preserved_verbatim_when_expansion_disabled() {
3179 let ext = Extensions::from_list(&[Extension::Smart, Extension::AutoIdentifiers]);
3180 let blocks = parse_ext("\\newcommand{\\foo}{bar}\n", ext);
3181 assert_eq!(
3182 blocks,
3183 vec![Block::RawBlock(
3184 Format("latex".to_owned().into()),
3185 "\\newcommand{\\foo}{bar}".to_owned().into(),
3186 )],
3187 );
3188 }
3189
3190 fn plain_text(input: &str) -> String {
3192 parse(input)
3193 .iter()
3194 .filter_map(|block| match block {
3195 Block::Para(inlines) => Some(to_plain_text(inlines)),
3196 _ => None,
3197 })
3198 .collect::<Vec<_>>()
3199 .join("\n")
3200 }
3201
3202 #[test]
3203 fn simple_macro_expands_to_its_body() {
3204 assert_eq!(plain_text("\\newcommand{\\x}{Y}\n\n\\x\n"), "Y");
3205 }
3206
3207 #[test]
3208 fn macro_optional_argument_defaults_when_absent() {
3209 assert_eq!(
3210 plain_text("\\newcommand{\\x}[2][d]{#1#2}\n\n\\x{B}\n"),
3211 "dB"
3212 );
3213 assert_eq!(
3214 plain_text("\\newcommand{\\x}[2][d]{#1#2}\n\n\\x[A]{B}\n"),
3215 "AB",
3216 );
3217 }
3218
3219 #[test]
3220 fn macro_body_invoking_another_macro_expands_fully() {
3221 assert_eq!(
3222 plain_text("\\newcommand{\\a}{\\b}\n\\newcommand{\\b}{Z}\n\n\\a\n"),
3223 "Z",
3224 );
3225 }
3226
3227 #[test]
3230 fn nested_invocations_do_not_accumulate_depth() {
3231 let mut source = String::from("\\newcommand{\\a}{\\b}\n\\newcommand{\\b}{Z}\n\n");
3232 for _ in 0..300 {
3233 source.push_str("\\a ");
3234 }
3235 assert_eq!(plain_text(&source).matches('Z').count(), 300);
3236 }
3237
3238 #[test]
3240 fn many_sequential_invocations_all_expand() {
3241 let mut source = String::from("\\newcommand{\\hi}{Hello}\n\n");
3242 for _ in 0..300 {
3243 source.push_str("\\hi ");
3244 }
3245 assert_eq!(plain_text(&source).matches("Hello").count(), 300);
3246 }
3247
3248 #[test]
3250 fn self_recursive_macro_terminates() {
3251 let _ = parse("\\newcommand{\\x}{\\x}\n\n\\x\n");
3252 }
3253
3254 #[test]
3257 fn expansion_completed_by_following_source_matches_flattened() {
3258 assert_eq!(
3259 parse("\\newcommand{\\bo}{\\textbf}\n\n\\bo{word}\n"),
3260 parse("\\textbf{word}\n"),
3261 );
3262 }
3263
3264 #[test]
3267 fn expansion_ending_at_environment_boundary_matches_flattened() {
3268 assert_eq!(
3269 parse("\\newcommand{\\c}{content}\n\n\\begin{quote}\\c\\end{quote}\n"),
3270 parse("\\begin{quote}content\\end{quote}\n"),
3271 );
3272 }
3273}