mirror of
https://github.com/storytold/craft-libs.git
synced 2026-11-14 06:22:24 +00:00
craft-xlsx: read parts with canonical prefixes so kept trees can be written back (#1031)
The part layer read every part by local name, so extLst children (x14:, xr:, xm:), mc:AlternateContent and r:id lost their namespaces and could not be written back. Parts are now read with one fixed prefix per namespace (the main namespace unprefixed), mc:Ignorable and mc:Choice/@Requires are rewritten to match, and ns::declare prepares a kept tree for writing. craft-xml: a namespace listed with the empty prefix reads unprefixed.
This commit is contained in:
1 parent
1e3d3968c1
commit
34aa8c1cfd
8 files changed
+225
-11
No files matched your search
@@ -14,6 +14,12 @@ This is the part layer. `Package::open` opens the container through `craft-opc`
|
||||
| `sheet(entry, each)` | each `Row` of `Cell`s to a callback, then a `SheetPart`: the rest of the worksheet as a tree, its relationships, columns and merged cells |
|
||||
| `opc()`, `read_xml()`, `optional_xml()`, `rels()` | any other part |
|
||||
|
||||
Every part is read with fixed prefixes (`ns::CANONICAL`): the SpreadsheetML main namespace
|
||||
(transitional or strict) unprefixed, the others by `ns::NAMESPACES` (`r:id`, `mc:AlternateContent`,
|
||||
`x14:sparklineGroups`, `xm:sqref`), and the prefixes in `mc:Ignorable` and `mc:Choice/@Requires`
|
||||
rewritten to match. A tree kept from a part goes back out with `ns::declare`, which declares what
|
||||
it uses. A name in a namespace that is not listed reads as `?:name` and is not written.
|
||||
|
||||
A cell keeps the file's terms: a shared-string index, a format index, the formula text as the
|
||||
file spells it, value and cell metadata indexes. `CellValue::resolve` gives the value it shows.
|
||||
Shared formulas are not expanded and formulas are not parsed: that needs a formula grammar.
|
||||
|
||||
@@ -13,6 +13,10 @@
|
||||
//! read without holding it; what is left of the part (properties, columns, merged cells,
|
||||
//! validation, and so on) comes back as a tree ([`SheetPart`]).
|
||||
//!
|
||||
//! Parts are read with fixed prefixes ([`ns`]): the SpreadsheetML main namespace has none
|
||||
//! (`sheetData`), the others have theirs whatever the file used (`r:id`, `x14:sparklineGroups`,
|
||||
//! `mc:AlternateContent`), so a tree kept from a part can be written back with [`ns::declare`].
|
||||
//!
|
||||
//! Nothing is interpreted beyond the format: a cell holds a shared-string index and a format
|
||||
//! index, a width is in the file's units, a formula is the file's text. Every other part
|
||||
//! (drawings, charts, pivot caches, macros) is reachable through [`Package::opc`], so a caller
|
||||
@@ -45,6 +49,7 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
pub mod ns;
|
||||
mod package;
|
||||
mod sheet;
|
||||
mod strings;
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
//! Namespaces: every part is read with one fixed prefix per namespace, whatever prefix the file
|
||||
//! used, so a tree can be matched by name and written back with [`declare`].
|
||||
//!
|
||||
//! SpreadsheetML's main namespace (transitional or strict) has no prefix: `sheetData`, `row`.
|
||||
//! The others have the prefixes in [`NAMESPACES`]: `r:id`, `mc:AlternateContent`,
|
||||
//! `x14:dataValidation`, `xm:sqref`, `a:solidFill`. A name in a namespace that is not listed
|
||||
//! reads as `?:local`; [`craft_xml::El::write_xml`] leaves such names out.
|
||||
|
||||
use craft_xml::{AttrValueHook, Canonical, El, Node, Scope};
|
||||
|
||||
/// SpreadsheetML, transitional (ECMA-376 Part 1, 18).
|
||||
pub const MAIN: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
||||
/// SpreadsheetML, strict (ISO/IEC 29500-1).
|
||||
pub const MAIN_STRICT: &str = "http://purl.oclc.org/ooxml/spreadsheetml/main";
|
||||
/// Relationship references (`r:id`).
|
||||
pub const R: &str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
||||
/// Markup compatibility (ECMA-376 Part 3).
|
||||
pub const MC: &str = "http://schemas.openxmlformats.org/markup-compatibility/2006";
|
||||
|
||||
/// (prefix, namespace URI) for every namespace read with a prefix of its own. Where a prefix has
|
||||
/// several URIs (transitional and strict), the first is the one [`declare`] writes.
|
||||
pub const NAMESPACES: &[(&str, &str)] = &[
|
||||
("", MAIN),
|
||||
("", MAIN_STRICT),
|
||||
("r", R),
|
||||
("r", "http://purl.oclc.org/ooxml/officeDocument/relationships"),
|
||||
("mc", MC),
|
||||
("x12ac", "http://schemas.microsoft.com/office/spreadsheetml/2011/1/ac"),
|
||||
("x14ac", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac"),
|
||||
("x14", "http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"),
|
||||
("x15ac", "http://schemas.microsoft.com/office/spreadsheetml/2010/11/ac"),
|
||||
("x15", "http://schemas.microsoft.com/office/spreadsheetml/2010/11/main"),
|
||||
("x16r2", "http://schemas.microsoft.com/office/spreadsheetml/2015/02/main"),
|
||||
("xr", "http://schemas.microsoft.com/office/spreadsheetml/2014/revision"),
|
||||
("xr2", "http://schemas.microsoft.com/office/spreadsheetml/2015/revision2"),
|
||||
("xr3", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision3"),
|
||||
("xr4", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision4"),
|
||||
("xr5", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision5"),
|
||||
("xr6", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision6"),
|
||||
("xr7", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision7"),
|
||||
("xr8", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision8"),
|
||||
("xr9", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision9"),
|
||||
("xr10", "http://schemas.microsoft.com/office/spreadsheetml/2016/revision10"),
|
||||
("xm", "http://schemas.microsoft.com/office/excel/2006/main"),
|
||||
("xda", "http://schemas.microsoft.com/office/spreadsheetml/2017/dynamicarray"),
|
||||
("rv", "http://schemas.microsoft.com/office/spreadsheetml/2017/richdata"),
|
||||
("rvrel", "http://schemas.microsoft.com/office/spreadsheetml/2022/richvaluerel"),
|
||||
("xcalcf", "http://schemas.microsoft.com/office/spreadsheetml/2018/calcfeatures"),
|
||||
("x18tc", "http://schemas.microsoft.com/office/spreadsheetml/2018/threadedcomments"),
|
||||
("xpdl", "http://schemas.microsoft.com/office/spreadsheetml/2016/pivotdefaultlayout"),
|
||||
("xfpb", "http://schemas.microsoft.com/office/spreadsheetml/2022/featurepropertybag"),
|
||||
("xpy", "http://schemas.microsoft.com/office/spreadsheetml/2023/python"),
|
||||
("a", "http://schemas.openxmlformats.org/drawingml/2006/main"),
|
||||
("a", "http://purl.oclc.org/ooxml/drawingml/main"),
|
||||
("xdr", "http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing"),
|
||||
("xdr", "http://purl.oclc.org/ooxml/drawingml/spreadsheetDrawing"),
|
||||
("c", "http://schemas.openxmlformats.org/drawingml/2006/chart"),
|
||||
("c", "http://purl.oclc.org/ooxml/drawingml/chart"),
|
||||
("cx", "http://schemas.microsoft.com/office/drawing/2014/chartex"),
|
||||
("pic", "http://schemas.openxmlformats.org/drawingml/2006/picture"),
|
||||
("a14", "http://schemas.microsoft.com/office/drawing/2010/main"),
|
||||
("v", "urn:schemas-microsoft-com:vml"),
|
||||
("o", "urn:schemas-microsoft-com:office:office"),
|
||||
("x", "urn:schemas-microsoft-com:office:excel"),
|
||||
("xsi", "http://www.w3.org/2001/XMLSchema-instance"),
|
||||
];
|
||||
|
||||
/// The markup-compatibility attributes whose values are lists of prefixes (or of `prefix:name`).
|
||||
const PREFIX_LISTS: &[&str] = &["mc:Ignorable", "mc:ProcessContent", "mc:MustUnderstand", "mc:PreserveElements", "mc:PreserveAttributes"];
|
||||
|
||||
/// Rewrites the prefixes in `mc:Ignorable` (and its kin) and `mc:Choice/@Requires` to the
|
||||
/// canonical ones. In a list, a prefix of an unlisted namespace is left out; in `Requires` it
|
||||
/// becomes `?`, so that the choice is known to need something this crate cannot carry.
|
||||
fn mc_prefixes(el: &str, attr: &str, value: &str, scope: &Scope<'_>) -> Option<String> {
|
||||
let requires = el == "mc:Choice" && attr == "Requires";
|
||||
if !requires && !PREFIX_LISTS.contains(&attr) {
|
||||
return None;
|
||||
}
|
||||
let canon = |p: &str| scope.uri(p).map_or("?", |uri| CANONICAL.prefix(uri));
|
||||
let tokens = value.split_whitespace().filter_map(|t| {
|
||||
let (p, rest) = t.split_once(':').map_or((t, None), |(p, l)| (p, Some(l)));
|
||||
let c = canon(p);
|
||||
if requires {
|
||||
return Some(c.to_string());
|
||||
}
|
||||
(c != "?" && !c.is_empty()).then(|| rest.map_or_else(|| c.to_string(), |l| format!("{c}:{l}")))
|
||||
});
|
||||
Some(tokens.collect::<Vec<_>>().join(" "))
|
||||
}
|
||||
|
||||
const MC_HOOK: &AttrValueHook<'static> = &mc_prefixes;
|
||||
|
||||
/// The canonical names parts are read with.
|
||||
pub const CANONICAL: Canonical<'static> = Canonical { namespaces: NAMESPACES, unknown: "?", element: None, attr_value: Some(MC_HOOK) };
|
||||
|
||||
/// Prepares a tree read with [`CANONICAL`] for writing: declares on `root` every namespace its
|
||||
/// names and markup-compatibility attributes use (the main namespace as the default one), and
|
||||
/// drops each `mc:Choice` that requires a namespace this crate does not know, since its content
|
||||
/// was not kept. Declarations already on `root` are replaced.
|
||||
pub fn declare(root: &mut El) {
|
||||
drop_unknown_choices(root);
|
||||
let mut used: Vec<&str> = Vec::new();
|
||||
collect(root, &mut used);
|
||||
let mut decls = vec![("xmlns".to_string(), MAIN.to_string())];
|
||||
for (p, uri) in NAMESPACES {
|
||||
if !p.is_empty() && used.contains(p) && !decls.iter().any(|(k, _)| k.strip_prefix("xmlns:") == Some(p)) {
|
||||
decls.push((format!("xmlns:{p}"), uri.to_string()));
|
||||
}
|
||||
}
|
||||
root.attrs.retain(|(k, _)| k != "xmlns" && !k.starts_with("xmlns:"));
|
||||
decls.append(&mut root.attrs);
|
||||
root.attrs = decls;
|
||||
}
|
||||
|
||||
fn drop_unknown_choices(e: &mut El) {
|
||||
e.children
|
||||
.retain(|n| !matches!(n, Node::El(c) if c.name == "mc:Choice" && c.attr("Requires").is_some_and(|r| r.split_whitespace().any(|p| p == "?"))));
|
||||
for c in e.elements_mut() {
|
||||
drop_unknown_choices(c);
|
||||
}
|
||||
}
|
||||
|
||||
fn collect<'a>(e: &'a El, used: &mut Vec<&'a str>) {
|
||||
if let Some((p, _)) = e.name.split_once(':') {
|
||||
used.push(p);
|
||||
}
|
||||
for (k, v) in &e.attrs {
|
||||
if let Some((p, _)) = k.split_once(':') {
|
||||
used.push(p);
|
||||
}
|
||||
if PREFIX_LISTS.contains(&k.as_str()) || (e.name == "mc:Choice" && k == "Requires") {
|
||||
used.extend(v.split_whitespace().map(|t| t.split_once(':').map_or(t, |(p, _)| p)));
|
||||
}
|
||||
}
|
||||
for c in e.elements() {
|
||||
collect(c, used);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn read(xml: &str) -> El {
|
||||
craft_xml::Options::canonical(&CANONICAL).parse(xml.as_bytes()).unwrap_or_default()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn names_get_fixed_prefixes_and_write_back() {
|
||||
let x = format!(
|
||||
r#"<s:worksheet xmlns:s="{MAIN_STRICT}" xmlns:rel="http://purl.oclc.org/ooxml/officeDocument/relationships" xmlns:m="{MC}" xmlns:e="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" xmlns:z="urn:future" m:Ignorable="e z"><s:sheetData/><s:drawing rel:id="rId1"/><m:AlternateContent><m:Choice Requires="z"><z:new/></m:Choice><m:Choice Requires="e"><s:a/></m:Choice><m:Fallback><s:b/></m:Fallback></m:AlternateContent></s:worksheet>"#
|
||||
);
|
||||
let mut root = read(&x);
|
||||
assert_eq!(root.attr("mc:Ignorable"), Some("x14ac"));
|
||||
assert_eq!(root.child("drawing").and_then(|d| d.attr("r:id")), Some("rId1"));
|
||||
let choices: Vec<_> = root.find_all("mc:Choice").filter_map(|c| c.attr("Requires")).collect();
|
||||
assert_eq!(choices, ["?", "x14ac"]);
|
||||
declare(&mut root);
|
||||
let out = root.to_xml();
|
||||
assert_eq!(
|
||||
out,
|
||||
format!(
|
||||
r#"<worksheet xmlns="{MAIN}" xmlns:r="{R}" xmlns:mc="{MC}" xmlns:x14ac="http://schemas.microsoft.com/office/spreadsheetml/2009/9/ac" mc:Ignorable="x14ac"><sheetData/><drawing r:id="rId1"/><mc:AlternateContent><mc:Choice Requires="x14ac"><a/></mc:Choice><mc:Fallback><b/></mc:Fallback></mc:AlternateContent></worksheet>"#
|
||||
)
|
||||
);
|
||||
// What was written reads back to the same tree.
|
||||
let mut again = read(&out);
|
||||
declare(&mut again);
|
||||
assert_eq!(again, root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extension_lists_keep_their_namespaces() {
|
||||
let x = r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"><extLst><ext uri="{CCE6A557-97BC-4b89-ADB6-D9C93CAAB3DF}" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"><x14:dataValidations xmlns:xm="http://schemas.microsoft.com/office/excel/2006/main" count="1"><x14:dataValidation type="list"><x14:formula1><xm:f>Other!$A$1:$A$3</xm:f></x14:formula1><xm:sqref>G1:G4</xm:sqref></x14:dataValidation></x14:dataValidations></ext></extLst></worksheet>"#;
|
||||
let mut root = read(x);
|
||||
assert!(root.find("dataValidation").is_none(), "an extension's element is not the main one");
|
||||
assert_eq!(root.find("xm:sqref").map(|e| e.text().into_owned()), Some("G1:G4".into()));
|
||||
declare(&mut root);
|
||||
assert!(root.to_xml().starts_with(r#"<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main" xmlns:x14="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main" xmlns:xm="#));
|
||||
assert_eq!(read(&root.to_xml()), read(x));
|
||||
}
|
||||
}
|
||||
@@ -90,10 +90,10 @@ impl<'a> Package<'a> {
|
||||
&mut self.opc
|
||||
}
|
||||
|
||||
/// The XML options parts are read with: local names, an undefined entity kept as written,
|
||||
/// [`Limits::xml`].
|
||||
/// The XML options parts are read with: the names of [`crate::ns::CANONICAL`], an undefined
|
||||
/// entity kept as written, [`Limits::xml`].
|
||||
pub fn xml_options(&self) -> Options<'static> {
|
||||
Options { unknown_entity: UnknownEntity::Literal, ..Options::local() }.with_limits(self.limits.xml)
|
||||
Options { unknown_entity: UnknownEntity::Literal, ..Options::canonical(&crate::ns::CANONICAL) }.with_limits(self.limits.xml)
|
||||
}
|
||||
|
||||
/// Part `part` as XML; `Ok(None)` when there is no such part.
|
||||
|
||||
@@ -37,7 +37,8 @@ pub struct SheetEntry {
|
||||
/// `sheetId`, 0 when absent.
|
||||
pub sheet_id: u32,
|
||||
pub visibility: Visibility,
|
||||
/// The relationship id (`r:id`), empty when absent.
|
||||
/// The relationship id (`r:id`, or a plain `id` from a writer that left the namespace
|
||||
/// out), empty when absent.
|
||||
pub rel_id: String,
|
||||
pub kind: SheetKind,
|
||||
/// The sheet's part, `None` when the relationship is missing or points outside the package.
|
||||
@@ -76,7 +77,7 @@ impl Workbook {
|
||||
let mut wb = Workbook { date_1904: root.child("workbookPr").is_some_and(|p| p.flag("date1904", false)), ..Workbook::default() };
|
||||
if let Some(sheets) = root.child("sheets") {
|
||||
for s in sheets.children("sheet") {
|
||||
let rel_id = s.attr("id").unwrap_or_default().to_string();
|
||||
let rel_id = s.attr_or_plain("r:id").unwrap_or_default().to_string();
|
||||
let rel = rels.by_id(&rel_id);
|
||||
wb.sheets.push(SheetEntry {
|
||||
name: s.attr("name").unwrap_or_default().to_string(),
|
||||
|
||||
@@ -22,7 +22,8 @@ fn strict() -> Vec<u8> {
|
||||
r#"<x:worksheet {NS}><x:cols><x:col min="2" max="3" width="20.5" customWidth="1"/><x:col min="0" max="1"/></x:cols><x:sheetData>
|
||||
<x:row r="1"><x:c r="A1" t="s"><x:v>1</x:v></x:c><x:c r="B1" s="1"><x:v>1</x:v></x:c><x:c r="C1"><x:f>B1*2</x:f><x:v>2</x:v></x:c></x:row>
|
||||
<x:row r="2"><x:c r="A2" t="d"><x:v>1904-01-03</x:v></x:c><x:c r="B2" t="s"><x:v>9</x:v></x:c></x:row></x:sheetData>
|
||||
<x:mergeCells><x:mergeCell ref="A3:B4"/><x:mergeCell ref="C3"/></x:mergeCells><x:pageSetup orientation="landscape"/></x:worksheet>"#
|
||||
<x:mergeCells><x:mergeCell ref="A3:B4"/><x:mergeCell ref="C3"/></x:mergeCells><x:pageSetup orientation="landscape"/><x:drawing r:id="d1"/>
|
||||
<x:extLst><x:ext uri="{{05C60535-1F16-4fd2-B633-F4F36F0B64E0}}" xmlns:e="http://schemas.microsoft.com/office/spreadsheetml/2009/9/main"><e:sparklineGroups xmlns:m="http://schemas.microsoft.com/office/excel/2006/main"><e:sparklineGroup><e:sparklines><e:sparkline><m:f>A1:E1</m:f><m:sqref>F1</m:sqref></e:sparkline></e:sparklines></e:sparklineGroup></e:sparklineGroups></x:ext></x:extLst></x:worksheet>"#
|
||||
);
|
||||
zip(&[
|
||||
("[Content_Types].xml", b"<Types xmlns=\"http://schemas.openxmlformats.org/package/2006/content-types\"/>"),
|
||||
@@ -82,6 +83,13 @@ fn a_strict_workbook_with_prefixes() {
|
||||
assert_eq!((part.columns[0].first, part.columns[0].last, part.columns[0].width), (1, 2, Some(20.5)));
|
||||
assert_eq!(part.root.child("pageSetup").and_then(|e| e.attr("orientation")), Some("landscape"));
|
||||
assert!(part.root.child("sheetData").is_some_and(|d| d.elements().next().is_none()));
|
||||
// Other namespaces keep fixed prefixes, so the rest of the part can be written back.
|
||||
assert_eq!(part.root.child("drawing").and_then(|d| d.attr("r:id")), Some("d1"));
|
||||
assert_eq!(part.root.find("xm:sqref").map(|e| e.text().into_owned()).as_deref(), Some("F1"));
|
||||
let mut root = part.root.clone();
|
||||
craft_xlsx::ns::declare(&mut root);
|
||||
let again = craft_xml::Options::canonical(&craft_xlsx::ns::CANONICAL).parse(root.to_xml().as_bytes()).unwrap();
|
||||
assert_eq!(again, part.root);
|
||||
assert_eq!((part.rows, part.dropped, part.damaged.as_deref()), (2, 0, None));
|
||||
let values: Vec<_> = cells.iter().map(|c| c.value.resolve(&strings, true)).collect();
|
||||
assert_eq!(values, [Value::text("one"), Value::Number(1.0), Value::Number(2.0), Value::Number(2.0), Value::Empty]);
|
||||
|
||||
@@ -81,7 +81,8 @@ pub type AttrValueHook<'a> = dyn Fn(&str, &str, &str, &Scope<'_>) -> Option<Stri
|
||||
/// Canonical prefixes by namespace URI.
|
||||
///
|
||||
/// - An element in a listed namespace is named `prefix:local`; in an unlisted one,
|
||||
/// `unknown:local`; in no namespace (or with a prefix nothing declares), `local`.
|
||||
/// `unknown:local`; in no namespace (or with a prefix nothing declares), `local`. A namespace
|
||||
/// listed with the empty prefix reads unprefixed too: a vocabulary's own namespace.
|
||||
/// - A prefixed attribute is named the same way. An unprefixed attribute is in no namespace
|
||||
/// and keeps its name.
|
||||
/// - The `xml` prefix needs no entry: `xml:space` stays `xml:space`.
|
||||
@@ -511,7 +512,7 @@ impl<'o, 'i> Events<'o, 'i> {
|
||||
let listed = canon.prefix(uri);
|
||||
let parent = self.open.last().map(|o| o.name.as_str());
|
||||
let chosen = canon.element.and_then(|hook| hook(&NsName { prefix: listed, uri, local, parent })).unwrap_or(listed);
|
||||
format!("{chosen}:{local}")
|
||||
if chosen.is_empty() { local.to_string() } else { format!("{chosen}:{local}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,9 +522,9 @@ impl<'o, 'i> Events<'o, 'i> {
|
||||
Names::AsWritten => raw.to_string(),
|
||||
Names::Local => local_of(raw).to_string(),
|
||||
Names::Canonical(canon) => match raw.split_once(':') {
|
||||
Some((prefix, local)) => match self.uri(prefix) {
|
||||
Some(uri) => format!("{}:{local}", canon.prefix(uri)),
|
||||
None => local.to_string(),
|
||||
Some((prefix, local)) => match self.uri(prefix).map(|uri| canon.prefix(uri)) {
|
||||
Some(p) if !p.is_empty() => format!("{p}:{local}"),
|
||||
_ => local.to_string(),
|
||||
},
|
||||
None => raw.to_string(),
|
||||
},
|
||||
|
||||
@@ -64,6 +64,17 @@ fn word_unknown_namespaces_read_behind_a_question_mark_and_are_not_written() {
|
||||
assert_eq!(body.to_xml(), r#"<w:body><w:p w:old="2"><w:r>t</w:r></w:p><x/><plain/></w:body>"#);
|
||||
}
|
||||
|
||||
/// A vocabulary's own namespace listed with the empty prefix reads unprefixed (SpreadsheetML
|
||||
/// keeps its main namespace as the default one), whatever prefix the document used.
|
||||
#[test]
|
||||
fn an_empty_canonical_prefix_reads_unprefixed() {
|
||||
const MAIN: &str = "http://schemas.openxmlformats.org/spreadsheetml/2006/main";
|
||||
let canon = Canonical::new(&[("", MAIN), ("x14", "urn:x14")]);
|
||||
let x = format!(r#"<x:worksheet xmlns:x="{MAIN}" xmlns:e="urn:x14"><x:sheetData x:a="1" e:b="2"/><e:id/><row xmlns="{MAIN}"/></x:worksheet>"#);
|
||||
let root = Options::canonical(&canon).parse(x.as_bytes()).unwrap();
|
||||
assert_eq!(root.to_xml(), r#"<worksheet><sheetData a="1" x14:b="2"/><x14:id/><row/></worksheet>"#);
|
||||
}
|
||||
|
||||
/// WordCraft reads the one Strict WordprocessingDrawing namespace as Word 2010's several, by
|
||||
/// element and parent: the element hook.
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user