// Conservative formatting import: resolve actual definitions; emit only the
// writer's fixed presets and exact integer-mm widths, never source XML.
///|
let read_style = "urn:oasis:names:tc:opendocument:xmlns:style:1.0#"
///|
let read_number = "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0#"
///|
priv struct ReadFormats {
defs : Map[String, ReadElement]
defaults : Map[String, ReadElement]
numbers : Map[String, ReadElement]
}
///|
priv struct ReadFormatting {
source : ReadFormats
presets : ReadFormats
cell_cache : Map[(String, String), Style?]
width_cache : Map[(Bool, String), String?]
height_cache : Map[(Bool, String), String?]
mut retained_cells : Int32
mut retained_widths : Int32
mut retained_heights : Int32
}
///|
fn ReadFormats::new() -> ReadFormats {
{ defs: {}, defaults: {}, numbers: {}, }
}
///|
fn read_format_parts(node : ReadElement) -> Array[ReadPart] {
node.parts.filter(part => {
match part {
ReadText(t) => node.name == read_number + "text" || t.trim() != ""
ReadElement(_) => true
}
})
}
///|
fn read_format_equal(
a : ReadElement,
b : ReadElement,
root~ : Bool = true,
) -> Bool {
if a.name != b.name {
return false
}
let aa = a.attrs.filter((k, _) => !(root && k == read_style + "name"))
let ba = b.attrs.filter((k, _) => !(root && k == read_style + "name"))
if aa.length() != ba.length() {
return false
}
for k, v in aa {
if ba.get(k) != Some(v) {
return false
}
}
let ap = read_format_parts(a)
let bp = read_format_parts(b)
if ap.length() != bp.length() {
return false
}
for i, p in ap {
match (p, bp[i]) {
(ReadText(x), ReadText(y)) => if x != y { return false }
(ReadElement(x), ReadElement(y)) =>
if !read_format_equal(x, y, root=false) {
return false
}
_ => return false
}
}
true
}
///|
fn ReadFormats::insert(
self : ReadFormats,
node : ReadElement,
) -> Unit raise OdsError {
if self.defs.length() + self.defaults.length() + self.numbers.length() >= 2048 {
raise Invalid("basic formatting definition budget exceeded")
}
let (map, key) = if node.name == read_style + "default-style" {
(self.defaults, node.attribute(read_style, "family").unwrap_or(""))
} else if node.name == read_style + "style" {
(
self.defs,
node.attribute(read_style, "family").unwrap_or("") +
":" +
node.attribute(read_style, "name").unwrap_or(""),
)
} else if node.name.has_prefix(read_number) {
(self.numbers, node.attribute(read_style, "name").unwrap_or(""))
} else {
return
}
if key == "" {
return
}
if map.get(key) is Some(previous) && !read_format_equal(previous, node) {
raise Invalid(
"ambiguous duplicate formatting definition: \{read_preview(key)}",
)
}
map.set(key, node)
}
///|
fn ReadFormats::collect(
self : ReadFormats,
root : ReadElement,
) -> Unit raise OdsError {
if root.name != read_office + "document-styles" &&
root.name != read_office + "document-content" {
raise Invalid("expected ODF formatting document root")
}
for container in root.elements() {
if container.name == read_office + "styles" ||
container.name == read_office + "automatic-styles" {
for definition in container.elements() {
self.insert(definition)
}
} else if root.name == read_office + "document-styles" &&
![read_office + "font-face-decls", read_office + "master-styles"].contains(
container.name,
) {
raise Invalid("unsupported styles document feature")
}
}
}
///|
fn read_format_overlay(
base : Map[String, String],
child : Map[String, String],
) -> Map[String, String] {
let result = base.copy()
for key, value in child {
result.set(key, value)
}
result
}
///|
fn ReadFormats::properties(
self : ReadFormats,
family : String,
name : String?,
depth~ : Int32 = 0,
seen~ : Array[String] = [],
) -> Map[String, String]? raise OdsError {
if depth > 16 {
return None
}
let node = match name {
None =>
match self.defaults.get(family) {
Some(node) => node
None => return Some(Map([]))
}
Some(name) => {
if seen.contains(name) {
return None
}
match self.defs.get(family + ":" + name) {
Some(node) => node
None => return None
}
}
}
for key, _ in node.attrs {
if ![
read_style + "name",
read_style + "family",
read_style + "parent-style-name",
read_style + "data-style-name",
read_style + "display-name",
].contains(key) {
return None
}
}
let chain = seen.copy()
if name is Some(name) {
chain.push(name)
}
let parent = node.attribute(read_style, "parent-style-name")
let props = if parent is Some(parent) {
match self.properties(family, Some(parent), depth=depth + 1, seen=chain) {
Some(p) => p
None => return None
}
} else {
Map([])
}
let visited : Array[String] = []
for property in node.elements() {
if ![
read_style + "text-properties",
read_style + "paragraph-properties",
read_style + "table-cell-properties",
read_style + "table-column-properties",
read_style + "table-row-properties",
].contains(property.name) ||
visited.contains(property.name) ||
!property.elements().is_empty() {
return None
}
visited.push(property.name)
for key, value in property.attrs {
props.set(property.name + "@" + key, value)
}
}
if node.attribute(read_style, "data-style-name") is Some(data) {
props.set("#data-style", data)
}
Some(props)
}
///|
fn ReadFormats::effective(
self : ReadFormats,
family : String,
name : String?,
) -> Map[String, String]? raise OdsError {
let base = match self.properties(family, None) {
Some(p) => p
None => return None
}
match name {
None => Some(base)
Some(_) =>
match self.properties(family, name) {
Some(p) => Some(read_format_overlay(base, p))
None => None
}
}
}
///|
fn read_props_equal(
a : Map[String, String],
b : Map[String, String],
actual : ReadFormats,
expected : ReadFormats,
) -> Bool {
if a.length() != b.length() {
return false
}
for key, value in a {
if key == "#data-style" {
match (actual.numbers.get(value), b.get(key)) {
(Some(definition), Some(name)) =>
match expected.numbers.get(name) {
Some(target) =>
if !read_format_equal(definition, target) {
return false
}
None => return false
}
_ => return false
}
} else if b.get(key) != Some(value) {
return false
}
}
true
}
///|
fn ReadFormatting::resolve_cell_style(
self : ReadFormatting,
name : String?,
value~ : Value = Empty,
) -> Style? raise OdsError {
let source = match self.source.effective("table-cell", name) {
Some(p) => p
None => return None
}
let typed = typed_format_key(value) != ""
let known_type_format = if typed {
match source.get("#data-style") {
Some(name) =>
match self.source.numbers.get(name) {
Some(definition) => {
let expected = read_xml(
"\{typed_number_format(value)}",
)
read_format_equal(definition, expected.elements()[0].elements()[0])
}
None => false
}
None => false
}
} else {
false
}
if typed && !known_type_format {
return None
}
let source = if known_type_format {
source.filter((key, _) => key != "#data-style")
} else {
source
}
for preset in [Plain, Header, Highlight, Decimal2, DateISO] {
match self.presets.effective("table-cell", Some(preset.id())) {
Some(expected) => {
let expected = if known_type_format {
expected.filter((key, _) => key != "#data-style")
} else {
expected
}
if read_props_equal(source, expected, self.source, self.presets) {
return Some(preset)
}
}
None => ()
}
}
None
}
///|
fn ReadFormatting::resolve_column_width(
self : ReadFormatting,
name : String?,
) -> String? raise OdsError {
let props = match self.source.effective("table-column", name) {
Some(p) => p
None => return None
}
let width_key = read_style +
"table-column-properties@" +
read_style +
"column-width"
let optimal_key = read_style +
"table-column-properties@" +
read_style +
"use-optimal-column-width"
for key, value in props {
let neutral_break = (
key ==
read_style +
"table-column-properties@urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0#break-before" ||
key ==
read_style +
"table-column-properties@urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0#break-after"
) &&
value == "auto"
if key != width_key &&
!(key == optimal_key && value == "false") &&
!neutral_break {
return None
}
}
match props.get(width_key) {
Some(width) => physical_length(width).map(v => v.0)
None => None
}
}
///|
fn read_formatting(
root : ReadElement,
styles : Array[String],
) -> ReadFormatting raise OdsError {
let source = ReadFormats::new()
for xml in styles {
source.collect(read_xml(xml))
}
source.collect(root)
let presets = ReadFormats::new()
presets.collect(read_xml(styles_xml()))
{
source,
presets,
cell_cache: Map([]),
width_cache: Map([]),
height_cache: Map([]),
retained_cells: 0,
retained_widths: 0,
retained_heights: 0,
}
}
///|
fn ReadFormatting::cell_style(
self : ReadFormatting,
name : String?,
value~ : Value = Empty,
) -> Style? raise OdsError {
let reference = match name {
Some(name) => "name:" + name
None => "default"
}
let key = (reference, typed_format_key(value))
match self.cell_cache.get(key) {
Some(cached) => cached
None => {
if self.cell_cache.length() >= 2049 {
return None
}
let resolved = self.resolve_cell_style(name, value~)
if name is None ||
self.source.defs.contains("table-cell:" + name.unwrap_or("")) {
self.cell_cache.set(key, resolved)
}
resolved
}
}
}
///|
fn ReadFormatting::column_width(
self : ReadFormatting,
name : String?,
) -> String? raise OdsError {
let key = (name is Some(_), name.unwrap_or(""))
match self.width_cache.get(key) {
Some(cached) => cached
None => {
let resolved = self.resolve_column_width(name)
if self.width_cache.length() < 2049 &&
(
name is None ||
self.source.defs.contains("table-column:" + name.unwrap_or(""))
) {
self.width_cache.set(key, resolved)
}
resolved
}
}
}
///|
fn ReadFormatting::row_height(
self : ReadFormatting,
name : String?,
) -> String? raise OdsError {
let key = (name is Some(_), name.unwrap_or(""))
if self.height_cache.get(key) is Some(cached) {
return cached
}
let props = match self.source.effective("table-row", name) {
Some(p) => p
None => return None
}
let prefix = read_style + "table-row-properties@"
let height_key = prefix + read_style + "row-height"
let optimal_key = prefix + read_style + "use-optimal-row-height"
for k, v in props {
let neutral_break = (
k ==
prefix +
"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0#break-before" ||
k ==
prefix +
"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0#break-after"
) &&
v == "auto"
if k != height_key && !(k == optimal_key && v == "false") && !neutral_break {
return None
}
}
let result = match props.get(height_key) {
Some(v) => physical_length(v).map(x => x.0)
None => None
}
if self.height_cache.length() < 2049 {
self.height_cache.set(key, result)
}
result
}