1use std::collections::HashMap;
17
18use eframe::egui::{
19 Align, ColorImage, Context, Pos2, Rect, Sense, Stroke, StrokeKind, TextFormat, TextureHandle,
20 TextureOptions, Ui, pos2, text::LayoutJob, vec2,
21};
22use odox_core::{
23 Border, Document, Element, Family, Node, Ns, Properties, TextAlign, TextProperties,
24};
25
26use crate::format::{self, DEFAULT_SIZE};
27
28#[derive(Default)]
33pub struct Pictures {
34 textures: HashMap<String, Option<TextureHandle>>,
35}
36
37impl Pictures {
38 pub fn get(
40 &mut self,
41 ctx: &Context,
42 document: &Document,
43 href: &str,
44 ) -> Option<&TextureHandle> {
45 if !self.textures.contains_key(href) {
46 let texture = document
47 .picture(href)
48 .and_then(decode)
49 .map(|image| ctx.load_texture(href, image, TextureOptions::LINEAR));
50 self.textures.insert(href.to_owned(), texture);
51 }
52 self.textures.get(href).and_then(Option::as_ref)
53 }
54
55 pub fn clear(&mut self) {
57 self.textures.clear();
58 }
59}
60
61fn decode(bytes: &[u8]) -> Option<ColorImage> {
62 let decoded = image::load_from_memory(bytes).ok()?.to_rgba8();
63 let size = [decoded.width() as usize, decoded.height() as usize];
64 Some(ColorImage::from_rgba_unmultiplied(size, decoded.as_raw()))
65}
66
67pub struct Flow<'a> {
69 pub document: &'a Document,
71 pub pictures: &'a mut Pictures,
73 pub zoom: f32,
75 pub palette: format::Palette,
77 pub scroll_to_heading: Option<usize>,
83 headings_seen: usize,
85}
86
87impl<'a> Flow<'a> {
88 pub fn new(document: &'a Document, pictures: &'a mut Pictures, zoom: f32) -> Self {
90 Self {
91 document,
92 pictures,
93 zoom,
94 palette: format::Palette::default(),
95 scroll_to_heading: None,
96 headings_seen: 0,
97 }
98 }
99}
100
101const LIST_STEP: f32 = 18.0;
103const LIST_GUTTER: f32 = 20.0;
105const LABEL_GAP: f32 = 5.0;
107
108struct Spacing {
110 left: f32,
112 right: f32,
114 indent: f32,
116 before: f32,
118 after: f32,
120}
121
122impl Spacing {
123 fn of(properties: &odox_core::ParagraphProperties, zoom: f32) -> Self {
124 let points = |length: Option<odox_core::Length>| {
125 length.map_or(0.0, odox_core::Length::points) * zoom
126 };
127 Self {
128 left: points(properties.margin.left),
129 right: points(properties.margin.right),
130 indent: points(properties.text_indent),
131 before: points(properties.margin.top),
132 after: points(properties.margin.bottom),
133 }
134 }
135}
136
137#[derive(Clone, Copy)]
142struct Run<'a> {
143 inherited: &'a TextProperties,
145 size: f32,
148 format: &'a TextFormat,
150 palette: format::Palette,
152}
153
154#[derive(Default)]
156struct Counters(Vec<usize>);
157
158impl Counters {
159 fn bump(&mut self, level: usize, start: usize) -> usize {
160 while self.0.len() <= level {
161 self.0.push(0);
162 }
163 if self.0[level] == 0 {
166 self.0[level] = start;
167 } else {
168 self.0[level] += 1;
169 }
170 self.0.truncate(level + 1);
173 self.0[level]
174 }
175
176 fn at(&self, level: usize) -> usize {
177 self.0.get(level).copied().unwrap_or(1)
178 }
179}
180
181impl Flow<'_> {
182 pub fn blocks(&mut self, ui: &mut Ui, parent: &Element, width: f32) {
187 let mut counters = Counters::default();
188 self.blocks_with(ui, parent, width, &mut counters);
189 }
190
191 fn blocks_with(&mut self, ui: &mut Ui, parent: &Element, width: f32, counters: &mut Counters) {
192 for element in parent.elements() {
193 match () {
194 () if element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h") => {
195 self.paragraph(ui, element, width, None);
196 }
197 () if element.is(&Ns::Text, "list") => {
198 let continues = element.attr(&Ns::Text, "continue-numbering") == Some("true")
203 || element.attr(&Ns::Text, "continue-list").is_some();
204 if !continues {
205 *counters = Counters::default();
206 }
207 self.list(ui, element, width, 0, None, counters);
208 }
209 () if element.is(&Ns::Table, "table") => self.table(ui, element, width),
210 () if element.is(&Ns::Draw, "frame") => self.frame(ui, element, width),
211 () if element.is(&Ns::Text, "soft-page-break") => self.page_break(ui, width),
212 () if is_block_container(element) => {
213 self.blocks_with(ui, element, width, counters);
214 }
215 () => {}
216 }
217 }
218 }
219
220 fn style_of(&self, element: &Element, family: &Family) -> std::rc::Rc<Properties> {
222 let name = element
223 .attr(&Ns::Text, "style-name")
224 .or_else(|| element.attr(&Ns::Table, "style-name"))
225 .unwrap_or("Standard");
226 self.document.styles.resolve(family, name)
227 }
228
229 fn paragraph(&mut self, ui: &mut Ui, element: &Element, width: f32, label: Option<&str>) {
231 let properties = self.style_of(element, &Family::Paragraph);
232 let zoom = self.zoom;
233 let size = format::size_of(&properties.text, DEFAULT_SIZE);
234
235 let Spacing {
236 left,
237 right,
238 indent,
239 before,
240 after,
241 } = Spacing::of(&properties.paragraph, zoom);
242
243 let body_width = (width - left - right).max(1.0);
247 let wrap = (body_width - indent.max(0.0)).max(1.0);
248
249 if before > 0.0 {
250 ui.add_space(before);
251 }
252
253 let base = format::text_format(&properties.text, DEFAULT_SIZE, zoom, self.palette);
254 let mut job = LayoutJob {
255 wrap: eframe::egui::text::TextWrapping {
256 max_width: wrap,
257 ..Default::default()
258 },
259 halign: match properties.paragraph.align.unwrap_or(TextAlign::Start) {
260 TextAlign::Center => Align::Center,
261 TextAlign::End => Align::Max,
262 _ => Align::Min,
263 },
264 justify: properties.paragraph.align == Some(TextAlign::Justify),
265 ..LayoutJob::default()
266 };
267 let mut frames = Vec::new();
268 let run = Run {
269 inherited: &properties.text,
270 size,
271 format: &base,
272 palette: self.palette,
273 };
274 self.runs(element, &run, &mut job, &mut frames);
275
276 if job.text.is_empty() {
279 job.append(" ", 0.0, base.clone());
280 }
281 if let Some(height) = format::line_height(properties.paragraph.line_height, size) {
282 for section in &mut job.sections {
283 section.format.line_height = Some(height * zoom);
284 }
285 }
286
287 let galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(job));
288 let height = galley.size().y;
289 let (rect, _) = ui.allocate_exact_size(vec2(width, height), Sense::hover());
290
291 if element.is(&Ns::Text, "h") {
292 if self.scroll_to_heading == Some(self.headings_seen) {
293 ui.scroll_to_rect(rect, Some(Align::TOP));
294 }
295 self.headings_seen += 1;
296 }
297
298 if let Some(fill) = properties.paragraph.background {
299 ui.painter().rect_filled(rect, 0.0, format::color32(fill));
300 }
301 paint_borders(ui, rect, &properties.paragraph.border, zoom);
302
303 let anchor = match properties.paragraph.align.unwrap_or(TextAlign::Start) {
307 TextAlign::Center => rect.left() + left + indent.max(0.0) + wrap / 2.0,
308 TextAlign::End => rect.left() + left + indent.max(0.0) + wrap,
309 _ => rect.left() + left + indent.max(0.0),
310 };
311 ui.painter()
312 .galley(pos2(anchor, rect.top()), galley, base.color);
313
314 if let Some(label) = label {
315 let mut label_job = LayoutJob::default();
316 label_job.append(label, 0.0, base.clone());
317 let label_galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(label_job));
318 let x = rect.left() + left - label_galley.size().x - LABEL_GAP * zoom;
322 ui.painter()
323 .galley(pos2(x, rect.top()), label_galley, base.color);
324 }
325
326 for frame in frames {
327 self.frame(ui, &frame, body_width);
328 }
329 if after > 0.0 {
330 ui.add_space(after);
331 }
332 }
333
334 fn runs(
341 &self,
342 parent: &Element,
343 run: &Run<'_>,
344 job: &mut LayoutJob,
345 frames: &mut Vec<Element>,
346 ) {
347 let Run {
348 inherited,
349 size,
350 format,
351 palette,
352 } = *run;
353 for child in &parent.children {
354 match child {
355 Node::Text(text) | Node::CData(text) => job.append(text, 0.0, format.clone()),
356 Node::Comment(_) | Node::ProcessingInstruction(_) => {}
357 Node::Element(element) => {
358 if element.is(&Ns::Text, "s") {
359 let count = element.attr_usize(&Ns::Text, "c").unwrap_or(1).min(256);
360 job.append(&" ".repeat(count), 0.0, format.clone());
361 } else if element.is(&Ns::Text, "tab") {
362 job.append(" ", 0.0, format.clone());
367 } else if element.is(&Ns::Text, "line-break") {
368 job.append("\n", 0.0, format.clone());
369 } else if element.is(&Ns::Text, "span") {
370 let style = self.style_of(element, &Family::Text);
371 let merged = merge(inherited, &style.text);
372 let inner_size = format::size_of(&merged, size);
373 let inner = format::text_format(&merged, size, self.zoom, palette);
374 let run = Run {
375 inherited: &merged,
376 size: inner_size,
377 format: &inner,
378 palette,
379 };
380 self.runs(element, &run, job, frames);
381 } else if element.is(&Ns::Text, "a") {
382 let style = self.style_of(element, &Family::Text);
383 let merged = merge(inherited, &style.text);
384 let inner_size = format::size_of(&merged, size);
385 let inner = format::link_format(&merged, size, self.zoom, palette);
386 let run = Run {
387 inherited: &merged,
388 size: inner_size,
389 format: &inner,
390 palette,
391 };
392 self.runs(element, &run, job, frames);
393 } else if element.is(&Ns::Draw, "frame") {
394 frames.push(element.clone());
395 } else if element.is(&Ns::Text, "note") {
396 if let Some(citation) = element.child(&Ns::Text, "note-citation") {
400 let mut raised = format.clone();
401 raised.font_id.size *= 0.7;
402 raised.valign = Align::TOP;
403 job.append(&citation.plain_text(), 0.0, raised);
404 }
405 } else if is_inline_passthrough(element) {
406 self.runs(element, run, job, frames);
407 }
408 }
409 }
410 }
411 }
412
413 fn list(
415 &mut self,
416 ui: &mut Ui,
417 element: &Element,
418 width: f32,
419 level: usize,
420 inherited_style: Option<&str>,
421 counters: &mut Counters,
422 ) {
423 let style_name = element
424 .attr(&Ns::Text, "style-name")
425 .or(inherited_style)
426 .unwrap_or_default()
427 .to_owned();
428 #[allow(clippy::cast_precision_loss)]
432 let indent = LIST_STEP * self.zoom * (level as f32 + 1.0);
433 let gutter = LIST_GUTTER * self.zoom;
434
435 for item in element.elements() {
436 let numbered = item.is(&Ns::Text, "list-item");
437 if !numbered && !item.is(&Ns::Text, "list-header") {
438 continue;
439 }
440 let label = if numbered {
441 let level_style = self.level_style(&style_name, level);
442 let start = level_style
443 .as_ref()
444 .and_then(|s| s.attr_usize(&Ns::Text, "start-value"))
445 .unwrap_or(1);
446 let number = counters.bump(level, start);
447 Self::label(level_style.as_ref(), level, number, counters)
448 } else {
449 String::new()
452 };
453
454 let mut first = true;
455 for block in item.elements() {
456 if block.is(&Ns::Text, "list") {
457 self.list(ui, block, width, level + 1, Some(&style_name), counters);
458 continue;
459 }
460 let on_this_block = if first && !label.is_empty() {
461 Some(label.as_str())
462 } else {
463 None
464 };
465 if block.is(&Ns::Text, "p") || block.is(&Ns::Text, "h") {
466 first = false;
467 ui.horizontal_top(|ui| {
471 ui.add_space(indent + gutter);
472 self.paragraph(ui, block, width - indent - gutter, on_this_block);
473 });
474 } else if block.is(&Ns::Table, "table") {
475 first = false;
476 ui.horizontal_top(|ui| {
477 ui.add_space(indent + gutter);
478 self.table(ui, block, width - indent - gutter);
479 });
480 } else if block.is(&Ns::Draw, "frame") {
481 first = false;
482 ui.horizontal_top(|ui| {
483 ui.add_space(indent + gutter);
484 self.frame(ui, block, width - indent - gutter);
485 });
486 }
487 }
488 }
489 }
490
491 fn level_style(&self, list_style: &str, level: usize) -> Option<Element> {
494 let style = self.document.styles.list_style(list_style)?;
495 style
496 .elements()
497 .find(|e| e.attr_usize(&Ns::Text, "level") == Some(level + 1))
498 .cloned()
499 }
500
501 fn label(
503 level_style: Option<&Element>,
504 level: usize,
505 number: usize,
506 counters: &Counters,
507 ) -> String {
508 let Some(style) = level_style else {
509 return "\u{2022}".to_owned();
512 };
513 if style.is(&Ns::Text, "list-level-style-bullet") {
514 return style
515 .attr(&Ns::Text, "bullet-char")
516 .unwrap_or("\u{2022}")
517 .to_owned();
518 }
519 if style.is(&Ns::Text, "list-level-style-number") {
520 let format = style.attr(&Ns::Style, "num-format").unwrap_or("1");
521 let prefix = style.attr(&Ns::Text, "num-prefix").unwrap_or_default();
522 let suffix = style.attr(&Ns::Text, "num-suffix").unwrap_or_default();
523 let display = style
526 .attr_usize(&Ns::Text, "display-levels")
527 .unwrap_or(1)
528 .max(1);
529 let mut numbers = Vec::new();
530 let first = (level + 1).saturating_sub(display);
531 for ancestor in first..level {
532 numbers.push(number_text(counters.at(ancestor), format));
533 }
534 numbers.push(number_text(number, format));
535 return format!("{prefix}{}{suffix}", numbers.join("."));
536 }
537 "\u{2022}".to_owned()
539 }
540
541 fn table(&mut self, ui: &mut Ui, table: &Element, width: f32) {
543 let columns = column_widths(self.document, table, width, self.zoom);
544 if columns.is_empty() {
545 return;
546 }
547 ui.add_space(4.0 * self.zoom);
548 self.rows(ui, table, &columns);
549 ui.add_space(4.0 * self.zoom);
550 }
551
552 fn rows(&mut self, ui: &mut Ui, parent: &Element, columns: &[f32]) {
553 for element in parent.elements() {
554 if element.is(&Ns::Table, "table-row") {
555 self.row(ui, element, columns);
556 } else if element.is(&Ns::Table, "table-header-rows")
557 || element.is(&Ns::Table, "table-rows")
558 || element.is(&Ns::Table, "table-row-group")
559 {
560 self.rows(ui, element, columns);
561 }
562 }
563 }
564
565 fn row(&mut self, ui: &mut Ui, row: &Element, columns: &[f32]) {
566 let reserved = ui.painter().add(eframe::egui::Shape::Noop);
570 let mut painted = Vec::new();
571
572 let response = ui.horizontal_top(|ui| {
573 ui.spacing_mut().item_spacing.x = 0.0;
574 let mut column = 0usize;
575 for cell in row.elements() {
576 let covered = cell.is(&Ns::Table, "covered-table-cell");
577 if !covered && !cell.is(&Ns::Table, "table-cell") {
578 continue;
579 }
580 let repeat = cell
581 .attr_usize(&Ns::Table, "number-columns-repeated")
582 .unwrap_or(1)
583 .max(1);
584 let spanned = cell
585 .attr_usize(&Ns::Table, "number-columns-spanned")
586 .unwrap_or(1)
587 .max(1);
588 for _ in 0..repeat {
589 let width: f32 = columns
595 .iter()
596 .skip(column)
597 .take(spanned)
598 .sum::<f32>()
599 .max(8.0);
600 if !covered {
601 let properties = self.style_of(cell, &Family::TableCell);
602 let padding = properties
603 .cell
604 .padding
605 .left
606 .map_or(2.0, odox_core::Length::points)
607 * self.zoom;
608 let inner = ui
609 .allocate_ui_with_layout(
610 vec2(width, 0.0),
611 eframe::egui::Layout::top_down(Align::Min),
612 |ui| {
613 ui.add_space(padding);
614 ui.set_min_width(width);
615 ui.set_max_width(width);
616 let content = (width - padding * 2.0).max(8.0);
617 self.blocks(ui, cell, content);
618 ui.add_space(padding);
619 },
620 )
621 .response
622 .rect;
623 painted.push((inner, properties));
624 }
625 column += 1;
626 }
627 }
628 });
629
630 let row_rect = response.response.rect;
631 let mut shapes = Vec::new();
632 for (rect, properties) in painted {
633 let cell_rect = Rect::from_min_max(
634 pos2(rect.left(), row_rect.top()),
635 pos2(rect.right(), row_rect.bottom()),
636 );
637 if let Some(fill) = properties.cell.background {
638 shapes.push(eframe::egui::Shape::rect_filled(
639 cell_rect,
640 0.0,
641 format::color32(fill),
642 ));
643 }
644 for (edge, from, to) in edges(cell_rect) {
645 if let Some(border) = edge_of(&properties.cell.border, edge) {
646 shapes.push(eframe::egui::Shape::line_segment(
647 [from, to],
648 Stroke::new(
649 (border.width.points() * self.zoom).max(1.0),
650 format::color32(border.color),
651 ),
652 ));
653 }
654 }
655 }
656 ui.painter().set(reserved, eframe::egui::Shape::Vec(shapes));
657 }
658
659 fn frame(&mut self, ui: &mut Ui, frame: &Element, width: f32) {
661 let zoom = self.zoom;
662 let declared = |local: &str| {
663 frame
664 .attr(&Ns::Svg, local)
665 .and_then(odox_core::Length::parse)
666 .map(|l| l.points() * zoom)
667 };
668
669 if let Some(image) = frame.child(&Ns::Draw, "image") {
670 let href = image
671 .attr(&Ns::Xlink, "href")
672 .unwrap_or_default()
673 .to_owned();
674 let found = self
675 .pictures
676 .get(ui.ctx(), self.document, &href)
677 .map(|texture| (texture.id(), texture.size()));
678 if let Some((id, [pixels_wide, pixels_high])) = found {
679 #[allow(clippy::cast_precision_loss)]
680 let aspect = if pixels_wide == 0 {
681 1.0
682 } else {
683 pixels_high as f32 / pixels_wide as f32
684 };
685 let w = declared("width").unwrap_or(width).min(width);
686 let h = declared("height").unwrap_or(w * aspect);
687 let size = vec2(w, h);
688 ui.add(eframe::egui::Image::new((id, size)).fit_to_exact_size(size));
689 return;
690 }
691 }
692
693 if let Some(box_) = frame.child(&Ns::Draw, "text-box") {
697 let w = declared("width").unwrap_or(width).min(width);
698 ui.allocate_ui_with_layout(
699 vec2(w, 0.0),
700 eframe::egui::Layout::top_down(Align::Min),
701 |ui| {
702 ui.set_max_width(w);
703 eframe::egui::Frame::group(ui.style()).show(ui, |ui| {
704 self.blocks(ui, box_, w - 16.0 * zoom);
705 });
706 },
707 );
708 return;
709 }
710
711 let w = declared("width").unwrap_or(width).min(width);
712 let h = declared("height").unwrap_or(48.0 * zoom);
713 let (rect, _) = ui.allocate_exact_size(vec2(w, h), Sense::hover());
714 ui.painter().rect_stroke(
715 rect,
716 2.0,
717 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
718 StrokeKind::Inside,
719 );
720 }
721
722 fn page_break(&mut self, ui: &mut Ui, width: f32) {
727 ui.add_space(8.0 * self.zoom);
728 let (rect, _) = ui.allocate_exact_size(vec2(width, 1.0), Sense::hover());
729 ui.painter().hline(
730 rect.x_range(),
731 rect.center().y,
732 Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
733 );
734 ui.add_space(8.0 * self.zoom);
735 }
736}
737
738fn merge(parent: &TextProperties, child: &TextProperties) -> TextProperties {
741 TextProperties {
742 font_family: child
743 .font_family
744 .clone()
745 .or_else(|| parent.font_family.clone()),
746 size: child.size.or(parent.size),
747 bold: child.bold.or(parent.bold),
748 italic: child.italic.or(parent.italic),
749 underline: child.underline.or(parent.underline),
750 strike: child.strike.or(parent.strike),
751 color: child.color.or(parent.color),
752 background: child.background.or(parent.background),
753 position: child.position.or(parent.position),
754 uppercase: child.uppercase.or(parent.uppercase),
755 }
756}
757
758#[derive(Clone, Copy)]
760enum Edge {
761 Left,
762 Right,
763 Top,
764 Bottom,
765}
766
767fn edges(rect: Rect) -> [(Edge, Pos2, Pos2); 4] {
768 [
769 (Edge::Left, rect.left_top(), rect.left_bottom()),
770 (Edge::Right, rect.right_top(), rect.right_bottom()),
771 (Edge::Top, rect.left_top(), rect.right_top()),
772 (Edge::Bottom, rect.left_bottom(), rect.right_bottom()),
773 ]
774}
775
776fn edge_of(borders: &odox_core::Edges<Border>, edge: Edge) -> Option<Border> {
777 match edge {
778 Edge::Left => borders.left,
779 Edge::Right => borders.right,
780 Edge::Top => borders.top,
781 Edge::Bottom => borders.bottom,
782 }
783}
784
785fn paint_borders(ui: &Ui, rect: Rect, borders: &odox_core::Edges<Border>, zoom: f32) {
786 for (edge, from, to) in edges(rect) {
787 if let Some(border) = edge_of(borders, edge) {
788 ui.painter().line_segment(
789 [from, to],
790 Stroke::new(
791 (border.width.points() * zoom).max(1.0),
792 format::color32(border.color),
793 ),
794 );
795 }
796 }
797}
798
799fn column_widths(document: &Document, table: &Element, width: f32, zoom: f32) -> Vec<f32> {
804 let mut declared = Vec::new();
805 collect_columns(document, table, &mut declared);
806 if declared.is_empty() {
807 return Vec::new();
808 }
809 let total: f32 = declared.iter().filter_map(|w| *w).sum();
810 let unstated = declared.iter().filter(|w| w.is_none()).count();
811
812 let scaled = total * zoom;
815 let factor = if scaled > width && scaled > 0.0 {
816 width / scaled
817 } else {
818 1.0
819 };
820 #[allow(clippy::cast_precision_loss)]
821 let share = if unstated == 0 {
822 0.0
823 } else {
824 ((width - scaled * factor) / unstated as f32).max(16.0)
825 };
826 declared
827 .into_iter()
828 .map(|w| w.map_or(share, |points| points * zoom * factor))
829 .collect()
830}
831
832fn collect_columns(document: &Document, parent: &Element, into: &mut Vec<Option<f32>>) {
833 for element in parent.elements() {
834 if element.is(&Ns::Table, "table-column") {
835 let repeat = element
836 .attr_usize(&Ns::Table, "number-columns-repeated")
837 .unwrap_or(1)
838 .clamp(1, 1024);
839 let width = element
840 .attr(&Ns::Table, "style-name")
841 .map(|name| document.styles.resolve(&Family::TableColumn, name))
842 .and_then(|p| p.column_width)
843 .map(odox_core::Length::points);
844 for _ in 0..repeat {
845 into.push(width);
846 }
847 } else if element.is(&Ns::Table, "table-columns")
848 || element.is(&Ns::Table, "table-header-columns")
849 || element.is(&Ns::Table, "table-column-group")
850 {
851 collect_columns(document, element, into);
852 }
853 }
854}
855
856fn number_text(number: usize, format: &str) -> String {
858 match format.chars().next() {
859 Some('a') => alphabetic(number, b'a'),
860 Some('A') => alphabetic(number, b'A'),
861 Some('i') => roman(number).to_lowercase(),
862 Some('I') => roman(number),
863 None => String::new(),
866 _ => number.to_string(),
867 }
868}
869
870fn alphabetic(number: usize, first: u8) -> String {
872 let mut n = number;
873 let mut out = Vec::new();
874 while n > 0 {
875 let remainder = (n - 1) % 26;
876 out.push(first + u8::try_from(remainder).unwrap_or(0));
877 n = (n - 1) / 26;
878 }
879 out.reverse();
880 String::from_utf8(out).unwrap_or_default()
881}
882
883fn roman(number: usize) -> String {
884 const VALUES: [(usize, &str); 13] = [
885 (1000, "M"),
886 (900, "CM"),
887 (500, "D"),
888 (400, "CD"),
889 (100, "C"),
890 (90, "XC"),
891 (50, "L"),
892 (40, "XL"),
893 (10, "X"),
894 (9, "IX"),
895 (5, "V"),
896 (4, "IV"),
897 (1, "I"),
898 ];
899 if number == 0 || number > 3999 {
902 return number.to_string();
903 }
904 let mut left = number;
905 let mut out = String::new();
906 for (value, numeral) in VALUES {
907 while left >= value {
908 out.push_str(numeral);
909 left -= value;
910 }
911 }
912 out
913}
914
915fn is_block_container(element: &Element) -> bool {
917 element.name.ns == Ns::Text
918 && matches!(
919 &*element.name.local,
920 "section"
921 | "index-body"
922 | "index-title"
923 | "table-of-content"
924 | "illustration-index"
925 | "table-index"
926 | "object-index"
927 | "user-index"
928 | "alphabetical-index"
929 | "bibliography"
930 | "tracked-changes"
931 | "deletion"
932 )
933}
934
935fn is_inline_passthrough(element: &Element) -> bool {
941 element.name.ns == Ns::Text
942 && matches!(
943 &*element.name.local,
944 "bookmark"
945 | "bookmark-start"
946 | "bookmark-end"
947 | "reference-mark"
948 | "reference-mark-start"
949 | "reference-mark-end"
950 | "span"
951 | "bibliography-mark"
952 | "ruby"
953 | "ruby-base"
954 | "meta"
955 | "meta-field"
956 | "change-start"
957 | "change-end"
958 | "page-number"
959 | "page-count"
960 | "title"
961 | "subject"
962 | "author-name"
963 | "author-initials"
964 | "chapter"
965 | "file-name"
966 | "sheet-name"
967 | "date"
968 | "time"
969 | "creator"
970 | "description"
971 | "keywords"
972 | "sequence"
973 | "bookmark-ref"
974 | "sequence-ref"
975 | "reference-ref"
976 | "variable-get"
977 | "variable-set"
978 | "user-field-get"
979 | "placeholder"
980 | "conditional-text"
981 | "hidden-text"
982 | "text-input"
983 )
984}