citum_engine/render/
typst.rs1use super::format::OutputFormat;
9use citum_schema::template::WrapPunctuation;
10
11#[derive(Debug, Clone, Default)]
13pub struct Typst;
14
15impl Typst {
16 fn escape_text(input: &str) -> String {
17 let mut escaped = String::with_capacity(input.len());
18 for ch in input.chars() {
19 match ch {
20 '\\' => escaped.push_str("\\\\"),
21 '#' | '[' | ']' | '<' | '>' | '*' | '_' | '@' | '$' => {
22 escaped.push('\\');
23 escaped.push(ch);
24 }
25 _ => escaped.push(ch),
26 }
27 }
28 escaped
29 }
30
31 fn escape_string(input: &str) -> String {
32 let mut escaped = String::with_capacity(input.len());
33 for ch in input.chars() {
34 match ch {
35 '\\' => escaped.push_str("\\\\"),
36 '"' => escaped.push_str("\\\""),
37 _ => escaped.push(ch),
38 }
39 }
40 escaped
41 }
42}
43
44impl OutputFormat for Typst {
45 type Output = String;
46
47 fn text(&self, s: &str) -> Self::Output {
48 Self::escape_text(s)
49 }
50
51 fn join(&self, items: Vec<Self::Output>, delimiter: &str) -> Self::Output {
52 items.join(delimiter)
53 }
54
55 fn finish(&self, output: Self::Output) -> String {
56 output
57 }
58
59 fn emph(&self, content: Self::Output) -> Self::Output {
60 if content.is_empty() {
61 return content;
62 }
63 format!("_{content}_")
64 }
65
66 fn strong(&self, content: Self::Output) -> Self::Output {
67 if content.is_empty() {
68 return content;
69 }
70 format!("*{content}*")
71 }
72
73 fn small_caps(&self, content: Self::Output) -> Self::Output {
74 if content.is_empty() {
75 return content;
76 }
77 format!("#smallcaps[{content}]")
78 }
79
80 fn superscript(&self, content: Self::Output) -> Self::Output {
81 if content.is_empty() {
82 return content;
83 }
84 format!("#super[{content}]")
85 }
86
87 fn quote(&self, content: Self::Output) -> Self::Output {
88 if content.is_empty() {
89 return content;
90 }
91 format!("\u{201C}{content}\u{201D}")
92 }
93
94 fn affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
95 format!("{}{}{}", self.text(prefix), content, self.text(suffix))
96 }
97
98 fn inner_affix(&self, prefix: &str, content: Self::Output, suffix: &str) -> Self::Output {
99 format!("{}{}{}", self.text(prefix), content, self.text(suffix))
100 }
101
102 fn wrap_punctuation(&self, wrap: &WrapPunctuation, content: Self::Output) -> Self::Output {
103 match wrap {
104 WrapPunctuation::Parentheses => format!("({content})"),
105 WrapPunctuation::Brackets => format!("[{content}]"),
106 WrapPunctuation::Quotes => format!("\u{201C}{content}\u{201D}"),
107 }
108 }
109
110 fn semantic(&self, _class: &str, content: Self::Output) -> Self::Output {
111 content
112 }
113
114 fn annotation(&self, content: Self::Output) -> Self::Output {
115 if content.is_empty() {
116 return content;
117 }
118 format!("\n#block(class: \"citum-annotation\")[{}]", content)
119 }
120
121 fn citation(&self, ids: Vec<String>, content: Self::Output) -> Self::Output {
122 if content.is_empty() || ids.len() != 1 {
123 return content;
124 }
125
126 #[allow(clippy::unwrap_used, reason = "length checked")]
127 let id = ids.first().unwrap();
128 format!("#link(<{}>)[{}]", self.format_id(id), content)
129 }
130
131 fn link(&self, url: &str, content: Self::Output) -> Self::Output {
132 if content.is_empty() {
133 return content;
134 }
135
136 if let Some(label) = url.strip_prefix('#') {
137 format!("#link(<{}>)[{}]", self.format_id(label), content)
138 } else {
139 format!(r#"#link("{}")[{}]"#, Self::escape_string(url), content)
140 }
141 }
142
143 fn format_id(&self, id: &str) -> String {
144 let mut normalized = String::with_capacity(id.len() + 4);
145 normalized.push_str("ref-");
146 for ch in id.chars() {
147 if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | ':' | '.') {
148 normalized.push(ch);
149 } else {
150 normalized.push('-');
151 }
152 }
153 normalized
154 }
155
156 fn bibliography(&self, entries: Vec<Self::Output>) -> Self::Output {
157 self.join(entries, "\n\n")
158 }
159
160 fn entry(
161 &self,
162 id: &str,
163 content: Self::Output,
164 url: Option<&str>,
165 _metadata: &super::format::ProcEntryMetadata,
166 ) -> Self::Output {
167 let content = if let Some(u) = url {
168 self.link(u, content)
169 } else {
170 content
171 };
172
173 format!("{} <{}>", content, self.format_id(id))
174 }
175}