///|
priv struct SharedStringEntry {
text : String
runs : Array[RichTextRun]?
}
///|
priv struct InlineStringEntry {
text : String
runs : Array[RichTextRun]?
}
///|
fn remove_lexical_element_subtrees(
xml : StringView,
tag_name : StringView,
) -> String raise XlsxError {
let first = match find_xml_open_tag_start(xml, tag_name) {
Some(value) => value
None => return xml.to_owned()
}
let output = StringBuilder::new(size_hint=xml.length())
let mut cursor = 0
let mut start = first
for ;; {
output.write_view(xml[cursor:start])
let open_end = match xml_open_tag_end_from(xml, start + 1) {
Some(value) => value
None => raise InvalidXml(msg="\{tag_name.to_owned()} tag not closed")
}
if xml[start + 1:open_end].trim().has_suffix("/") {
cursor = open_end + 1
} else {
let (close_start, close_end) = match
find_xml_close_tag_from(xml, tag_name, open_end + 1) {
Some(value) => value
None => raise InvalidXml(msg="\{tag_name.to_owned()} close missing")
}
ignore(close_start)
cursor = close_end + 1
}
match find_xml_open_tag_start_from(xml, tag_name, cursor) {
Some(next) => start = next
None => break
}
}
output.write_view(xml[cursor:])
output.to_string()
}
///|
fn parse_text_nodes(xml : StringView) -> String raise XlsxError {
// Phonetic guides (`rPh`) are annotations, not displayed cell text.
let semantic = remove_lexical_element_subtrees(xml, "rPh")
let sb = StringBuilder::new()
let mut cursor = 0
while find_xml_open_tag_start_from(semantic, "t", cursor) is Some(start) {
let open_end = match xml_open_tag_end_from(semantic, start + 1) {
Some(value) => value
None => raise InvalidXml(msg="shared string text tag not closed")
}
if semantic[start + 1:open_end].trim().has_suffix("/") {
cursor = open_end + 1
continue
}
let (close_start, close_end) = match
find_xml_close_tag_from(semantic, "t", open_end + 1) {
Some(value) => value
None => raise InvalidXml(msg="shared string text close missing")
}
sb.write_view(unescape_xml_text(semantic[open_end + 1:close_start]))
cursor = close_end + 1
}
sb.to_string()
}
///|
/// Read a boolean font flag from an `rPr` body: `true` if the ``
/// element is present and its `val` attribute (default `"1"`) is truthy,
/// `false` if the element is absent or `val` is falsy.
fn read_font_bool_flag(
rpr_body : StringView,
tag : StringView,
) -> Bool raise XlsxError {
match tag_attributes_in(rpr_body, tag) {
Some(attrs) =>
match attr_value(attrs, "val") {
Some(val) => parse_bool_attr(val)
None => true
}
None => false
}
}
///|
fn parse_rich_text_font(body : StringView) -> RichTextFont? raise XlsxError {
let rpr_body = match extract_tag_body_from(body, "rPr") {
Some(value) => value
None => return None
}
let mut bold = false
let mut italic = false
let mut strike = false
let mut outline = false
let mut shadow = false
let mut condense = false
let mut extended = false
let mut underline : String? = None
let mut size : Double? = None
let mut color : String? = None
let mut color_theme : Int? = None
let mut color_indexed : Int? = None
let mut color_tint : Double? = None
let mut charset : Int? = None
let mut family_number : Int? = None
let mut scheme : String? = None
let mut vert_align : String? = None
let mut family : String? = None
let mut has_any = false
// Each of these boolean font flags is present-with-optional-val="0"; a flag
// that resolves true marks the font as non-default. `read_font_bool_flag`
// centralizes that parse. (`extend` is the XML tag for the `extended` field.)
bold = read_font_bool_flag(rpr_body, "b")
if bold {
has_any = true
}
italic = read_font_bool_flag(rpr_body, "i")
if italic {
has_any = true
}
strike = read_font_bool_flag(rpr_body, "strike")
if strike {
has_any = true
}
outline = read_font_bool_flag(rpr_body, "outline")
if outline {
has_any = true
}
shadow = read_font_bool_flag(rpr_body, "shadow")
if shadow {
has_any = true
}
condense = read_font_bool_flag(rpr_body, "condense")
if condense {
has_any = true
}
extended = read_font_bool_flag(rpr_body, "extend")
if extended {
has_any = true
}
match tag_attributes_in(rpr_body, "u") {
Some(tag) => {
let value = match attr_value(tag, "val") {
Some(val) => val
None => "single"
}
if value != "none" && value != "0" {
underline = Some(value.to_string())
has_any = true
}
}
None => ()
}
match tag_attributes_in(rpr_body, "sz") {
Some(tag) =>
match attr_value(tag, "val") {
Some(val) =>
try @string.parse_double(val) catch {
_ => ()
} noraise {
value => {
size = Some(value)
has_any = true
}
}
None => ()
}
None => ()
}
match tag_attributes_in(rpr_body, "charset") {
Some(tag) =>
match attr_value(tag, "val") {
Some(val) =>
try @string.parse_int(val, base=10) catch {
_ => ()
} noraise {
value => {
charset = Some(value)
has_any = true
}
}
None => ()
}
None => ()
}
match tag_attributes_in(rpr_body, "family") {
Some(tag) =>
match attr_value(tag, "val") {
Some(val) =>
try @string.parse_int(val, base=10) catch {
_ => ()
} noraise {
value => {
family_number = Some(value)
has_any = true
}
}
None => ()
}
None => ()
}
match tag_attributes_in(rpr_body, "rFont") {
Some(tag) =>
match attr_value(tag, "val") {
Some(val) => {
family = Some(val)
has_any = true
}
None => ()
}
None => ()
}
match tag_attributes_in(rpr_body, "color") {
Some(tag) => {
match attr_value(tag, "rgb") {
Some(val) => {
color = Some(val)
has_any = true
}
None => ()
}
match attr_value(tag, "theme") {
Some(val) =>
try @string.parse_int(val, base=10) catch {
_ => raise InvalidXml(msg="rich text color theme invalid")
} noraise {
value => {
color_theme = Some(value)
has_any = true
}
}
None => ()
}
match attr_value(tag, "indexed") {
Some(val) =>
try @string.parse_int(val, base=10) catch {
_ => raise InvalidXml(msg="rich text color indexed invalid")
} noraise {
value => {
color_indexed = Some(value)
has_any = true
}
}
None => ()
}
match attr_value(tag, "tint") {
Some(val) =>
try @string.parse_double(val) catch {
_ => raise InvalidXml(msg="rich text color tint invalid")
} noraise {
value => {
color_tint = Some(value)
has_any = true
}
}
None => ()
}
}
None => ()
}
match tag_attributes_in(rpr_body, "vertAlign") {
Some(tag) =>
match attr_value(tag, "val") {
Some(val) => {
vert_align = Some(val.to_string())
has_any = true
}
None => ()
}
None => ()
}
match tag_attributes_in(rpr_body, "scheme") {
Some(tag) =>
match attr_value(tag, "val") {
Some(val) => {
scheme = Some(val.to_string())
has_any = true
}
None => ()
}
None => ()
}
if has_any {
Some({
bold,
italic,
strike,
outline,
shadow,
condense,
extended,
underline,
size,
color,
color_theme,
color_indexed,
color_tint,
charset,
family_number,
scheme,
vert_align,
family,
})
} else {
None
}
}
///|
fn parse_rich_text_runs(xml : StringView) -> Array[RichTextRun] raise XlsxError {
let runs : Array[RichTextRun] = []
let xml_str = xml.to_owned()
let mut index = 0
while index < xml_str.length() {
let rest = xml_str[index:]
let rel = match rest.find(" pos
None => break
}
let start = index + rel
let after = start + 2
if after >= xml_str.length() {
break
}
let next = xml_str[after]
if next != '>' &&
next != ' ' &&
next != '\t' &&
next != '\n' &&
next != '\r' {
index = after
continue
}
let tail = xml_str[after:]
let open_end_rel = match tail.find(">") {
Some(pos) => pos
None => break
}
let body_start = after + open_end_rel + 1
let body_rest = xml_str[body_start:]
let (close_rel, close_end_rel) = match
find_xml_close_tag_from(body_rest, "r", 0) {
Some((pos, end)) => (pos, end)
None => break
}
let body_end = body_start + close_rel
let body = xml_str[body_start:body_end]
let run_text = parse_text_nodes(body)
let font = parse_rich_text_font(body)
runs.push({ text: run_text, font })
index = body_start + close_end_rel + 1
}
runs
}
///|
fn parse_shared_strings(
xml : StringView,
budget? : ReadBudget,
) -> Array[SharedStringEntry] raise XlsxError {
match budget {
Some(value) => {
value.checkpoint()
value.charge_work(xml.length())
}
None => ()
}
let scanner = @ooxml.XmlStartTagScanner::new(
xml,
cancelled=match budget {
Some(value) => value.cancelled
None => () => false
},
)
if !workbook_scanner_next(scanner) ||
scanner.depth() != 1 ||
scanner.local_name() != "sst" ||
(
scanner.namespace_uri() != "" &&
scanner.namespace_uri() != transitional_spreadsheet_namespace &&
scanner.namespace_uri() != strict_spreadsheet_namespace
) {
raise InvalidXml(msg="shared strings document element is invalid")
}
let shared_strings_namespace = scanner.namespace_uri().to_owned()
while workbook_scanner_next(scanner) {
if scanner.namespace_uri() == shared_strings_namespace &&
scanner.local_name() == "si" &&
scanner.depth() != 2 {
raise InvalidXml(msg="shared string item is not a direct child")
}
}
match budget {
Some(value) => {
value.checkpoint()
value.charge_work(xml.length())
}
None => ()
}
let values : Array[SharedStringEntry] = []
let mut cursor = 0
while find_xml_open_tag_start_from(xml, "si", cursor) is Some(start) {
match budget {
Some(value) => value.checkpoint()
None => ()
}
let open_end = match xml_open_tag_end_from(xml, start + 1) {
Some(value) => value
None => raise InvalidXml(msg="shared string item tag not closed")
}
let (close_start, close_end) = match
find_xml_close_tag_from(xml, "si", open_end + 1) {
Some(value) => value
None => raise InvalidXml(msg="shared string item close missing")
}
let body = xml[open_end + 1:close_start]
let runs = if body.contains(" [] }
if parsed.length() > 0 {
Some(parsed)
} else {
None
}
} else {
None
}
values.push({ text: parse_text_nodes(body), runs })
cursor = close_end + 1
}
values
}
///|
test "shared strings reject nested same-namespace index decoys" {
let valid =
#|real
let values = parse_shared_strings(valid)
assert_eq(values.length(), 1)
assert_eq(values[0].text, "real")
let nested =
#|decoyreal
try parse_shared_strings(nested) catch {
InvalidXml(msg~) =>
assert_eq(msg, "shared string item is not a direct child")
_ => fail("unexpected nested shared-string error")
} noraise {
_ => fail("nested shared-string decoy was accepted")
}
}