///|
/// Compatibility diagnostics for the supported MoonCite profile. This is not
/// validation against the full CSL schema; metadata below info is not rendered.
/// Empty means accepted by this profile, not full CSL 1.0.2 conformance.
pub fn style_compatibility_issues(style_xml : String) -> Array[String] {
match parse_xml(style_xml) {
Err(e) => [e.message()]
Ok(root) => {
let issues = profile_issues(root)
if issues.is_empty() {
match parse_style(style_xml) {
Err(e) => issues.push(e.message())
Ok(_) => ()
}
}
issues
}
}
}
///|
fn profile_issues(root : XmlElem) -> Array[String] {
let issues = []
if root.name != "style" {
issues.push("root element must be style")
return issues
}
let version = root.attr("version")
if version != "1.0" && version != "1.0.1" && version != "1.0.2" {
issues.push(
"/style: supported CSL version declarations are 1.0, 1.0.1, 1.0.2 (subset only)",
)
}
profile_walk(root, "/style", issues)
issues
}
///|
fn profile_walk(el : XmlElem, path : String, issues : Array[String]) -> Unit {
if el.name == "info" {
return
}
let common = " prefix suffix delimiter text-case font-style font-weight vertical-align "
let name_options = " form and delimiter initialize-with initialize name-as-sort-order sort-separator delimiter-precedes-last delimiter-precedes-et-al et-al-min et-al-use-first et-al-use-last names-delimiter "
let allowed = match el.name {
"style" =>
" version xmlns class default-locale title name-delimiter name-form " +
name_options
"macro" => " name "
"citation" =>
" collapse after-collapse-delimiter disambiguate-add-year-suffix name-delimiter name-form " +
name_options
"bibliography" => " name-delimiter name-form " + name_options
"layout" | "group" | "choose" => common
"text" => common + " variable value macro term form "
"names" => common + name_options + " variable "
"name" => common + name_options
"label" => common + " variable form plural "
"number" => common + " variable form "
"date" => common + " variable "
"date-part" => " name form prefix suffix text-case "
"if" | "else-if" =>
" variable type is-numeric is-uncertain-date is-plural match "
"else" | "substitute" | "sort" | "terms" => " "
"key" => " variable macro sort "
"locale" => " lang xml:lang "
"term" => " name "
_ => {
issues.push(path + ": unsupported element " + el.name)
return
}
}
for key, value in el.attrs {
if !allowed.contains(" " + key + " ") {
issues.push(
path + "/@" + key + ": unsupported option (not silently ignored)",
)
continue
}
let choices = match key {
"collapse" => " citation-number "
"disambiguate-add-year-suffix" | "initialize" | "et-al-use-last" =>
" true false "
"font-style" => " normal italic "
"font-weight" => " normal bold "
"vertical-align" => " baseline sup superscript "
"text-case" =>
" lowercase uppercase capitalize-first capitalize-all title sentence "
"and" => " text symbol "
"name-as-sort-order" => " first all "
"delimiter-precedes-last" | "delimiter-precedes-et-al" =>
" contextual always never after-inverted-name "
"match" => " any all none "
"sort" => " ascending descending "
"class" => " in-text note "
"plural" => " contextual always never "
"form" =>
match el.name {
"name" | "names" => " long short count "
"number" => " numeric ordinal long-ordinal roman "
"date-part" =>
if el.attr("name") == "year" {
" long "
} else if el.attr("name") == "month" {
" long short numeric numeric-leading-zeros "
} else {
" numeric numeric-leading-zeros ordinal "
}
"label" => " long short "
"text" => " long "
_ => " long short count "
}
"name" => if el.name == "date-part" { " year month day " } else { "" }
_ => ""
}
if !choices.is_empty() &&
!value.is_empty() &&
!choices.contains(" " + value + " ") {
issues.push(path + "/@" + key + ": unsupported value " + value)
}
if key == "et-al-min" || key == "et-al-use-first" {
match parse_int(value) {
Some(n) if n > 0 => ()
_ => issues.push(path + "/@" + key + ": expected positive integer")
}
}
}
if el.name == "style" &&
!el.attr("default-locale").is_empty() &&
el.attr("default-locale") != "en-US" &&
el.attr("default-locale") != "zh-CN" {
issues.push(path + "/@default-locale: only en-US and zh-CN are supported")
}
if el.name == "date" && el.kids_named("date-part").is_empty() {
issues.push(
path + ": locale-dependent date forms are unsupported; supply date-part",
)
}
if el.name == "text" || el.name == "number" || el.name == "key" {
for variable in el.attr("variable").split(" ") {
if variable == "first-reference-note-number" ||
variable == "citation-label" {
issues.push(
path +
"/@variable: unsupported generated context variable " +
variable.to_owned(),
)
}
}
}
if el.name == "text" {
let mut selectors = 0
for key in ["variable", "value", "macro", "term"] {
if el.attrs.contains(key) {
selectors = selectors + 1
}
}
if selectors != 1 {
issues.push(
path + ": text requires exactly one variable/value/macro/term selector",
)
}
}
let unique_children = match el.name {
"style" => ["info", "citation", "bibliography"]
"citation" | "bibliography" => ["sort", "layout"]
"names" => ["name", "label", "substitute"]
_ => []
}
for name in unique_children {
if el.kids_named(name).length() > 1 {
issues.push(path + "/" + name + ": duplicate singleton element")
}
}
if (el.name == "citation" || el.name == "bibliography") &&
el.kids_named("layout").length() != 1 {
issues.push(path + ": exactly one layout is required")
}
if el.name == "style" {
let macro_names : Map[String, Bool] = Map([])
for macro_el in el.kids_named("macro") {
let name = macro_el.attr("name")
if name.is_empty() || macro_names.contains(name) {
issues.push(path + "/macro: empty or duplicate macro name " + name)
}
macro_names[name] = true
}
}
for child in el.kids() {
let children = match el.name {
"style" => " info macro citation bibliography locale "
"citation" | "bibliography" => " sort layout "
"sort" => " key "
"names" => " name label substitute "
"date" => " date-part "
"choose" => " if else-if else "
"locale" => " terms "
"terms" => " term "
"macro" | "layout" | "group" | "if" | "else-if" | "else" | "substitute" =>
" text names label number date group choose "
_ => " "
}
let next = path + "/" + child.name
if !children.contains(" " + child.name + " ") {
issues.push(next + ": element not supported in this parent")
}
profile_walk(child, next, issues)
}
}