///|
pub(all) struct TextOptions {
include_hidden : Bool
include_headers : Bool
include_footers : Bool
include_notes : Bool
paragraph_separator : String
cell_separator : String
trim_trailing_space : Bool
} derive(Eq, Debug)
///|
pub fn TextOptions::default() -> TextOptions {
{
include_hidden: false,
include_headers: false,
include_footers: false,
include_notes: false,
paragraph_separator: "\n",
cell_separator: "\t",
trim_trailing_space: false,
}
}
///|
fn TextOptions::accepts(self : TextOptions, region : Region) -> Bool {
match region {
Body => true
Header => self.include_headers
Footer => self.include_footers
Footnote | Annotation => self.include_notes
}
}
///|
fn trim_line_ends(text : String) -> String {
let out = StringBuilder()
let pending = StringBuilder()
for c in text.iter() {
match c {
' ' | '\t' => pending.write_char(c)
'\n' => {
pending.reset()
out.write_char(c)
}
_ => {
out.write_string(pending.to_string())
pending.reset()
out.write_char(c)
}
}
}
out.to_string()
}
///|
pub fn Document::to_text(
self : Document,
options? : TextOptions = TextOptions::default(),
) -> String {
let out = StringBuilder()
let mut previous : Paragraph? = None
for paragraph in self.paragraphs {
if !options.accepts(paragraph.region) {
continue
}
if previous is Some(before) {
if paragraph.table_row is Some(row) &&
before.table_row == Some(row) &&
paragraph.region == before.region &&
paragraph.table_cell != before.table_cell {
out.write_string(options.cell_separator)
} else {
out.write_string(options.paragraph_separator)
}
}
let text = paragraph.text(include_hidden=options.include_hidden)
if options.trim_trailing_space {
out.write_string(trim_line_ends(text))
} else {
out.write_string(text)
}
previous = Some(paragraph)
}
out.to_string()
}
///|
pub fn region_name(region : Region) -> String {
match region {
Body => "body"
Header => "header"
Footer => "footer"
Footnote => "footnote"
Annotation => "annotation"
}
}
///|
pub fn alignment_name(alignment : Alignment) -> String {
match alignment {
Left => "left"
Right => "right"
Center => "center"
Justify => "justify"
}
}
///|
fn safe_link(target : String) -> Bool {
let lower = target.to_lower()
if !(lower.has_prefix("https://") ||
lower.has_prefix("http://") ||
lower.has_prefix("mailto:") ||
lower.has_prefix("#")) {
return false
}
for c in target.iter() {
if c.to_int() < 32 || c == '<' || c == '>' || c == '"' {
return false
}
}
true
}
///|
fn markdown_escape(text : String, table : Bool) -> String {
let out = StringBuilder()
for c in text.iter() {
match c {
'\\'
| '`'
| '*'
| '_'
| '{'
| '}'
| '['
| ']'
| '('
| ')'
| '#'
| '+'
| '-'
| '.'
| '!'
| '|'
| '~' => {
out.write_char('\\')
out.write_char(c)
}
'<' => out.write_string("<")
'>' => out.write_string(">")
'&' => out.write_string("&")
'\n' => out.write_string(if table { "
" } else { " \n" })
_ => out.write_char(c)
}
}
out.to_string()
}
///|
fn markdown_link(target : String) -> String {
let out = StringBuilder()
for c in target.iter() {
match c {
' ' => out.write_string("%20")
'(' => out.write_string("%28")
')' => out.write_string("%29")
'\\' => out.write_string("%5C")
_ => out.write_char(c)
}
}
out.to_string()
}
///|
fn markdown_run(run : Run, table : Bool) -> String {
let escaped = markdown_escape(run.text, table)
if escaped.is_empty() {
return ""
}
let out = StringBuilder()
// Leave boundary whitespace outside emphasis delimiters.
let chars = escaped.to_array()
let mut start = 0
let mut end = chars.length()
while start < end &&
(chars[start] == ' ' || chars[start] == '\t' || chars[start] == '\n') {
start += 1
}
while end > start &&
(
chars[end - 1] == ' ' ||
chars[end - 1] == '\t' ||
chars[end - 1] == '\n'
) {
end -= 1
}
out.write_string(String::from_array(chars[:start]))
if start < end {
let linked = match run.link {
Some(link) => safe_link(link)
None => false
}
if linked {
out.write_char('[')
}
if run.style.bold {
out.write_string("**")
}
if run.style.italic {
out.write_char('*')
}
if run.style.strike {
out.write_string("~~")
}
out.write_string(String::from_array(chars[start:end]))
if run.style.strike {
out.write_string("~~")
}
if run.style.italic {
out.write_char('*')
}
if run.style.bold {
out.write_string("**")
}
if linked {
out.write_string("](\{markdown_link(run.link.unwrap())})")
}
}
out.write_string(String::from_array(chars[end:]))
out.to_string()
}
///|
fn markdown_paragraph(
paragraph : Paragraph,
include_hidden : Bool,
table : Bool,
) -> String {
let out = StringBuilder()
for run in paragraph.runs {
if include_hidden || !run.style.hidden {
out.write_string(markdown_run(run, table))
}
}
out.to_string()
}
///|
pub fn Document::to_markdown(
self : Document,
options? : TextOptions = TextOptions::default(),
) -> String {
let out = StringBuilder()
let mut in_table = false
let mut row : Int? = None
let mut cell : Int? = None
let mut columns = 0
let mut first_row = true
let mut first_block = true
for paragraph in self.paragraphs {
if !options.accepts(paragraph.region) {
continue
}
if paragraph.table_row is Some(current_row) {
if !in_table {
if !first_block {
out.write_string("\n\n")
}
first_block = false
in_table = true
first_row = true
columns = 0
row = Some(current_row)
cell = None
out.write_char('|')
} else if row != Some(current_row) {
out.write_string(" |\n")
if first_row {
out.write_char('|')
for _ in 0..")
}
cell = paragraph.table_cell
out.write_string(
markdown_paragraph(paragraph, options.include_hidden, true),
)
} else {
if in_table {
out.write_string(" |")
if first_row {
out.write_string("\n|")
for _ in 0..