///|
pub struct FormControl {
cell : String
control_type : String
text : String
mut macro_name : String?
mut checked : Bool?
mut cell_link : String?
mut width : Int?
mut height : Int?
mut current_val : Int?
mut min_val : Int?
mut max_val : Int?
mut inc_change : Int?
mut page_change : Int?
mut horizontally : Bool?
mut format : GraphicOptions
mut paragraph : Array[RichTextRun]
} derive(Debug)
///|
let max_form_control_value = 30000
///|
fn normalize_form_control_type(
control_type : StringView,
) -> (String, String) raise XlsxError {
let lower = control_type.to_owned().to_lower()
let normalized = lower
.replace_all(old=" ", new="")
.replace_all(old="_", new="")
match normalized {
"button" => ("Button", "Button")
"checkbox" => ("CheckBox", "Checkbox")
"radiobutton" | "optionbutton" | "radio" => ("OptionButton", "Radio")
"scroll" | "scrollbar" => ("ScrollBar", "Scroll")
"spin" | "spinbutton" | "spinner" => ("SpinButton", "Spin")
"gbox" | "groupbox" => ("GroupBox", "GBox")
"label" => ("Label", "Label")
_ => raise InvalidSheetOperation(msg="unsupported form control type")
}
}
///|
pub fn FormControl::new(
cell : String,
control_type : String,
text? : String = "",
) -> FormControl raise XlsxError {
if cell == "" {
raise InvalidSheetOperation(msg="form control cell empty")
}
if control_type == "" {
raise InvalidSheetOperation(msg="form control type empty")
}
let (canonical_type, _) = normalize_form_control_type(control_type)
ignore(cell_ref_to_rc(cell))
{
cell,
control_type: canonical_type,
text,
macro_name: None,
checked: None,
cell_link: None,
width: None,
height: None,
current_val: None,
min_val: None,
max_val: None,
inc_change: None,
page_change: None,
horizontally: None,
format: GraphicOptions::new(),
paragraph: [],
}
}
///|
pub fn FormControl::set_macro_name(
self : FormControl,
value : StringView,
) -> Unit {
let v = value.to_owned()
self.macro_name = if v == "" { None } else { Some(v) }
}
///|
pub fn FormControl::set_checked(self : FormControl, checked : Bool) -> Unit {
self.checked = Some(checked)
}
///|
pub fn FormControl::set_cell_link(
self : FormControl,
cell_link : StringView,
) -> Unit raise XlsxError {
let value = cell_link.to_owned()
if value == "" {
self.cell_link = None
return
}
ignore(cell_ref_to_rc(value))
self.cell_link = Some(value)
}
///|
pub fn FormControl::set_width(
self : FormControl,
width : Int,
) -> Unit raise XlsxError {
if width <= 0 {
raise InvalidSheetOperation(msg="form control width invalid")
}
self.width = Some(width)
}
///|
pub fn FormControl::set_height(
self : FormControl,
height : Int,
) -> Unit raise XlsxError {
if height <= 0 {
raise InvalidSheetOperation(msg="form control height invalid")
}
self.height = Some(height)
}
///|
pub fn FormControl::set_current_val(
self : FormControl,
value : Int,
) -> Unit raise XlsxError {
if value < 0 || value > max_form_control_value {
raise InvalidSheetOperation(msg="form control current value invalid")
}
self.current_val = Some(value)
}
///|
pub fn FormControl::set_min_val(
self : FormControl,
value : Int,
) -> Unit raise XlsxError {
if value < 0 || value > max_form_control_value {
raise InvalidSheetOperation(msg="form control min value invalid")
}
self.min_val = Some(value)
}
///|
pub fn FormControl::set_max_val(
self : FormControl,
value : Int,
) -> Unit raise XlsxError {
if value < 0 || value > max_form_control_value {
raise InvalidSheetOperation(msg="form control max value invalid")
}
self.max_val = Some(value)
}
///|
pub fn FormControl::set_inc_change(
self : FormControl,
value : Int,
) -> Unit raise XlsxError {
if value < 0 || value > max_form_control_value {
raise InvalidSheetOperation(msg="form control inc change invalid")
}
self.inc_change = Some(value)
}
///|
pub fn FormControl::set_page_change(
self : FormControl,
value : Int,
) -> Unit raise XlsxError {
if value < 0 || value > max_form_control_value {
raise InvalidSheetOperation(msg="form control page change invalid")
}
self.page_change = Some(value)
}
///|
pub fn FormControl::set_horizontally(self : FormControl, value : Bool) -> Unit {
self.horizontally = Some(value)
}
///|
pub fn FormControl::set_format(
self : FormControl,
format : GraphicOptions,
) -> Unit {
self.format = format
}
///|
pub fn FormControl::set_paragraph(
self : FormControl,
paragraph : Array[RichTextRun],
) -> Unit {
self.paragraph = paragraph
}
///|
struct FormControlVmlPreset {
filled : String?
fill_color : String?
stroked : String?
stroke_color : String?
button : String?
fill_xml : String?
auto_fill : String?
text_h_align : String?
text_v_align : String?
no_three_d : Bool
first_button : Bool
} derive(Debug)
///|
fn form_control_vml_preset(control_type : StringView) -> FormControlVmlPreset? {
let lower = control_type.to_owned().to_lower()
let normalized = lower
.replace_all(old=" ", new="")
.replace_all(old="_", new="")
match normalized {
"button" =>
Some({
filled: None,
fill_color: Some("buttonFace [67]"),
stroked: None,
stroke_color: Some("windowText [64]"),
button: Some("t"),
fill_xml: Some(
"\n",
),
auto_fill: Some("True"),
text_h_align: Some("Center"),
text_v_align: Some("Center"),
no_three_d: false,
first_button: false,
})
"checkbox" =>
Some({
filled: Some("f"),
fill_color: Some("window [65]"),
stroked: Some("f"),
stroke_color: Some("windowText [64]"),
button: None,
fill_xml: None,
auto_fill: Some("True"),
text_h_align: None,
text_v_align: Some("Center"),
no_three_d: true,
first_button: false,
})
"radiobutton" | "optionbutton" | "radio" =>
Some({
filled: Some("f"),
fill_color: Some("window [65]"),
stroked: Some("f"),
stroke_color: Some("windowText [64]"),
button: None,
fill_xml: None,
auto_fill: Some("False"),
text_h_align: None,
text_v_align: Some("Center"),
no_three_d: true,
first_button: true,
})
"scroll" | "scrollbar" =>
Some({
filled: None,
fill_color: None,
stroked: Some("f"),
stroke_color: Some("windowText [64]"),
button: None,
fill_xml: None,
auto_fill: None,
text_h_align: None,
text_v_align: None,
no_three_d: false,
first_button: false,
})
"spin" | "spinbutton" | "spinner" =>
Some({
filled: None,
fill_color: None,
stroked: Some("f"),
stroke_color: Some("windowText [64]"),
button: None,
fill_xml: None,
auto_fill: Some("False"),
text_h_align: None,
text_v_align: None,
no_three_d: false,
first_button: false,
})
"gbox" | "groupbox" =>
Some({
filled: Some("f"),
fill_color: None,
stroked: Some("f"),
stroke_color: Some("windowText [64]"),
button: None,
fill_xml: None,
auto_fill: Some("False"),
text_h_align: None,
text_v_align: None,
no_three_d: true,
first_button: false,
})
"label" =>
Some({
filled: Some("f"),
fill_color: Some("window [65]"),
stroked: Some("f"),
stroke_color: Some("windowText [64]"),
button: None,
fill_xml: None,
auto_fill: Some("False"),
text_h_align: None,
text_v_align: None,
no_three_d: false,
first_button: false,
})
_ => None
}
}
///|
fn write_form_controls_vml(
sheet_id : Int,
sheet : Worksheet,
controls : ArrayView[FormControl],
shape_id_start? : Int = 2048,
budget? : WritePartBudget,
) -> String raise XlsxError {
if !vml_shape_id_range_is_valid(shape_id_start, controls.length()) {
raise InvalidVmlDrawing(msg="form control VML shape id range is invalid")
}
let sb = LimitedXmlBuilder::new(budget)
sb.write_view("\n")
sb.write_view(
"\n",
)
sb.write_view(" \n")
sb.write_view(" \n")
sb.write_view(" \n")
sb.write_view(
" \n",
)
sb.write_view(" \n")
sb.write_view(" \n")
sb.write_view(" \n")
for index, control in controls {
let shape_id = shape_id_start + index
let (_, object_type) = normalize_form_control_type(control.control_type)
let (row, col) = cell_ref_to_rc(control.cell)
let row0 = row - 1
let col0 = col - 1
let base_width_px = match control.width {
Some(v) => v
None => 140
}
let base_height_px = match control.height {
Some(v) => v
None => 60
}
let scale_x = match control.format.scale_x {
Some(v) => if v <= 0.0 { 1.0 } else { v }
None => 1.0
}
let scale_y = match control.format.scale_y {
Some(v) => if v <= 0.0 { 1.0 } else { v }
None => 1.0
}
let width_px = (Double::from_int(base_width_px) * scale_x).to_int()
let height_px = (Double::from_int(base_height_px) * scale_y).to_int()
let mut col_end0 = col0
let mut x2 = width_px
let mut cur_col = col
while x2 >= sheet.col_width_pixels(cur_col) {
x2 = x2 - sheet.col_width_pixels(cur_col)
cur_col = cur_col + 1
col_end0 = col_end0 + 1
}
let mut row_end0 = row0
let mut y2 = height_px
let mut cur_row = row
while y2 >= sheet.row_height_pixels(cur_row) {
y2 = y2 - sheet.row_height_pixels(cur_row)
cur_row = cur_row + 1
row_end0 = row_end0 + 1
}
let anchor = "\{col0}, 0, \{row0}, 0, \{col_end0}, \{x2}, \{row_end0}, \{y2}"
let preset = form_control_vml_preset(control.control_type)
sb.write_view(" {
match p.button {
Some(v) => {
sb.write_view(" o:button=\"")
sb.write_xml_attr(v)
sb.write_view("\"")
}
None => ()
}
match p.filled {
Some(v) => {
sb.write_view(" filled=\"")
sb.write_xml_attr(v)
sb.write_view("\"")
}
None => ()
}
match p.fill_color {
Some(v) => {
sb.write_view(" fillcolor=\"")
sb.write_xml_attr(v)
sb.write_view("\"")
}
None => ()
}
match p.stroked {
Some(v) => {
sb.write_view(" stroked=\"")
sb.write_xml_attr(v)
sb.write_view("\"")
}
None => ()
}
match p.stroke_color {
Some(v) => {
sb.write_view(" strokecolor=\"")
sb.write_xml_attr(v)
sb.write_view("\"")
}
None => ()
}
}
None => ()
}
sb.write_view(
" style=\"position:absolute;73.5pt;width:108pt;height:59.25pt;z-index:1;mso-wrap-style:tight\" o:insetmode=\"auto\">\n",
)
sb.write_view(" \n")
match preset {
Some(p) =>
match p.fill_xml {
Some(v) => sb.write_view(" \{v}")
None => ()
}
None => ()
}
if control.text != "" || control.paragraph.length() > 0 {
sb.write_view(" \n")
sb.write_view(" ")
if control.text != "" {
sb.write_view("")
sb.write_xml_text(control.text)
sb.write_view("")
}
for run in control.paragraph {
sb.write_view(" {
match font.family {
Some(face) => {
sb.write_view(" face=\"")
sb.write_xml_attr(face)
sb.write_view("\"")
}
None => ()
}
match font.size {
Some(sz) => {
let v = (sz * 20.0).to_int()
if v > 0 {
sb.write_view(" size=\"\{v}\"")
}
}
None => ()
}
match font.color {
Some(color) => {
let normalized = if color.has_prefix("#") {
color
} else {
"#\{color}"
}
sb.write_view(" color=\"")
sb.write_xml_attr(normalized)
sb.write_view("\"")
}
None => ()
}
}
None => ()
}
sb.write_view(">")
let font = run.font
match font {
Some(value) => {
if value.bold {
sb.write_view("")
}
if value.italic {
sb.write_view("")
}
match value.underline {
Some("double") => sb.write_view("")
Some("single") => sb.write_view("")
_ => ()
}
}
None => ()
}
sb.write_xml_text(run.text)
sb.write_view("
\r\n")
match font {
Some(value) => {
match value.underline {
Some("double") | Some("single") => sb.write_view("")
_ => ()
}
if value.italic {
sb.write_view("")
}
if value.bold {
sb.write_view("")
}
}
None => ()
}
sb.write_view("")
}
sb.write_view("
\n")
sb.write_view(" \n")
}
sb.write_view(" \n")
sb.write_view(" \{anchor}\n")
match preset {
Some(p) => {
match p.auto_fill {
Some(v) => {
sb.write_view(" ")
sb.write_xml_text(v)
sb.write_view("\n")
}
None => ()
}
match p.text_h_align {
Some(v) => {
sb.write_view(" ")
sb.write_xml_text(v)
sb.write_view("\n")
}
None => ()
}
match p.text_v_align {
Some(v) => {
sb.write_view(" ")
sb.write_xml_text(v)
sb.write_view("\n")
}
None => ()
}
if p.no_three_d {
sb.write_view(" \n")
}
if p.first_button {
sb.write_view(" \n")
}
}
None => ()
}
match control.format.positioning {
Some(Absolute) => {
sb.write_view(" \n")
sb.write_view(" \n")
}
Some(OneCell) => sb.write_view(" \n")
Some(TwoCell) => ()
None => ()
}
match control.format.print_object {
Some(false) =>
sb.write_view(" False\n")
_ => ()
}
match control.macro_name {
Some(name) => {
sb.write_view(" ")
sb.write_xml_text(name)
sb.write_view("\n")
}
None => ()
}
if control.text != "" {
sb.write_view(" ")
sb.write_xml_text(control.text)
sb.write_view("\n")
}
match control.cell_link {
Some(link) => {
sb.write_view(" ")
sb.write_xml_text(link)
sb.write_view("\n")
}
None => ()
}
match control.current_val {
Some(v) => sb.write_view(" \{v}\n")
None => ()
}
match control.min_val {
Some(v) => sb.write_view(" \{v}\n")
None => ()
}
match control.max_val {
Some(v) => sb.write_view(" \{v}\n")
None => ()
}
match control.inc_change {
Some(v) => sb.write_view(" \{v}\n")
None => ()
}
match control.page_change {
Some(v) => sb.write_view(" \{v}\n")
None => ()
}
match control.horizontally {
Some(true) => sb.write_view(" \n")
_ => ()
}
if control.control_type == "ScrollBar" {
sb.write_view(" 15\n")
}
match control.checked {
Some(true) => sb.write_view(" 1\n")
Some(false) => sb.write_view(" 0\n")
None => ()
}
sb.write_view(" \{row0}\n")
sb.write_view(" \{col0}\n")
sb.write_view(" \n")
sb.write_view(" \n")
}
sb.write_view("")
sb.to_string()
}
///|
fn xml_has_tag(body : StringView, tag : StringView) -> Bool {
let t = tag.to_owned()
body.contains("<\{t}/>") ||
body.contains("<\{t} />") ||
body.contains("<\{t}>\{t}>") ||
body.contains("<\{t}>")
}
///|
fn extract_xml_text_tag(body : StringView, tag : StringView) -> String? {
let open = "<\{tag.to_owned()}>"
let close = "\{tag.to_owned()}>"
match body.find(open) {
Some(start) => {
let rest = body[start + open.length():]
let end = match rest.find(close) {
Some(v) => v
None => return None
}
let value = rest[:end]
Some(unescape_xml_text(value))
}
None => None
}
}
///|
fn strip_vml_font_markup(text : StringView) -> String {
let mut s = text.to_owned()
s = s.replace(old="", new="")
s = s.replace(old="", new="")
s = s.replace(old="", new="")
s = s.replace(old="", new="")
s = s.replace(old="", new="")
s = s.replace(old="", new="")
s = s.replace(old="", new="")
s = s.replace(old="
\r\n", new="")
s = s.replace(old="
", new="")
s = s.replace(old="
", new="")
s = s.replace(old="
", new="")
s = s.replace(old="
", new="")
s
}
///|
fn vml_rich_text_font(
bold : Bool,
italic : Bool,
underline : String?,
family : String?,
size : Double?,
color : String?,
) -> RichTextFont {
{
bold,
italic,
strike: false,
outline: false,
shadow: false,
condense: false,
extended: false,
underline,
size,
color,
color_theme: None,
color_indexed: None,
color_tint: None,
charset: None,
family_number: None,
scheme: None,
vert_align: None,
family,
}
}
///|
fn parse_vml_textbox(
shape : StringView,
) -> (String, Array[RichTextRun]) raise XlsxError {
let empty : Array[RichTextRun] = []
let start = match shape.find(" v
None => return ("", empty)
}
let after_textbox = shape[start:]
let div_start = match after_textbox.find(" v
None => return ("", empty)
}
let div_after = after_textbox[div_start:]
let div_open_end = match div_after.find(">") {
Some(v) => v
None => return ("", empty)
}
let div_body = div_after[div_open_end + 1:]
let div_close = match div_body.find("
") {
Some(v) => v
None => return ("", empty)
}
let inner = div_body[:div_close]
if !inner.contains(" v
None => break
}
let font_start = i + font_rel
let font_open = inner_s[font_start:]
let open_end_rel = match font_open.find(">") {
Some(v) => v
None => break
}
let attrs = font_open[5:open_end_rel]
let content_start = font_start + open_end_rel + 1
let after_open = inner_s[content_start:]
let close_rel = match after_open.find("") {
Some(v) => v
None => break
}
let raw_inner = after_open[:close_rel]
let bold = raw_inner.contains("")
let italic = raw_inner.contains("")
let underline = if raw_inner.contains("") {
Some("double")
} else if raw_inner.contains("") {
Some("single")
} else {
None
}
let face = attr_value(attrs, "face")
let size = match attr_value(attrs, "size") {
Some(v) => {
let n = @string.parse_int(v.trim(), base=10) catch { _ => 0 }
if n > 0 {
Some(Double::from_int(n) / 20.0)
} else {
None
}
}
None => None
}
let color = match attr_value(attrs, "color") {
Some(v) => if v.trim() == "" { None } else { Some(v) }
None => None
}
let stripped = strip_vml_font_markup(raw_inner)
let text = unescape_xml_text(stripped)
let has_style = bold ||
italic ||
underline is Some(_) ||
face is Some(_) ||
size is Some(_) ||
color is Some(_)
let font = if has_style {
Some(vml_rich_text_font(bold, italic, underline, face, size, color))
} else {
None
}
runs.push({ text, font })
i = content_start + close_rel + "".length()
}
if runs.length() > 0 {
match runs[0].font {
None => {
let base_text = runs[0].text
let paragraph : Array[RichTextRun] = []
for run in runs[1:] {
paragraph.push(run)
}
return (base_text, paragraph)
}
Some(_) => return ("", runs)
}
}
("", empty)
}
///|
fn parse_form_controls_vml(
xml : StringView,
) -> Array[FormControl] raise XlsxError {
let out : Array[FormControl] = []
for chunk in xml.split(" v
None => continue
}
let cd = chunk[cd_start + "") {
Some(v) => v
None => continue
}
let tag = cd[:end_tag]
let object_type = match attr_value(tag, "ObjectType") {
Some(v) => v
None => continue
}
if object_type == "Note" {
continue
}
let body = cd[end_tag + 1:]
let mut row0_opt : Int? = None
let mut col0_opt : Int? = None
match extract_xml_text_tag(body, "x:Row") {
Some(v) =>
try @string.parse_int(v, base=10) catch {
_ => ()
} noraise {
value => row0_opt = Some(value)
}
None => ()
}
match extract_xml_text_tag(body, "x:Column") {
Some(v) =>
try @string.parse_int(v, base=10) catch {
_ => ()
} noraise {
value => col0_opt = Some(value)
}
None => ()
}
let (row0, col0) = match (row0_opt, col0_opt) {
(Some(r), Some(c)) => (r, c)
_ =>
match extract_xml_text_tag(body, "x:Anchor") {
Some(anchor) => {
let parts : Array[StringView] = []
for part in anchor.split(",") {
parts.push(part)
}
if parts.length() < 4 {
continue
}
let col0 = try @string.parse_int(parts[0].trim(), base=10) catch {
_ => continue
} noraise {
value => value
}
let row0 = try @string.parse_int(parts[2].trim(), base=10) catch {
_ => continue
} noraise {
value => value
}
(row0, col0)
}
None => continue
}
}
let cell = cell_ref_from(row0 + 1, col0 + 1)
let (textbox_text, paragraph) = parse_vml_textbox(chunk)
let text = if textbox_text != "" {
textbox_text
} else {
match extract_xml_text_tag(body, "x:Text") {
Some(v) => v
None => ""
}
}
let control = FormControl::new(cell, object_type, text~) catch {
_ => continue
}
control.paragraph = paragraph
match extract_xml_text_tag(body, "x:FmlaMacro") {
Some(v) => control.macro_name = if v == "" { None } else { Some(v) }
None => ()
}
match extract_xml_text_tag(body, "x:FmlaLink") {
Some(v) => control.cell_link = if v == "" { None } else { Some(v) }
None => ()
}
match extract_xml_text_tag(body, "x:Val") {
Some(v) => {
let n = @string.parse_int(v.trim(), base=10) catch { _ => 0 }
if n >= 0 && n <= max_form_control_value {
control.current_val = Some(n)
}
}
None => ()
}
match extract_xml_text_tag(body, "x:Min") {
Some(v) => {
let n = @string.parse_int(v.trim(), base=10) catch { _ => 0 }
if n >= 0 && n <= max_form_control_value {
control.min_val = Some(n)
}
}
None => ()
}
match extract_xml_text_tag(body, "x:Max") {
Some(v) => {
let n = @string.parse_int(v.trim(), base=10) catch { _ => 0 }
if n >= 0 && n <= max_form_control_value {
control.max_val = Some(n)
}
}
None => ()
}
match extract_xml_text_tag(body, "x:Inc") {
Some(v) => {
let n = @string.parse_int(v.trim(), base=10) catch { _ => 0 }
if n >= 0 && n <= max_form_control_value {
control.inc_change = Some(n)
}
}
None => ()
}
match extract_xml_text_tag(body, "x:Page") {
Some(v) => {
let n = @string.parse_int(v.trim(), base=10) catch { _ => 0 }
if n >= 0 && n <= max_form_control_value {
control.page_change = Some(n)
}
}
None => ()
}
if xml_has_tag(body, "x:Horiz") {
control.horizontally = Some(true)
}
match extract_xml_text_tag(body, "x:PrintObject") {
Some(v) => {
let s = v.trim().to_lower()
if s == "false" || s == "0" {
control.format.print_object = Some(false)
} else if s == "true" || s == "1" {
control.format.print_object = Some(true)
}
}
None => ()
}
if xml_has_tag(body, "x:MoveWithCells") &&
xml_has_tag(body, "x:SizeWithCells") {
control.format.positioning = Some(Absolute)
} else if xml_has_tag(body, "x:SizeWithCells") {
control.format.positioning = Some(OneCell)
}
match extract_xml_text_tag(body, "x:Checked") {
Some(v) => {
let checked_text = v.trim().to_lower()
control.checked = Some(
checked_text != "" && checked_text != "0" && checked_text != "false",
)
}
None => ()
}
out.push(control)
}
out
}
///|
test "form control vml parse checked false values" {
let xml =
#|
#|
#|
#| 0,0,0,0,1,0,1,0
#| False
#|
#|
#|
#|
#| 0,0,1,0,1,0,2,0
#| false
#|
#|
#|
#|
#| 0,0,2,0,1,0,3,0
#| 0
#|
#|
#|
let controls = parse_form_controls_vml(xml)
inspect(controls.length(), content="3")
inspect(controls[0].cell, content="A1")
debug_inspect(controls[0].checked, content="Some(false)")
inspect(controls[1].cell, content="A2")
debug_inspect(controls[1].checked, content="Some(false)")
inspect(controls[2].cell, content="A3")
debug_inspect(controls[2].checked, content="Some(false)")
}
///|
test "form control vml parse checked true values" {
let xml =
#|
#|
#|
#| 0,0,0,0,1,0,1,0
#| 1
#|
#|
#|
#|
#| 0,0,1,0,1,0,2,0
#| true
#|
#|
#|
let controls = parse_form_controls_vml(xml)
inspect(controls.length(), content="2")
inspect(controls[0].cell, content="A1")
debug_inspect(controls[0].checked, content="Some(true)")
inspect(controls[1].cell, content="A2")
debug_inspect(controls[1].checked, content="Some(true)")
}