1#[derive(Clone, Debug, Default, PartialEq)]
13pub struct Document {
14 pub blocks: Vec<Block>,
15}
16
17impl Document {
18 pub fn plain_text(&self) -> String {
21 let mut out = String::new();
22 for block in &self.blocks {
23 block.push_plain(&mut out);
24 }
25 out.trim_end().to_string()
26 }
27}
28
29#[derive(Clone, Debug, PartialEq)]
31pub enum Block {
32 Heading(Heading),
35 Byline(Vec<Inline>),
37 Paragraph(Vec<Inline>),
39 Lines(Vec<Vec<Inline>>),
42 List(List),
44 Definitions(Vec<Definition>),
46 Code(CodeBlock),
48 Table(TableBlock),
50 Quote {
52 blocks: Vec<Block>,
53 attribution: Option<Vec<Inline>>,
54 },
55 Admonition {
57 kind: AdmonitionKind,
58 title: Option<Vec<Inline>>,
59 blocks: Vec<Block>,
60 },
61 Aside {
63 title: Option<Vec<Inline>>,
64 blocks: Vec<Block>,
65 },
66 Indent { amount: usize, blocks: Vec<Block> },
68 Rule,
70 Image { target: String, alt: String },
72 Properties {
75 name: Option<String>,
76 entries: Vec<(String, String)>,
77 },
78 Banner {
81 left: String,
82 center: String,
83 right: String,
84 },
85 Caption(Vec<Inline>),
87}
88
89impl Block {
90 fn push_plain(&self, out: &mut String) {
91 match self {
92 Block::Heading(heading) => {
93 if let Some(keyword) = &heading.keyword {
94 out.push_str(&keyword.text);
95 out.push(' ');
96 }
97 push_inlines(&heading.content, out);
98 out.push('\n');
99 }
100 Block::Byline(inlines) | Block::Paragraph(inlines) | Block::Caption(inlines) => {
101 push_inlines(inlines, out);
102 out.push('\n');
103 }
104 Block::Lines(lines) => {
105 for line in lines {
106 push_inlines(line, out);
107 out.push('\n');
108 }
109 }
110 Block::List(list) => {
111 for item in &list.items {
112 for block in &item.blocks {
113 block.push_plain(out);
114 }
115 }
116 }
117 Block::Definitions(definitions) => {
118 for definition in definitions {
119 push_inlines(&definition.term, out);
120 out.push('\n');
121 for block in &definition.blocks {
122 block.push_plain(out);
123 }
124 }
125 }
126 Block::Code(code) => {
127 out.push_str(&code.code);
128 out.push('\n');
129 }
130 Block::Table(table) => {
131 for row in table.header.iter().chain(&table.rows) {
132 let cells: Vec<String> = row.iter().map(|cell| inlines_text(cell)).collect();
133 out.push_str(&cells.join(" | "));
134 out.push('\n');
135 }
136 }
137 Block::Quote {
138 blocks,
139 attribution,
140 } => {
141 for block in blocks {
142 block.push_plain(out);
143 }
144 if let Some(attribution) = attribution {
145 push_inlines(attribution, out);
146 out.push('\n');
147 }
148 }
149 Block::Admonition { title, blocks, .. } | Block::Aside { title, blocks } => {
150 if let Some(title) = title {
151 push_inlines(title, out);
152 out.push('\n');
153 }
154 for block in blocks {
155 block.push_plain(out);
156 }
157 }
158 Block::Indent { blocks, .. } => {
159 for block in blocks {
160 block.push_plain(out);
161 }
162 }
163 Block::Rule => out.push_str("---\n"),
164 Block::Image { alt, .. } => {
165 out.push_str(alt);
166 out.push('\n');
167 }
168 Block::Properties { name, entries } => {
169 if let Some(name) = name {
170 out.push_str(name);
171 out.push('\n');
172 }
173 for (key, value) in entries {
174 out.push_str(key);
175 out.push_str(": ");
176 out.push_str(value);
177 out.push('\n');
178 }
179 }
180 Block::Banner {
181 left,
182 center,
183 right,
184 } => {
185 out.push_str(&format!("{left} {center} {right}\n"));
186 }
187 }
188 }
189}
190
191#[derive(Clone, Debug, Default, PartialEq)]
193pub struct Heading {
194 pub level: u8,
196 pub content: Vec<Inline>,
197 pub keyword: Option<Keyword>,
199 pub priority: Option<String>,
201 pub tags: Vec<String>,
203}
204
205#[derive(Clone, Debug, PartialEq, Eq)]
207pub struct Keyword {
208 pub text: String,
209 pub done: bool,
210}
211
212#[derive(Clone, Debug, PartialEq)]
214pub struct List {
215 pub kind: ListKind,
216 pub items: Vec<ListItem>,
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq)]
221pub enum ListKind {
222 Bullet,
224 Ordered { start: u64 },
226 Callout,
229}
230
231#[derive(Clone, Debug, Default, PartialEq)]
234pub struct ListItem {
235 pub checkbox: Option<Checkbox>,
236 pub number: Option<u64>,
239 pub blocks: Vec<Block>,
240}
241
242#[derive(Clone, Copy, Debug, PartialEq, Eq)]
244pub enum Checkbox {
245 Unchecked,
246 Checked,
247 Partial,
249}
250
251#[derive(Clone, Debug, Default, PartialEq)]
253pub struct Definition {
254 pub term: Vec<Inline>,
255 pub blocks: Vec<Block>,
256}
257
258#[derive(Clone, Debug, Default, PartialEq)]
260pub struct CodeBlock {
261 pub language: Option<String>,
264 pub code: String,
265 pub callouts: Vec<Callout>,
267}
268
269#[derive(Clone, Copy, Debug, PartialEq, Eq)]
272pub struct Callout {
273 pub line: usize,
274 pub start: usize,
275 pub end: usize,
276}
277
278#[derive(Clone, Debug, Default, PartialEq)]
280pub struct TableBlock {
281 pub header: Option<Vec<Vec<Inline>>>,
282 pub rows: Vec<Vec<Vec<Inline>>>,
283 pub align: Vec<Align>,
285 pub sections: Vec<usize>,
287 pub title: Option<Vec<Inline>>,
289}
290
291#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
293pub enum Align {
294 #[default]
295 Left,
296 Center,
297 Right,
298}
299
300#[derive(Clone, Debug, PartialEq, Eq)]
302pub enum AdmonitionKind {
303 Note,
304 Tip,
305 Important,
306 Warning,
307 Caution,
308 Other(String),
309}
310
311impl AdmonitionKind {
312 pub fn from_label(label: &str) -> Option<Self> {
315 Some(match label.to_ascii_uppercase().as_str() {
316 "NOTE" => AdmonitionKind::Note,
317 "TIP" | "HINT" => AdmonitionKind::Tip,
318 "IMPORTANT" => AdmonitionKind::Important,
319 "WARNING" => AdmonitionKind::Warning,
320 "CAUTION" | "DANGER" => AdmonitionKind::Caution,
321 _ => return None,
322 })
323 }
324
325 pub fn label(&self) -> String {
327 match self {
328 AdmonitionKind::Note => "Note".into(),
329 AdmonitionKind::Tip => "Tip".into(),
330 AdmonitionKind::Important => "Important".into(),
331 AdmonitionKind::Warning => "Warning".into(),
332 AdmonitionKind::Caution => "Caution".into(),
333 AdmonitionKind::Other(label) => label.clone(),
334 }
335 }
336
337 pub fn style_key(&self) -> &'static str {
339 match self {
340 AdmonitionKind::Note => "document.admonition.note",
341 AdmonitionKind::Tip => "document.admonition.tip",
342 AdmonitionKind::Important => "document.admonition.important",
343 AdmonitionKind::Warning => "document.admonition.warning",
344 AdmonitionKind::Caution => "document.admonition.caution",
345 AdmonitionKind::Other(_) => "document.admonition",
346 }
347 }
348}
349
350#[derive(Clone, Debug, PartialEq)]
352pub enum Inline {
353 Text(String),
354 Emphasis(Vec<Inline>),
356 Strong(Vec<Inline>),
358 Underline(Vec<Inline>),
359 Strike(Vec<Inline>),
361 Code(String),
363 Link {
365 target: String,
366 content: Vec<Inline>,
367 },
368 Image {
370 target: String,
371 alt: String,
372 },
373 LineBreak,
375 Styled {
378 style: String,
379 content: Vec<Inline>,
380 },
381}
382
383impl Inline {
384 pub fn text(text: impl Into<String>) -> Self {
386 Inline::Text(text.into())
387 }
388
389 pub fn styled(style: impl Into<String>, content: Vec<Inline>) -> Self {
391 Inline::Styled {
392 style: style.into(),
393 content,
394 }
395 }
396}
397
398pub fn inlines_text(inlines: &[Inline]) -> String {
400 let mut out = String::new();
401 push_inlines(inlines, &mut out);
402 out
403}
404
405fn push_inlines(inlines: &[Inline], out: &mut String) {
406 for inline in inlines {
407 match inline {
408 Inline::Text(text) | Inline::Code(text) => out.push_str(text),
409 Inline::Emphasis(content)
410 | Inline::Strong(content)
411 | Inline::Underline(content)
412 | Inline::Strike(content)
413 | Inline::Styled { content, .. } => push_inlines(content, out),
414 Inline::Link { target, content } => {
415 if content.is_empty() {
416 out.push_str(target);
417 } else {
418 push_inlines(content, out);
419 }
420 }
421 Inline::Image { alt, .. } => out.push_str(alt),
422 Inline::LineBreak => out.push('\n'),
423 }
424 }
425}