///|
fn apply_affix(
affix : Affix,
text : String,
format~ : String = "text",
) -> String {
if text.is_empty() {
return ""
}
let mut value = text
value = apply_output_case(value, affix.text_case, format)
if format == "html" && affix.font_style == "italic" {
value = "" + value + ""
}
if format == "html" && affix.font_weight == "bold" {
value = "" + value + ""
}
if format == "html" &&
(affix.vertical_align == "sup" || affix.vertical_align == "superscript") {
value = "" + value + ""
}
output_escape(affix.prefix, format) +
value +
output_escape(affix.suffix, format)
}
///|
fn format_number(value : String, form : String) -> String {
match parse_int(value) {
Some(n) =>
if form == "ordinal" {
ordinal_en(n)
} else if form == "long-ordinal" {
let words = [
"first", "second", "third", "fourth", "fifth", "sixth", "seventh", "eighth",
"ninth", "tenth",
]
if n >= 1 && n <= 10 {
words[n - 1]
} else {
ordinal_en(n)
}
} else if form == "roman" {
roman_upper(n).to_lower()
} else {
n.to_string()
}
None => value
}
}
///|
fn format_date(
date : CslDate,
parts : Array[DatePart],
delimiter : String,
year_suffix~ : String = "",
locale~ : Locale = Locale::english(),
) -> String {
let values = []
for part in parts {
let mut value = ""
if part.name == "year" && date.year != 0 {
value = date.year.to_string() + year_suffix
}
if part.name == "month" && date.month != 0 {
value = if part.form == "numeric-leading-zeros" {
pad2(date.month)
} else if part.form == "numeric" {
date.month.to_string()
} else {
locale.month(
date.month,
form=if part.form.is_empty() { "long" } else { part.form },
)
}
}
if part.name == "day" && date.day != 0 {
value = if part.form == "ordinal" {
ordinal_en(date.day)
} else if part.form == "numeric-leading-zeros" {
pad2(date.day)
} else {
date.day.to_string()
}
}
if !value.is_empty() {
values.push(apply_affix(part_to_affix(part), value))
}
}
join_nonempty(values, delimiter)
}
///|
fn part_to_affix(part : DatePart) -> Affix {
{
prefix: part.prefix,
suffix: part.suffix,
delimiter: "",
text_case: part.text_case,
font_style: "",
font_weight: "",
vertical_align: "",
}
}
///|
fn output_escape(value : String, format : String) -> String {
if format == "html" {
html_escape(value)
} else {
value
}
}
///|
fn term_for(variable : String, form : String) -> String {
if form == "short" {
match variable {
"page" => "p."
"chapter" => "chap."
_ => variable
}
} else {
match variable {
"page" => "page"
"chapter" => "chapter"
"section" => "section"
_ => variable
}
}
}
///|
fn item_value(item : Item, variable : String) -> String {
match variable {
"citation-number" =>
item.strings
.get("citation-number")
.unwrap_or_else(fn() {
item.numbers
.get("citation-number")
.map(fn(n) { n.to_string() })
.unwrap_or("")
})
"year-suffix" => item.strings.get("year-suffix").unwrap_or("")
"author" | "editor" | "translator" => first_family(item, variable)
_ =>
match item.numbers.get(variable) {
Some(n) => n.to_string()
None => item.string(variable)
}
}
}
///|
fn node_value(
node : Node,
item : Item,
style : Style,
format : String,
) -> String {
match node {
Label(affix, spec) => {
let value = item_value(item, spec.variable)
if value.is_empty() {
return ""
}
let variable = if spec.variable == "locator" {
item.strings.get("locator-label").unwrap_or("page")
} else {
spec.variable
}
let plural = spec.plural == "always" ||
(
spec.plural != "never" &&
(value.contains("-") || value.contains("–") || value.contains(","))
)
let term = term_for(variable, spec.form)
let term = if plural && (spec.form.is_empty() || spec.form == "long") {
term + "s"
} else if plural && variable == "page" && spec.form == "short" {
"pp."
} else {
term
}
apply_affix(affix, output_escape(term, format), format~)
}
Text(affix, spec) => {
let value = if !spec.macro_name.is_empty() {
match style.macros.get(spec.macro_name) {
Some(xs) => render_nodes(xs, item, style, format)
None => ""
}
} else {
output_escape(
if !spec.value.is_empty() {
spec.value
} else if !spec.term.is_empty() {
if spec.form.is_empty() || spec.form == "long" {
style.locale.terms
.get(spec.term)
.unwrap_or(term_for(spec.term, spec.form))
} else {
term_for(spec.term, spec.form)
}
} else {
item_value(item, spec.variable)
},
format,
)
}
apply_affix(affix, value, format~)
}
Names(affix, spec) => {
let spec = resolve_name_node(spec, style)
let lists = []
for variable in spec.variable.split(" ") {
let variable = variable.to_owned()
let names = item.name(variable)
if !names.is_empty() {
lists.push(
format_names(
names,
{ ..spec, label_variable: variable, },
style,
format,
),
)
}
}
let mut value = join_nonempty(
lists,
output_escape(spec.options.get("names-delimiter").unwrap_or(""), format),
)
if value.is_empty() {
for substitute in spec.substitute {
value = node_value(substitute, item, style, format)
if !value.is_empty() {
break
}
}
}
apply_affix(affix, value, format~)
}
Number(affix, spec) =>
apply_affix(
affix,
output_escape(
format_number(item_value(item, spec.variable), spec.form),
format,
),
format~,
)
Date(affix, spec) =>
match item.date(spec.variable) {
Some(d) =>
apply_affix(
affix,
output_escape(
format_date(
d,
spec.parts,
spec.delimiter,
locale=style.locale,
year_suffix=if style.citation_disambiguate &&
!explicit_year_suffix(style) {
item.string("year-suffix")
} else {
""
},
),
format,
),
format~,
)
None => ""
}
Group(affix, children) =>
apply_affix(
affix,
join_rendered(
children,
item,
style,
format,
output_escape(affix.delimiter, format),
),
format~,
)
Choose(affix, branches) =>
apply_affix(affix, render_choose(branches, item, style, format), format~)
}
}
///|
fn condition_ok(condition : Condition, item : Item) -> Bool {
match condition {
Variable(v) => item.has(v)
Type(v) => item.item_type == v
Numeric(v) => {
let value = item_value(item, v)
is_csl_numeric(value)
}
IsUncertain(v) =>
match item.date(v) {
Some(d) => d.circa
None => false
}
IsPlural(v) => item.name(v).length() > 1
Position(_) => false
}
}
///|
fn branch_ok(branch : ChooseBranch, item : Item) -> Bool {
if branch.conditions.length() == 0 {
return true
}
let mut hits = 0
for c in branch.conditions {
if condition_ok(c, item) {
hits = hits + 1
}
}
if branch.match_mode == "any" {
hits > 0
} else if branch.match_mode == "none" {
hits == 0
} else {
hits == branch.conditions.length()
}
}
///|
fn render_choose(
branches : Array[ChooseBranch],
item : Item,
style : Style,
format : String,
) -> String {
for branch in branches {
if branch_ok(branch, item) {
return render_nodes(branch.children, item, style, format)
}
}
""
}
///|
fn render_nodes(
nodes : Array[Node],
item : Item,
style : Style,
format : String,
) -> String {
let out = []
let delimiter = ""
for node in nodes {
let value = node_value(node, item, style, format)
if !value.is_empty() {
out.push(value)
}
}
join_nonempty(out, delimiter)
}
///|
fn join_rendered(
children : Array[Node],
item : Item,
style : Style,
format : String,
delimiter : String,
) -> String {
let out = []
for child in children {
let value = node_value(child, item, style, format)
if !value.is_empty() {
out.push(value)
}
}
join_nonempty(out, delimiter)
}
///|
fn render_layout(
layout : Layout,
item : Item,
style : Style,
format : String,
) -> String {
apply_affix(
layout.affix,
render_nodes(layout.children, item, style, format),
format~,
)
}
///|
pub(all) struct Processor {
style : Style
}
///|
pub fn Processor::from_style(style_xml : String) -> Result[Processor, CslError] {
match parse_style(style_xml) {
Ok(style) => Ok({ style, })
Err(e) => Err(e)
}
}
///|
pub fn Processor::render_bibliography(
self : Processor,
items : Array[Item],
format~ : String = "text",
) -> Result[String, CslError] {
if format != "text" && format != "html" {
return Err(BadItem("unsupported output format: " + format))
}
let style = context_style(self.style, false)
let ordered = match prepare_items(items, style) {
Ok(xs) => xs
Err(e) => return Err(e)
}
match style.bibliography {
None => Err(MissingStyle)
Some(layout) => {
let out = []
for item in ordered {
out.push(render_layout(layout, item, style, format))
}
Ok(join_nonempty(out, "\n"))
}
}
}
///|
fn apply_output_case(text : String, mode : String, format : String) -> String {
if format != "html" || mode.is_empty() {
return apply_text_case(text, mode)
}
let chars = text.to_array()
let out = StringBuilder()
let mut plain = StringBuilder()
let mut i = 0
let mut first = true
while i < chars.length() {
if chars[i] == '<' || chars[i] == '&' {
let segment = plain.to_string()
if !segment.is_empty() {
out.write_string(
if mode == "capitalize-first" && !first {
segment
} else {
apply_text_case(segment, mode)
},
)
first = false
plain = StringBuilder()
}
let closing = if chars[i] == '<' { '>' } else { ';' }
while i < chars.length() {
let c = chars[i]
out.write_char(c)
i = i + 1
if c == closing {
break
}
}
} else {
plain.write_char(chars[i])
i = i + 1
}
}
let segment = plain.to_string()
out.write_string(
if mode == "capitalize-first" && !first {
segment
} else {
apply_text_case(segment, mode)
},
)
out.to_string()
}
///|
pub fn Processor::render_bibliography_html(
self : Processor,
items : Array[Item],
) -> Result[String, CslError] {
self.render_bibliography(items, format="html")
}
///|
pub fn render_one(style_xml : String, item : Item) -> Result[String, CslError] {
match Processor::from_style(style_xml) {
Ok(p) => p.render_bibliography([item])
Err(e) => Err(e)
}
}