///|
/// `{% trans %}`: ask the seam for the message, fall back to the singular or
/// plural text, fill `%(name)s` from the bound values, and write it as
/// trusted text when escaping is on, escaping the values instead.
fn Run::trans(self : Run, t : @syntax.Trans) -> Unit raise @spec.Refused {
let values : Map[String, @spec.Value] = Map([])
for pair in t.vars {
values[pair.0] = self.eval(pair.1)
}
let count = match t.count {
Some(e) => Some(self.eval(e))
None => None
}
let one = match count {
Some(c) => c == Int(1)
None => true
}
let fallback = match t.plural {
Some(p) if !one => p
_ => t.singular
}
let n = match count {
Some(c) => c.index()
None => None
}
let message = self.call
.translate(t.singular, context?=t.context, count?=n)
.unwrap_or(fallback)
let text = if values.is_empty() {
message
} else {
@defaults.percent(
self.call,
message,
Dict(values),
escape=self.autoescape(),
)
}
if !self.extended {
self.out.write_string(text)
}
}
///|
/// The elements HTML leaves without a closing tag.
let void_elements : Array[String] = [
"area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param",
"source", "track", "wbr",
]
///|
/// A value as JavaScript's `JSON.stringify` writes it, the text Pug gives an
/// attribute holding a number, a list or a mapping.
fn js_json(value : @spec.Value) -> String {
match value {
Null | Undefined(_) => "null"
Bool(b) => if b { "true" } else { "false" }
Int(n) => n.to_string()
Double(d) =>
if d.is_nan() || d.is_inf() {
"null"
} else if d == d.floor() && d.abs() < 1.0e21 {
d.to_int64().to_string()
} else {
d.to_string()
}
Text(s) | Markup(s) => Json::string(s).stringify()
Seq(items) | Tuple(items) => "[" + items.map(js_json).join(",") + "]"
Dict(fields) => {
let parts = []
for k, v in fields {
parts.push(Json::string(k).stringify() + ":" + js_json(v))
}
"{" + parts.join(",") + "}"
}
Object(_) => Json::string(value.to_string()).stringify()
}
}
///|
/// Pug's class value: a list's members joined, a mapping's keys whose
/// values are true, text as it stands.
fn class_text(value : @spec.Value) -> String {
match value {
Seq(items) | Tuple(items) =>
items.map(class_text).filter(c => !c.is_empty()).join(" ")
Dict(fields) =>
fields
.iter()
.filter(p => p.0 != "" && p.1.truthy())
.map(p => p.0)
.collect()
.join(" ")
Null | Undefined(_) | Bool(false) => ""
other => other.to_string()
}
}
///|
/// One attribute as Pug writes it: absent when false or missing, bare (or
/// repeated, outside the HTML doctype) when true, else quoted.
fn Run::write_attr(
self : Run,
name : String,
value : @spec.Value,
escape : Bool,
) -> Unit {
match value {
Bool(false) | Null | Undefined(_) => return
Text("") | Markup("") if name == "class" || name == "style" => return
Bool(true) =>
self.out.write_string(
if self.terse {
" " + name
} else {
" " + name + "=\"" + name + "\""
},
)
_ => {
let raw = match value {
Text(s) | Markup(s) => s
other => js_json(other)
}
let string_like = value is (Text(_) | Markup(_))
if !escape && !string_like && raw.contains("\"") {
self.out.write_string(
" " + name + "='" + raw.replace_all(old="'", new="'") + "'",
)
return
}
let text = if escape && !(value is Markup(_)) {
@spec.escape(raw)
} else {
raw
}
self.out.write_string(" " + name + "=\"" + text + "\"")
}
}
}
///|
fn Run::element(self : Run, el : @syntax.Element) -> Unit raise @spec.Refused {
if self.extended {
return
}
let tag = self.call.text(self.eval(el.tag))
self.out.write_char('<')
self.out.write_string(tag)
if el.splats.is_empty() {
let classes = []
let others = []
for attr in el.attrs {
let value = match attr.value {
Some(e) => self.eval(e)
None => Bool(true)
}
if attr.name == "class" {
classes.push((value, attr.escape))
} else {
others.push((attr.name, value, attr.escape))
}
}
if !classes.is_empty() {
let joined = classes
.map(pair => {
let c = class_text(pair.0)
if pair.1 {
@spec.escape(c)
} else {
c
}
})
.filter(c => !c.is_empty())
.join(" ")
self.write_attr("class", Text(joined), false)
}
for other in others {
let value = if other.0 == "style" {
@spec.Value::Text(style_text(other.1))
} else {
other.1
}
self.write_attr(other.0, value, other.2)
}
} else {
let merged : Map[String, @spec.Value] = Map([])
for attr in el.attrs {
let value = match attr.value {
Some(e) => {
let v = self.eval(e)
if attr.escape && attr.name != "class" && v is Text(_) {
@spec.Value::Text(@spec.escape(v.to_string()))
} else {
v
}
}
None => Bool(true)
}
merge_attr(merged, attr.name, value)
}
for splat in el.splats {
match self.eval(splat) {
Dict(fields) =>
for k, v in fields {
merge_attr(merged, k, v)
}
Undefined(name) => raise undefined_use(name)
v => raise @spec.invalid("&attributes takes a mapping, not \{v.kind()}")
}
}
match merged.get("class") {
Some(c) => self.write_attr("class", Text(class_text(c)), false)
None => ()
}
for k, v in merged {
if k == "class" {
continue
}
let value = if k == "style" {
@spec.Value::Text(style_text(v))
} else {
v
}
self.write_attr(k, value, false)
}
}
let empty = !self.xml && void_elements.contains(tag)
if el.self_close || empty {
self.out.write_string(if self.terse && !el.self_close { ">" } else { "/>" })
if el.body.iter().any(n => !(n is (Line(_) | Text(_)))) {
raise @spec.invalid(
"\{tag} is a self closing element: <\{tag}/> but contains nested content",
)
}
return
}
self.out.write_char('>')
self.run_nodes(el.body)
self.out.write_string("" + tag + ">")
}