1use std::collections::BTreeMap;
6
7use typub_ir::{
8 AdmonitionKind, Asset, AssetId, AssetSource, AssetVariant, AttrMap, Block, BlockAttrs,
9 Document, FlowListItem, FlowListItemMarker, FootnoteDef, HeadingLevel, ImageAsset, Inline,
10 List, ListKind, MathSource, RelativePath, RenderPayload, RenderedArtifact, TableCell,
11 TableCellKind, TableRow, TableSection, TableSectionKind, TaskListItem, Url,
12};
13
14pub fn empty_attrs() -> AttrMap {
16 AttrMap::new()
17}
18
19pub fn document(blocks: Vec<Block>) -> Document {
21 Document {
22 blocks,
23 footnotes: BTreeMap::new(),
24 assets: BTreeMap::new(),
25 meta: Default::default(),
26 }
27}
28
29pub fn document_with_assets(
31 blocks: Vec<Block>,
32 assets: impl IntoIterator<Item = (AssetId, Asset)>,
33) -> Document {
34 Document {
35 blocks,
36 footnotes: BTreeMap::new(),
37 assets: assets.into_iter().collect(),
38 meta: Default::default(),
39 }
40}
41
42pub fn document_full(
44 blocks: Vec<Block>,
45 footnotes: impl IntoIterator<Item = (typub_ir::FootnoteId, FootnoteDef)>,
46 assets: impl IntoIterator<Item = (AssetId, Asset)>,
47) -> Document {
48 Document {
49 blocks,
50 footnotes: footnotes.into_iter().collect(),
51 assets: assets.into_iter().collect(),
52 meta: Default::default(),
53 }
54}
55
56pub fn paragraph_text(text: &str) -> Block {
58 Block::Paragraph {
59 content: vec![Inline::Text(text.to_string())],
60 attrs: BlockAttrs::default(),
61 }
62}
63
64pub fn paragraph(content: Vec<Inline>) -> Block {
66 Block::Paragraph {
67 content,
68 attrs: BlockAttrs::default(),
69 }
70}
71
72pub fn heading_text(level: u8, text: &str) -> Block {
76 let normalized = level.clamp(1, 6);
77 let heading_level = match HeadingLevel::new(normalized) {
78 Ok(v) => v,
79 Err(_) => {
80 return paragraph_text(text);
82 }
83 };
84
85 Block::Heading {
86 level: heading_level,
87 id: None,
88 content: vec![Inline::Text(text.to_string())],
89 attrs: BlockAttrs::default(),
90 }
91}
92
93pub fn quote_text(text: &str) -> Block {
95 Block::Quote {
96 blocks: vec![paragraph_text(text)],
97 cite: None,
98 attrs: BlockAttrs::default(),
99 }
100}
101
102pub fn code_block(code: &str, language: &str) -> Block {
104 Block::CodeBlock {
105 code: code.to_string(),
106 language: if language.is_empty() {
107 None
108 } else {
109 Some(language.to_string())
110 },
111 filename: None,
112 highlight_lines: Vec::new(),
113 highlighted_html: None,
114 attrs: BlockAttrs::default(),
115 }
116}
117
118pub fn code_block_highlighted(code: &str, highlighted: &str, language: &str) -> Block {
120 Block::CodeBlock {
121 code: code.to_string(),
122 language: if language.is_empty() {
123 None
124 } else {
125 Some(language.to_string())
126 },
127 filename: None,
128 highlight_lines: Vec::new(),
129 highlighted_html: Some(highlighted.to_string()),
130 attrs: BlockAttrs::default(),
131 }
132}
133
134pub fn image(asset_id: &str, src: &str, alt: &str) -> (Block, (AssetId, Asset)) {
138 let id = AssetId(asset_id.to_string());
139 let asset = Asset::Image(ImageAsset {
140 source: AssetSource::RemoteUrl {
141 url: Url(src.to_string()),
142 },
143 meta: None,
144 variants: Vec::new(),
145 });
146
147 let block = Block::Paragraph {
148 content: vec![Inline::Image {
149 asset: typub_ir::AssetRef(id.clone()),
150 alt: alt.to_string(),
151 title: None,
152 attrs: Default::default(),
153 }],
154 attrs: BlockAttrs::default(),
155 };
156
157 (block, (id, asset))
158}
159
160pub fn image_marker(
162 asset_id: &str,
163 path: &str,
164 alt: &str,
165) -> Result<(Block, (AssetId, Asset)), String> {
166 let id = AssetId(asset_id.to_string());
167 let rel = RelativePath::new(path.to_string())?;
168 let asset = Asset::Image(ImageAsset {
169 source: AssetSource::LocalPath { path: rel },
170 meta: None,
171 variants: Vec::new(),
172 });
173
174 let block = Block::Paragraph {
175 content: vec![Inline::Image {
176 asset: typub_ir::AssetRef(id.clone()),
177 alt: alt.to_string(),
178 title: None,
179 attrs: Default::default(),
180 }],
181 attrs: BlockAttrs::default(),
182 };
183
184 Ok((block, (id, asset)))
185}
186
187pub fn divider() -> Block {
189 Block::Divider {
190 attrs: BlockAttrs::default(),
191 }
192}
193
194pub fn list_item(content: Vec<Inline>) -> FlowListItem {
196 FlowListItem {
197 marker: None,
198 blocks: vec![paragraph(content)],
199 }
200}
201
202pub fn bullet_list_text(items: &[&str]) -> Block {
204 let items = items
205 .iter()
206 .map(|s| FlowListItem {
207 marker: Some(FlowListItemMarker::Bullet),
208 blocks: vec![paragraph_text(s)],
209 })
210 .collect();
211
212 Block::List {
213 list: List {
214 kind: ListKind::Bullet { items },
215 },
216 attrs: BlockAttrs::default(),
217 }
218}
219
220pub fn numbered_list_text(items: &[&str]) -> Block {
222 let items = items
223 .iter()
224 .enumerate()
225 .map(|(i, s)| FlowListItem {
226 marker: Some(FlowListItemMarker::Number((i + 1) as u32)),
227 blocks: vec![paragraph_text(s)],
228 })
229 .collect();
230
231 Block::List {
232 list: List {
233 kind: ListKind::Numbered {
234 start: 1,
235 reversed: false,
236 marker: None,
237 items,
238 },
239 },
240 attrs: BlockAttrs::default(),
241 }
242}
243
244pub fn task_item(text: &str, checked: bool) -> TaskListItem {
246 TaskListItem {
247 checked,
248 blocks: vec![paragraph_text(text)],
249 }
250}
251
252pub fn task_list(items: Vec<TaskListItem>) -> Block {
254 Block::List {
255 list: List {
256 kind: ListKind::Task { items },
257 },
258 attrs: BlockAttrs::default(),
259 }
260}
261
262pub fn table_cell(content: Vec<Inline>) -> TableCell {
264 TableCell {
265 kind: TableCellKind::Data,
266 blocks: vec![paragraph(content)],
267 colspan: 1,
268 rowspan: 1,
269 scope: None,
270 align: None,
271 attrs: BlockAttrs::default(),
272 }
273}
274
275pub fn table_text(headers: &[&str], rows: &[Vec<&str>]) -> Block {
277 let head_rows = if headers.is_empty() {
278 Vec::new()
279 } else {
280 vec![TableRow {
281 cells: headers
282 .iter()
283 .map(|h| TableCell {
284 kind: TableCellKind::Header,
285 blocks: vec![paragraph_text(h)],
286 colspan: 1,
287 rowspan: 1,
288 scope: None,
289 align: None,
290 attrs: BlockAttrs::default(),
291 })
292 .collect(),
293 attrs: BlockAttrs::default(),
294 }]
295 };
296
297 let body_rows = rows
298 .iter()
299 .map(|r| TableRow {
300 cells: r
301 .iter()
302 .map(|c| TableCell {
303 kind: TableCellKind::Data,
304 blocks: vec![paragraph_text(c)],
305 colspan: 1,
306 rowspan: 1,
307 scope: None,
308 align: None,
309 attrs: BlockAttrs::default(),
310 })
311 .collect(),
312 attrs: BlockAttrs::default(),
313 })
314 .collect();
315
316 let mut sections = Vec::new();
317 if !head_rows.is_empty() {
318 sections.push(TableSection {
319 kind: TableSectionKind::Head,
320 rows: head_rows,
321 attrs: BlockAttrs::default(),
322 });
323 }
324 sections.push(TableSection {
325 kind: TableSectionKind::Body,
326 rows: body_rows,
327 attrs: BlockAttrs::default(),
328 });
329
330 Block::Table {
331 caption: None,
332 sections,
333 attrs: BlockAttrs::default(),
334 }
335}
336
337pub fn admonition_text(kind: AdmonitionKind, text: &str) -> Block {
339 Block::Admonition {
340 kind,
341 title: None,
342 blocks: vec![paragraph_text(text)],
343 attrs: BlockAttrs::default(),
344 }
345}
346
347pub fn math_inline_latex(src: &str) -> Inline {
349 Inline::MathInline {
350 math: RenderPayload {
351 src: Some(MathSource::Latex(src.to_string())),
352 rendered: None,
353 id: None,
354 },
355 attrs: Default::default(),
356 }
357}
358
359pub fn math_block_latex(src: &str) -> Block {
361 Block::MathBlock {
362 math: RenderPayload {
363 src: Some(MathSource::Latex(src.to_string())),
364 rendered: None,
365 id: None,
366 },
367 attrs: BlockAttrs::default(),
368 }
369}
370
371pub fn svg_inline(svg: &str) -> Inline {
373 Inline::SvgInline {
374 svg: RenderPayload {
375 src: None,
376 rendered: Some(RenderedArtifact::Svg(svg.to_string())),
377 id: None,
378 },
379 attrs: Default::default(),
380 }
381}
382
383pub fn svg_block(svg: &str) -> Block {
385 Block::SvgBlock {
386 svg: RenderPayload {
387 src: None,
388 rendered: Some(RenderedArtifact::Svg(svg.to_string())),
389 id: None,
390 },
391 attrs: BlockAttrs::default(),
392 }
393}
394
395pub fn asset_variant(
397 name: &str,
398 publish_url: &str,
399 width: Option<u32>,
400 height: Option<u32>,
401) -> AssetVariant {
402 AssetVariant {
403 name: name.to_string(),
404 publish_url: Url(publish_url.to_string()),
405 width,
406 height,
407 }
408}