use std::collections::HashMap;
use eframe::egui::{
Align, ColorImage, Context, Id, Key, Pos2, Rect, Sense, Stroke, StrokeKind, TextEdit,
TextFormat, TextureHandle, TextureOptions, Ui, pos2,
text::{CCursor, CCursorRange, CharIndex, LayoutJob},
text_selection::LabelSelectionState,
vec2,
};
use odox_core::{
Border, Document, Element, Family, Node, Ns, Properties, TextAlign, TextProperties,
};
use crate::format::{self, DEFAULT_SIZE};
#[derive(Default)]
pub struct Pictures {
textures: HashMap<String, Option<TextureHandle>>,
}
impl Pictures {
pub fn get(
&mut self,
ctx: &Context,
document: &Document,
href: &str,
) -> Option<&TextureHandle> {
if !self.textures.contains_key(href) {
let texture = document
.picture(href)
.and_then(decode)
.map(|image| ctx.load_texture(href, image, TextureOptions::LINEAR));
self.textures.insert(href.to_owned(), texture);
}
self.textures.get(href).and_then(Option::as_ref)
}
pub fn clear(&mut self) {
self.textures.clear();
}
}
fn decode(bytes: &[u8]) -> Option<ColorImage> {
let decoded = image::load_from_memory(bytes).ok()?.to_rgba8();
let size = [decoded.width() as usize, decoded.height() as usize];
Some(ColorImage::from_rgba_unmultiplied(size, decoded.as_raw()))
}
pub struct Flow<'a> {
pub document: &'a Document,
pub pictures: &'a mut Pictures,
pub zoom: f32,
pub palette: format::Palette,
pub scroll_to_heading: Option<usize>,
headings_seen: usize,
pub edit_mode: bool,
pub editor: Option<&'a mut Editor>,
pub clicked: Option<Vec<usize>>,
pub outcome: Option<Outcome>,
pub selectable: bool,
path: Vec<usize>,
detached: bool,
}
impl<'a> Flow<'a> {
pub fn new(document: &'a Document, pictures: &'a mut Pictures, zoom: f32) -> Self {
Self {
document,
pictures,
zoom,
palette: format::Palette::default(),
scroll_to_heading: None,
headings_seen: 0,
edit_mode: false,
editor: None,
clicked: None,
outcome: None,
selectable: true,
path: Vec::new(),
detached: false,
}
}
pub fn start_at(&mut self, prefix: Vec<usize>) {
self.path = prefix;
}
}
pub struct Editor {
pub path: Vec<usize>,
pub text: String,
pub original: String,
pub opened: bool,
pub caret: Option<usize>,
}
impl Editor {
pub fn open(path: Vec<usize>, text: String, caret: Option<usize>) -> Self {
Self {
path,
original: text.clone(),
text,
opened: true,
caret,
}
}
pub fn changed(&self) -> bool {
self.text != self.original
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Commit,
Cancel,
JoinPrevious,
}
const LIST_STEP: f32 = 18.0;
const LIST_GUTTER: f32 = 20.0;
const LABEL_GAP: f32 = 5.0;
struct Spacing {
left: f32,
right: f32,
indent: f32,
before: f32,
after: f32,
}
impl Spacing {
fn of(properties: &odox_core::ParagraphProperties, zoom: f32) -> Self {
let points = |length: Option<odox_core::Length>| {
length.map_or(0.0, odox_core::Length::points) * zoom
};
Self {
left: points(properties.margin.left),
right: points(properties.margin.right),
indent: points(properties.text_indent),
before: points(properties.margin.top),
after: points(properties.margin.bottom),
}
}
}
#[derive(Clone, Copy)]
struct Run<'a> {
inherited: &'a TextProperties,
size: f32,
format: &'a TextFormat,
palette: format::Palette,
}
#[derive(Default)]
struct Counters(Vec<usize>);
impl Counters {
fn bump(&mut self, level: usize, start: usize) -> usize {
while self.0.len() <= level {
self.0.push(0);
}
if self.0[level] == 0 {
self.0[level] = start;
} else {
self.0[level] += 1;
}
self.0.truncate(level + 1);
self.0[level]
}
fn at(&self, level: usize) -> usize {
self.0.get(level).copied().unwrap_or(1)
}
}
impl Flow<'_> {
pub fn blocks(&mut self, ui: &mut Ui, parent: &Element, width: f32) {
let mut counters = Counters::default();
self.blocks_with(ui, parent, width, &mut counters);
}
fn blocks_with(&mut self, ui: &mut Ui, parent: &Element, width: f32, counters: &mut Counters) {
for (index, element) in parent.elements_indexed() {
self.path.push(index);
match () {
() if element.is(&Ns::Text, "p") || element.is(&Ns::Text, "h") => {
self.paragraph(ui, element, width, None);
}
() if element.is(&Ns::Text, "list") => {
let continues = element.attr(&Ns::Text, "continue-numbering") == Some("true")
|| element.attr(&Ns::Text, "continue-list").is_some();
if !continues {
*counters = Counters::default();
}
self.list(ui, element, width, 0, None, counters);
}
() if element.is(&Ns::Table, "table") => self.table(ui, element, width),
() if element.is(&Ns::Draw, "frame") => self.frame(ui, element, width),
() if element.is(&Ns::Text, "soft-page-break") => self.page_break(ui, width),
() if is_block_container(element) => {
self.blocks_with(ui, element, width, counters);
}
() => {}
}
self.path.pop();
}
}
fn style_of(&self, element: &Element, family: &Family) -> std::rc::Rc<Properties> {
let name = element
.attr(&Ns::Text, "style-name")
.or_else(|| element.attr(&Ns::Table, "style-name"))
.unwrap_or("Standard");
self.document.styles.resolve(family, name)
}
fn paragraph(&mut self, ui: &mut Ui, element: &Element, width: f32, label: Option<&str>) {
let properties = self.style_of(element, &Family::Paragraph);
let zoom = self.zoom;
let size = format::size_of(&properties.text, DEFAULT_SIZE);
let Spacing {
left,
right,
indent,
before,
after,
} = Spacing::of(&properties.paragraph, zoom);
let body_width = (width - left - right).max(1.0);
let wrap = (body_width - indent.max(0.0)).max(1.0);
if before > 0.0 {
ui.add_space(before);
}
let base = format::text_format(&properties.text, DEFAULT_SIZE, zoom, self.palette);
if !self.detached && self.editor.as_ref().is_some_and(|e| e.path == self.path) {
let (job, _) = self.layout_job(element, &properties, &base, size, wrap);
let first = job
.sections
.first()
.map_or_else(|| base.clone(), |section| section.format.clone());
let mut editor = self.editor.take();
if let Some(editor) = editor.as_deref_mut() {
self.edit_box(ui, editor, &first, left, wrap);
}
self.editor = editor;
if after > 0.0 {
ui.add_space(after);
}
return;
}
let (job, frames) = self.layout_job(element, &properties, &base, size, wrap);
let galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(job));
let height = galley.size().y;
let sense = if self.selectable {
Sense::click_and_drag()
} else {
Sense::click()
};
let (rect, response) = ui.allocate_exact_size(vec2(width, height), sense);
if self.edit_mode && !self.detached && response.clicked() {
self.clicked = Some(self.path.clone());
}
if element.is(&Ns::Text, "h") {
if self.scroll_to_heading == Some(self.headings_seen) {
ui.scroll_to_rect(rect, Some(Align::TOP));
}
self.headings_seen += 1;
}
if let Some(fill) = properties.paragraph.background {
ui.painter().rect_filled(rect, 0.0, format::color32(fill));
}
paint_borders(ui, rect, &properties.paragraph.border, zoom);
let anchor = match properties.paragraph.align.unwrap_or(TextAlign::Start) {
TextAlign::Center => rect.left() + left + indent.max(0.0) + wrap / 2.0,
TextAlign::End => rect.left() + left + indent.max(0.0) + wrap,
_ => rect.left() + left + indent.max(0.0),
};
LabelSelectionState::label_text_selection(
ui,
&response,
pos2(anchor, rect.top()),
galley,
base.color,
Stroke::NONE,
);
if let Some(label) = label {
let mut label_job = LayoutJob::default();
label_job.append(label, 0.0, base.clone());
let label_galley = ui.ctx().fonts_mut(|fonts| fonts.layout_job(label_job));
let x = rect.left() + left - label_galley.size().x - LABEL_GAP * zoom;
ui.painter()
.galley(pos2(x, rect.top()), label_galley, base.color);
}
let was_detached = self.detached;
self.detached = true;
for frame in frames {
self.frame(ui, &frame, body_width);
}
self.detached = was_detached;
if after > 0.0 {
ui.add_space(after);
}
}
fn layout_job(
&self,
element: &Element,
properties: &Properties,
base: &TextFormat,
size: f32,
wrap: f32,
) -> (LayoutJob, Vec<Element>) {
let mut job = LayoutJob {
wrap: eframe::egui::text::TextWrapping {
max_width: wrap,
..Default::default()
},
halign: match properties.paragraph.align.unwrap_or(TextAlign::Start) {
TextAlign::Center => Align::Center,
TextAlign::End => Align::Max,
_ => Align::Min,
},
justify: properties.paragraph.align == Some(TextAlign::Justify),
..LayoutJob::default()
};
let mut frames = Vec::new();
let run = Run {
inherited: &properties.text,
size,
format: base,
palette: self.palette,
};
self.runs(element, &run, &mut job, &mut frames);
if job.text.is_empty() {
job.append(" ", 0.0, base.clone());
}
if let Some(measure) = properties.paragraph.line_height {
for section in &mut job.sections {
let own = section.format.font_id.size / self.zoom;
section.format.line_height = Some(measure.resolve(own) * self.zoom);
}
}
(job, frames)
}
fn edit_box(
&mut self,
ui: &mut Ui,
editor: &mut Editor,
base: &TextFormat,
left: f32,
wrap: f32,
) {
let id = Id::new("paragraph-editor");
let at_start = TextEdit::load_state(ui.ctx(), id)
.and_then(|state| state.cursor.char_range())
.is_some_and(|range| {
range.primary.index == CharIndex(0) && range.secondary.index == CharIndex(0)
});
let (backspace, keep) = ui.input(|input| {
(
input.key_pressed(Key::Backspace),
input.modifiers.command && input.key_pressed(Key::Enter),
)
});
if !editor.opened && at_start && backspace && self.outcome.is_none() {
self.outcome = Some(Outcome::JoinPrevious);
}
if ui.is_sizing_pass() {
let galley = ui.fonts_mut(|fonts| {
fonts.layout(editor.text.clone(), base.font_id.clone(), base.color, wrap)
});
ui.horizontal_top(|ui| {
ui.add_space(left);
ui.allocate_exact_size(vec2(wrap, galley.size().y + 4.0), Sense::hover());
});
return;
}
let response = ui
.horizontal_top(|ui| {
ui.add_space(left);
ui.add(
TextEdit::multiline(&mut editor.text)
.id(id)
.font(base.font_id.clone())
.text_color(base.color)
.background_color(self.palette.paper)
.desired_width(wrap)
.desired_rows(1)
.margin(vec2(2.0, 2.0)),
)
})
.inner;
if editor.opened {
editor.opened = false;
response.request_focus();
let mut state = TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
let at = editor
.caret
.unwrap_or_else(|| editor.text.chars().count())
.min(editor.text.chars().count());
state
.cursor
.set_char_range(Some(CCursorRange::one(CCursor::new(at))));
TextEdit::store_state(ui.ctx(), id, state);
return;
}
if keep && response.has_focus() {
response.surrender_focus();
self.outcome = Some(Outcome::Commit);
} else if response.lost_focus() && self.outcome.is_none() {
self.outcome = Some(if ui.input(|input| input.key_pressed(Key::Escape)) {
Outcome::Cancel
} else {
Outcome::Commit
});
}
}
fn runs(
&self,
parent: &Element,
run: &Run<'_>,
job: &mut LayoutJob,
frames: &mut Vec<Element>,
) {
let Run {
inherited,
size,
format,
palette,
} = *run;
for child in &parent.children {
match child {
Node::Text(text) | Node::CData(text) => job.append(text, 0.0, format.clone()),
Node::Comment(_) | Node::ProcessingInstruction(_) => {}
Node::Element(element) => {
if element.is(&Ns::Text, "s") {
let count = element.attr_usize(&Ns::Text, "c").unwrap_or(1).min(256);
job.append(&" ".repeat(count), 0.0, format.clone());
} else if element.is(&Ns::Text, "tab") {
job.append(" ", 0.0, format.clone());
} else if element.is(&Ns::Text, "line-break") {
job.append("\n", 0.0, format.clone());
} else if element.is(&Ns::Text, "span") {
let style = self.style_of(element, &Family::Text);
let merged = merge(inherited, &style.text);
let inner_size = format::size_of(&merged, size);
let inner = format::text_format(&merged, size, self.zoom, palette);
let run = Run {
inherited: &merged,
size: inner_size,
format: &inner,
palette,
};
self.runs(element, &run, job, frames);
} else if element.is(&Ns::Text, "a") {
let style = self.style_of(element, &Family::Text);
let merged = merge(inherited, &style.text);
let inner_size = format::size_of(&merged, size);
let inner = format::link_format(&merged, size, self.zoom, palette);
let run = Run {
inherited: &merged,
size: inner_size,
format: &inner,
palette,
};
self.runs(element, &run, job, frames);
} else if element.is(&Ns::Draw, "frame") {
frames.push(element.clone());
} else if element.is(&Ns::Text, "note") {
if let Some(citation) = element.child(&Ns::Text, "note-citation") {
let mut raised = format.clone();
raised.font_id.size *= 0.7;
raised.valign = Align::TOP;
job.append(&citation.plain_text(), 0.0, raised);
}
} else if is_inline_passthrough(element) {
self.runs(element, run, job, frames);
}
}
}
}
}
fn list(
&mut self,
ui: &mut Ui,
element: &Element,
width: f32,
level: usize,
inherited_style: Option<&str>,
counters: &mut Counters,
) {
let style_name = element
.attr(&Ns::Text, "style-name")
.or(inherited_style)
.unwrap_or_default()
.to_owned();
#[allow(clippy::cast_precision_loss)]
let indent = LIST_STEP * self.zoom * (level as f32 + 1.0);
let gutter = LIST_GUTTER * self.zoom;
for (item_index, item) in element.elements_indexed() {
let numbered = item.is(&Ns::Text, "list-item");
if !numbered && !item.is(&Ns::Text, "list-header") {
continue;
}
self.path.push(item_index);
let label = if numbered {
let level_style = self.level_style(&style_name, level);
let start = level_style
.as_ref()
.and_then(|s| s.attr_usize(&Ns::Text, "start-value"))
.unwrap_or(1);
let number = counters.bump(level, start);
Self::label(level_style.as_ref(), level, number, counters)
} else {
String::new()
};
let mut first = true;
for (block_index, block) in item.elements_indexed() {
self.path.push(block_index);
if block.is(&Ns::Text, "list") {
self.list(ui, block, width, level + 1, Some(&style_name), counters);
self.path.pop();
continue;
}
let on_this_block = if first && !label.is_empty() {
Some(label.as_str())
} else {
None
};
if block.is(&Ns::Text, "p") || block.is(&Ns::Text, "h") {
first = false;
ui.horizontal_top(|ui| {
ui.add_space(indent + gutter);
self.paragraph(ui, block, width - indent - gutter, on_this_block);
});
} else if block.is(&Ns::Table, "table") {
first = false;
ui.horizontal_top(|ui| {
ui.add_space(indent + gutter);
self.table(ui, block, width - indent - gutter);
});
} else if block.is(&Ns::Draw, "frame") {
first = false;
ui.horizontal_top(|ui| {
ui.add_space(indent + gutter);
self.frame(ui, block, width - indent - gutter);
});
}
self.path.pop();
}
self.path.pop();
}
}
fn level_style(&self, list_style: &str, level: usize) -> Option<Element> {
let style = self.document.styles.list_style(list_style)?;
style
.elements()
.find(|e| e.attr_usize(&Ns::Text, "level") == Some(level + 1))
.cloned()
}
fn label(
level_style: Option<&Element>,
level: usize,
number: usize,
counters: &Counters,
) -> String {
let Some(style) = level_style else {
return "\u{2022}".to_owned();
};
if style.is(&Ns::Text, "list-level-style-bullet") {
return style
.attr(&Ns::Text, "bullet-char")
.unwrap_or("\u{2022}")
.to_owned();
}
if style.is(&Ns::Text, "list-level-style-number") {
let format = style.attr(&Ns::Style, "num-format").unwrap_or("1");
let prefix = style.attr(&Ns::Text, "num-prefix").unwrap_or_default();
let suffix = style.attr(&Ns::Text, "num-suffix").unwrap_or_default();
let display = style
.attr_usize(&Ns::Text, "display-levels")
.unwrap_or(1)
.max(1);
let mut numbers = Vec::new();
let first = (level + 1).saturating_sub(display);
for ancestor in first..level {
numbers.push(number_text(counters.at(ancestor), format));
}
numbers.push(number_text(number, format));
return format!("{prefix}{}{suffix}", numbers.join("."));
}
"\u{2022}".to_owned()
}
fn table(&mut self, ui: &mut Ui, table: &Element, width: f32) {
let columns = column_widths(self.document, table, width, self.zoom);
if columns.is_empty() {
return;
}
ui.add_space(4.0 * self.zoom);
self.rows(ui, table, &columns);
ui.add_space(4.0 * self.zoom);
}
fn rows(&mut self, ui: &mut Ui, parent: &Element, columns: &[f32]) {
for (index, element) in parent.elements_indexed() {
self.path.push(index);
if element.is(&Ns::Table, "table-row") {
self.row(ui, element, columns);
} else if element.is(&Ns::Table, "table-header-rows")
|| element.is(&Ns::Table, "table-rows")
|| element.is(&Ns::Table, "table-row-group")
{
self.rows(ui, element, columns);
}
self.path.pop();
}
}
fn row(&mut self, ui: &mut Ui, row: &Element, columns: &[f32]) {
let reserved = ui.painter().add(eframe::egui::Shape::Noop);
let mut painted = Vec::new();
let response = ui.horizontal_top(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
let mut column = 0usize;
for (cell_index, cell) in row.elements_indexed() {
let covered = cell.is(&Ns::Table, "covered-table-cell");
if !covered && !cell.is(&Ns::Table, "table-cell") {
continue;
}
let repeat = cell
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.max(1);
let spanned = cell
.attr_usize(&Ns::Table, "number-columns-spanned")
.unwrap_or(1)
.max(1);
for _ in 0..repeat {
let width: f32 = columns
.iter()
.skip(column)
.take(spanned)
.sum::<f32>()
.max(8.0);
if !covered {
let properties = self.style_of(cell, &Family::TableCell);
let padding = properties
.cell
.padding
.left
.map_or(2.0, odox_core::Length::points)
* self.zoom;
let inner = ui
.allocate_ui_with_layout(
vec2(width, 0.0),
eframe::egui::Layout::top_down(Align::Min),
|ui| {
ui.add_space(padding);
ui.set_min_width(width);
ui.set_max_width(width);
let content = (width - padding * 2.0).max(8.0);
self.path.push(cell_index);
self.blocks(ui, cell, content);
self.path.pop();
ui.add_space(padding);
},
)
.response
.rect;
painted.push((inner, properties));
}
column += 1;
}
}
});
let row_rect = response.response.rect;
let mut shapes = Vec::new();
for (rect, properties) in painted {
let cell_rect = Rect::from_min_max(
pos2(rect.left(), row_rect.top()),
pos2(rect.right(), row_rect.bottom()),
);
if let Some(fill) = properties.cell.background {
shapes.push(eframe::egui::Shape::rect_filled(
cell_rect,
0.0,
format::color32(fill),
));
}
for (edge, from, to) in edges(cell_rect) {
if let Some(border) = edge_of(&properties.cell.border, edge) {
shapes.push(eframe::egui::Shape::line_segment(
[from, to],
Stroke::new(
(border.width.points() * self.zoom).max(1.0),
format::color32(border.color),
),
));
}
}
}
ui.painter().set(reserved, eframe::egui::Shape::Vec(shapes));
}
pub fn frame(&mut self, ui: &mut Ui, frame: &Element, width: f32) {
let zoom = self.zoom;
let declared = |local: &str| {
frame
.attr(&Ns::Svg, local)
.and_then(odox_core::Length::parse)
.map(|l| l.points() * zoom)
};
let found = frame
.elements()
.filter(|child| child.is(&Ns::Draw, "image"))
.find_map(|image| {
let href = image.attr(&Ns::Xlink, "href")?.to_owned();
self.pictures
.get(ui.ctx(), self.document, &href)
.map(|texture| (texture.id(), texture.size()))
});
if let Some((id, [pixels_wide, pixels_high])) = found {
#[allow(clippy::cast_precision_loss)]
let aspect = if pixels_wide == 0 {
1.0
} else {
pixels_high as f32 / pixels_wide as f32
};
let w = declared("width").unwrap_or(width).min(width);
let h = declared("height").unwrap_or(w * aspect);
let size = vec2(w, h);
ui.add(eframe::egui::Image::new((id, size)).fit_to_exact_size(size));
return;
}
if let Some((box_index, box_)) = frame
.elements_indexed()
.find(|(_, e)| e.is(&Ns::Draw, "text-box"))
{
let w = declared("width").unwrap_or(width).min(width);
ui.allocate_ui_with_layout(
vec2(w, 0.0),
eframe::egui::Layout::top_down(Align::Min),
|ui| {
ui.set_max_width(w);
eframe::egui::Frame::group(ui.style()).show(ui, |ui| {
self.path.push(box_index);
self.blocks(ui, box_, w - 16.0 * zoom);
self.path.pop();
});
},
);
return;
}
let w = declared("width").unwrap_or(width).min(width);
let h = declared("height").unwrap_or(48.0 * zoom);
let (rect, _) = ui.allocate_exact_size(vec2(w, h), Sense::hover());
ui.painter().rect_stroke(
rect,
2.0,
Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
StrokeKind::Inside,
);
}
fn page_break(&mut self, ui: &mut Ui, width: f32) {
ui.add_space(8.0 * self.zoom);
let (rect, _) = ui.allocate_exact_size(vec2(width, 1.0), Sense::hover());
ui.painter().hline(
rect.x_range(),
rect.center().y,
Stroke::new(1.0, self.palette.ink.gamma_multiply(0.3)),
);
ui.add_space(8.0 * self.zoom);
}
}
fn merge(parent: &TextProperties, child: &TextProperties) -> TextProperties {
TextProperties {
font_family: child
.font_family
.clone()
.or_else(|| parent.font_family.clone()),
size: child.size.or(parent.size),
bold: child.bold.or(parent.bold),
italic: child.italic.or(parent.italic),
underline: child.underline.or(parent.underline),
strike: child.strike.or(parent.strike),
color: child.color.or(parent.color),
background: child.background.or(parent.background),
position: child.position.or(parent.position),
uppercase: child.uppercase.or(parent.uppercase),
}
}
#[derive(Clone, Copy)]
enum Edge {
Left,
Right,
Top,
Bottom,
}
fn edges(rect: Rect) -> [(Edge, Pos2, Pos2); 4] {
[
(Edge::Left, rect.left_top(), rect.left_bottom()),
(Edge::Right, rect.right_top(), rect.right_bottom()),
(Edge::Top, rect.left_top(), rect.right_top()),
(Edge::Bottom, rect.left_bottom(), rect.right_bottom()),
]
}
fn edge_of(borders: &odox_core::Edges<Border>, edge: Edge) -> Option<Border> {
match edge {
Edge::Left => borders.left,
Edge::Right => borders.right,
Edge::Top => borders.top,
Edge::Bottom => borders.bottom,
}
}
fn paint_borders(ui: &Ui, rect: Rect, borders: &odox_core::Edges<Border>, zoom: f32) {
for (edge, from, to) in edges(rect) {
if let Some(border) = edge_of(borders, edge) {
ui.painter().line_segment(
[from, to],
Stroke::new(
(border.width.points() * zoom).max(1.0),
format::color32(border.color),
),
);
}
}
}
fn column_widths(document: &Document, table: &Element, width: f32, zoom: f32) -> Vec<f32> {
let mut declared = Vec::new();
collect_columns(document, table, &mut declared);
if declared.is_empty() {
return Vec::new();
}
let total: f32 = declared.iter().filter_map(|w| *w).sum();
let unstated = declared.iter().filter(|w| w.is_none()).count();
let scaled = total * zoom;
let factor = if scaled > width && scaled > 0.0 {
width / scaled
} else {
1.0
};
#[allow(clippy::cast_precision_loss)]
let share = if unstated == 0 {
0.0
} else {
((width - scaled * factor) / unstated as f32).max(16.0)
};
declared
.into_iter()
.map(|w| w.map_or(share, |points| points * zoom * factor))
.collect()
}
fn collect_columns(document: &Document, parent: &Element, into: &mut Vec<Option<f32>>) {
for element in parent.elements() {
if element.is(&Ns::Table, "table-column") {
let repeat = element
.attr_usize(&Ns::Table, "number-columns-repeated")
.unwrap_or(1)
.clamp(1, 1024);
let width = element
.attr(&Ns::Table, "style-name")
.map(|name| document.styles.resolve(&Family::TableColumn, name))
.and_then(|p| p.column_width)
.map(odox_core::Length::points);
for _ in 0..repeat {
into.push(width);
}
} else if element.is(&Ns::Table, "table-columns")
|| element.is(&Ns::Table, "table-header-columns")
|| element.is(&Ns::Table, "table-column-group")
{
collect_columns(document, element, into);
}
}
}
fn number_text(number: usize, format: &str) -> String {
match format.chars().next() {
Some('a') => alphabetic(number, b'a'),
Some('A') => alphabetic(number, b'A'),
Some('i') => roman(number).to_lowercase(),
Some('I') => roman(number),
None => String::new(),
_ => number.to_string(),
}
}
fn alphabetic(number: usize, first: u8) -> String {
let mut n = number;
let mut out = Vec::new();
while n > 0 {
let remainder = (n - 1) % 26;
out.push(first + u8::try_from(remainder).unwrap_or(0));
n = (n - 1) / 26;
}
out.reverse();
String::from_utf8(out).unwrap_or_default()
}
fn roman(number: usize) -> String {
const VALUES: [(usize, &str); 13] = [
(1000, "M"),
(900, "CM"),
(500, "D"),
(400, "CD"),
(100, "C"),
(90, "XC"),
(50, "L"),
(40, "XL"),
(10, "X"),
(9, "IX"),
(5, "V"),
(4, "IV"),
(1, "I"),
];
if number == 0 || number > 3999 {
return number.to_string();
}
let mut left = number;
let mut out = String::new();
for (value, numeral) in VALUES {
while left >= value {
out.push_str(numeral);
left -= value;
}
}
out
}
fn is_block_container(element: &Element) -> bool {
element.name.ns == Ns::Text
&& matches!(
&*element.name.local,
"section"
| "index-body"
| "index-title"
| "table-of-content"
| "illustration-index"
| "table-index"
| "object-index"
| "user-index"
| "alphabetical-index"
| "bibliography"
| "tracked-changes"
| "deletion"
)
}
fn is_inline_passthrough(element: &Element) -> bool {
element.name.ns == Ns::Text
&& matches!(
&*element.name.local,
"bookmark"
| "bookmark-start"
| "bookmark-end"
| "reference-mark"
| "reference-mark-start"
| "reference-mark-end"
| "span"
| "bibliography-mark"
| "ruby"
| "ruby-base"
| "meta"
| "meta-field"
| "change-start"
| "change-end"
| "page-number"
| "page-count"
| "title"
| "subject"
| "author-name"
| "author-initials"
| "chapter"
| "file-name"
| "sheet-name"
| "date"
| "time"
| "creator"
| "description"
| "keywords"
| "sequence"
| "bookmark-ref"
| "sequence-ref"
| "reference-ref"
| "variable-get"
| "variable-set"
| "user-field-get"
| "placeholder"
| "conditional-text"
| "hidden-text"
| "text-input"
)
}