///|
/// A token stream and the way its syntax reads a statement's body. The
/// brace syntax reads on through the same stream; the indentation syntax
/// reads the indented lines and swaps in the tokens of the closing line.
pub(all) struct Parser {
template : String
mut tokens : Array[Token]
mut at : Int32
/// Read nodes until one of the end tags; on return the current token is
/// the end tag's name.
body : (Parser, Array[String]) -> Array[Node] raise @spec.Refused
}
///|
pub fn Parser::current(self : Parser) -> Token {
self.tokens[self.at]
}
///|
fn Parser::look(self : Parser) -> Token {
if self.at + 1 < self.tokens.length() {
self.tokens[self.at + 1]
} else {
self.tokens[self.tokens.length() - 1]
}
}
///|
pub fn Parser::next(self : Parser) -> Token {
let token = self.tokens[self.at]
if self.at + 1 < self.tokens.length() {
self.at += 1
}
token
}
///|
pub fn Parser::fail(self : Parser, reason : String) -> @spec.Refused {
let token = self.current()
Syntax(template=self.template, line=token.line, column=token.column, reason~)
}
///|
fn Parser::is_op(self : Parser, op : String) -> Bool {
self.current().tok is Op(o) && o == op
}
///|
fn Parser::is_name(self : Parser, name : String) -> Bool {
self.current().tok is Name(n) && n == name
}
///|
fn Parser::skip_op(self : Parser, op : String) -> Bool {
if self.is_op(op) {
ignore(self.next())
true
} else {
false
}
}
///|
fn Parser::skip_name(self : Parser, name : String) -> Bool {
if self.is_name(name) {
ignore(self.next())
true
} else {
false
}
}
///|
fn Parser::expect_op(self : Parser, op : String) -> Unit raise @spec.Refused {
if !self.skip_op(op) {
raise self.fail("expected \{op}, found \{describe(self.current().tok)}")
}
}
///|
fn Parser::expect_name(
self : Parser,
name? : String,
) -> String raise @spec.Refused {
match (self.current().tok, name) {
(Name(n), None) => {
ignore(self.next())
n
}
(Name(n), Some(want)) if n == want => {
ignore(self.next())
n
}
(tok, Some(want)) =>
raise self.fail("expected \{want}, found \{describe(tok)}")
(tok, None) => raise self.fail("expected a name, found \{describe(tok)}")
}
}
///|
pub fn Parser::expect(self : Parser, tok : Tok) -> Unit raise @spec.Refused {
if self.current().tok != tok {
raise self.fail(
"expected \{describe(tok)}, found \{describe(self.current().tok)}",
)
}
ignore(self.next())
}
///|
fn describe(tok : Tok) -> String {
match tok {
Data(_) => "template text"
VarBegin => "{{"
VarEnd => "}}"
BlockBegin => "{%"
BlockEnd => "the end of the tag"
Name(n) => n
Str(s) => @spec.repr_text(s)
Int(n) => n.to_string()
Float(d) => @spec.python_float(d)
Op(o) => o
Lang(name, _, _) => "a \{name} block"
Eof => "the end of the template"
}
}
///|
/// The end of a tuple: the end of a tag, a closing parenthesis, or one of
/// the words a statement reserves after it (`in` after a loop's names).
fn Parser::tuple_end(self : Parser, extra : Array[String]) -> Bool {
match self.current().tok {
VarEnd | BlockEnd | Eof | Op(")") => true
Name(n) => extra.contains(n)
_ => false
}
}
///|
/// An expression, or several separated by commas as a tuple.
pub fn Parser::parse_tuple(
self : Parser,
simplified~ : Bool = false,
condexpr~ : Bool = true,
extra~ : Array[String] = [],
parenthesised~ : Bool = false,
) -> Expr raise @spec.Refused {
let items = []
let mut tuple = false
for ;; {
if !items.is_empty() {
self.expect_op(",")
}
if self.tuple_end(extra) {
break
}
items.push(
if simplified {
self.parse_primary()
} else if condexpr {
self.parse_expression()
} else {
self.parse_or()
},
)
if self.is_op(",") {
tuple = true
} else {
break
}
}
if !tuple {
if items.length() == 1 {
return items[0]
}
if !parenthesised {
raise self.fail(
"expected an expression, found \{describe(self.current().tok)}",
)
}
}
Tuple(items)
}
///|
pub fn Parser::parse_expression(self : Parser) -> Expr raise @spec.Refused {
let mut expr = self.parse_or()
while self.skip_name("if") {
let cond = self.parse_or()
let otherwise = if self.skip_name("else") {
Some(self.parse_expression())
} else {
None
}
expr = Cond(cond, expr, otherwise)
}
expr
}
///|
fn Parser::parse_or(self : Parser) -> Expr raise @spec.Refused {
let mut left = self.parse_and()
while self.skip_name("or") {
left = Binary(Or, left, self.parse_and())
}
left
}
///|
fn Parser::parse_and(self : Parser) -> Expr raise @spec.Refused {
let mut left = self.parse_not()
while self.skip_name("and") {
left = Binary(And, left, self.parse_not())
}
left
}
///|
fn Parser::parse_not(self : Parser) -> Expr raise @spec.Refused {
if self.skip_name("not") {
return Not(self.parse_not())
}
self.parse_compare()
}
///|
fn Parser::parse_compare(self : Parser) -> Expr raise @spec.Refused {
let first = self.parse_math1()
let ops = []
for ;; {
let op : Op? = match self.current().tok {
Op("==") => Some(Eq)
Op("!=") => Some(Ne)
Op("<") => Some(Lt)
Op(">") => Some(Gt)
Op("<=") => Some(Le)
Op(">=") => Some(Ge)
Name("in") => Some(In)
Name("not") if self.look().tok is Name("in") => {
ignore(self.next())
Some(NotIn)
}
_ => None
}
match op {
Some(op) => {
ignore(self.next())
ops.push((op, self.parse_math1()))
}
None => break
}
}
if ops.is_empty() {
first
} else {
Compare(first, ops)
}
}
///|
fn Parser::parse_math1(self : Parser) -> Expr raise @spec.Refused {
let mut left = self.parse_concat()
for ;; {
let op : Op = match self.current().tok {
Op("+") => Add
Op("-") => Sub
_ => break
}
ignore(self.next())
left = Binary(op, left, self.parse_concat())
}
left
}
///|
fn Parser::parse_concat(self : Parser) -> Expr raise @spec.Refused {
let parts = [self.parse_math2()]
while self.skip_op("~") {
parts.push(self.parse_math2())
}
if parts.length() == 1 {
parts[0]
} else {
Concat(parts)
}
}
///|
fn Parser::parse_math2(self : Parser) -> Expr raise @spec.Refused {
let mut left = self.parse_pow()
for ;; {
let op : Op = match self.current().tok {
Op("*") => Mul
Op("/") => Div
Op("//") => FloorDiv
Op("%") => Mod
_ => break
}
ignore(self.next())
left = Binary(op, left, self.parse_pow())
}
left
}
///|
fn Parser::parse_pow(self : Parser) -> Expr raise @spec.Refused {
let mut left = self.parse_unary()
while self.skip_op("**") {
left = Binary(Pow, left, self.parse_unary())
}
left
}
///|
fn Parser::parse_unary(
self : Parser,
filters~ : Bool = true,
) -> Expr raise @spec.Refused {
let node = if self.skip_op("-") {
Neg(self.parse_unary(filters=false))
} else if self.skip_op("+") {
Pos(self.parse_unary(filters=false))
} else {
self.parse_primary()
}
let node = self.parse_postfix(node)
if filters {
self.parse_filter_expr(node)
} else {
node
}
}
///|
fn Parser::parse_primary(self : Parser) -> Expr raise @spec.Refused {
let token = self.current()
match token.tok {
Name(n) => {
ignore(self.next())
match n {
"true" | "True" => Lit(Bool(true))
"false" | "False" => Lit(Bool(false))
"none" | "None" => Lit(Null)
_ => Name(n)
}
}
Str(s) => {
ignore(self.next())
let buf = StringBuilder()
buf.write_string(s)
while self.current().tok is Str(more) {
buf.write_string(more)
ignore(self.next())
}
Lit(Text(buf.to_string()))
}
Int(n) => {
ignore(self.next())
Lit(Int(n))
}
Float(d) => {
ignore(self.next())
Lit(Double(d))
}
Op("(") => {
ignore(self.next())
let inner = self.parse_tuple(parenthesised=true)
self.expect_op(")")
inner
}
Op("[") => {
ignore(self.next())
let items = []
while !self.is_op("]") {
if !items.is_empty() {
self.expect_op(",")
}
if self.is_op("]") {
break
}
items.push(self.parse_expression())
}
self.expect_op("]")
List(items)
}
Op("{") => {
ignore(self.next())
let pairs = []
while !self.is_op("}") {
if !pairs.is_empty() {
self.expect_op(",")
}
if self.is_op("}") {
break
}
let key = self.parse_expression()
self.expect_op(":")
pairs.push((key, self.parse_expression()))
}
self.expect_op("}")
Dict(pairs)
}
tok => raise self.fail("unexpected \{describe(tok)}")
}
}
///|
fn Parser::parse_postfix(
self : Parser,
node : Expr,
) -> Expr raise @spec.Refused {
let mut node = node
for ;; {
match self.current().tok {
Op("." | "[") => node = self.parse_subscript(node)
Op("(") => node = Call(node, self.parse_call_args())
_ => break
}
}
node
}
///|
fn Parser::parse_filter_expr(
self : Parser,
node : Expr,
) -> Expr raise @spec.Refused {
let mut node = node
for ;; {
match self.current().tok {
Op("|") => node = self.parse_filter(Some(node))
Name("is") => node = self.parse_test(node)
Op("(") => node = Call(node, self.parse_call_args())
_ => break
}
}
node
}
///|
fn Parser::parse_subscript(
self : Parser,
node : Expr,
) -> Expr raise @spec.Refused {
if self.skip_op(".") {
let token = self.next()
return match token.tok {
Name(n) => Attr(node, n)
Int(n) => Item(node, Lit(Int(n)))
_ => raise self.fail("expected a name or a number after the dot")
}
}
self.expect_op("[")
let args = []
while !self.is_op("]") {
if !args.is_empty() {
self.expect_op(",")
}
args.push(self.parse_subscribed())
}
self.expect_op("]")
Item(node, if args.length() == 1 { args[0] } else { Tuple(args) })
}
///|
fn Parser::parse_subscribed(self : Parser) -> Expr raise @spec.Refused {
let start = if self.skip_op(":") {
None
} else {
let node = self.parse_expression()
if !self.skip_op(":") {
return node
}
Some(node)
}
let stop = if self.is_op(":") || self.is_op("]") || self.is_op(",") {
None
} else {
Some(self.parse_expression())
}
let step = if self.skip_op(":") {
if self.is_op("]") || self.is_op(",") {
None
} else {
Some(self.parse_expression())
}
} else {
None
}
Slice(start, stop, step)
}
///|
fn Parser::parse_call_args(self : Parser) -> Array[Arg] raise @spec.Refused {
self.expect_op("(")
let args = []
let mut named = false
let mut spread = false
let mut spread_named = false
while !self.is_op(")") {
if !args.is_empty() {
self.expect_op(",")
if self.is_op(")") {
break
}
}
if self.skip_op("*") {
if spread || spread_named {
raise self.fail("invalid syntax for a call")
}
spread = true
args.push(Spread(self.parse_expression()))
} else if self.skip_op("**") {
if spread_named {
raise self.fail("invalid syntax for a call")
}
spread_named = true
args.push(SpreadNamed(self.parse_expression()))
} else if self.current().tok is Name(key) && self.look().tok is Op("=") {
if spread_named {
raise self.fail("invalid syntax for a call")
}
ignore(self.next())
ignore(self.next())
named = true
args.push(Named(key, self.parse_expression()))
} else {
if spread || spread_named || named {
raise self.fail("a positional argument follows a named one")
}
args.push(Pos(self.parse_expression()))
}
}
self.expect_op(")")
args
}
///|
/// `|name(args)`, chained; `input` is `None` for a filter block's head.
pub fn Parser::parse_filter(
self : Parser,
input : Expr?,
) -> Expr raise @spec.Refused {
let mut node = input
let mut first = input is None
while self.is_op("|") || first {
if !first {
ignore(self.next())
}
first = false
if self.is_op(":") {
raise self.fail("filter arguments take parentheses, as in |truncate(3)")
}
let mut name = self.expect_name()
while self.skip_op(".") {
name = name + "." + self.expect_name()
}
if self.is_op(":") {
raise self.fail("filter arguments take parentheses, as in |\{name}(…)")
}
let args = if self.is_op("(") { self.parse_call_args() } else { [] }
node = Some(Filter(node, name, args))
}
node.unwrap()
}
///|
fn Parser::parse_test(self : Parser, node : Expr) -> Expr raise @spec.Refused {
ignore(self.next())
let negated = self.skip_name("not")
let mut name = self.expect_name()
while self.skip_op(".") {
name = name + "." + self.expect_name()
}
let args = match self.current().tok {
Op("(") => self.parse_call_args()
Name("else" | "or" | "and") => []
Name("is") => raise self.fail("tests cannot be chained with is")
Name(_) | Str(_) | Int(_) | Float(_) | Op("[" | "{") => {
let arg = self.parse_postfix(self.parse_primary())
[Pos(arg)]
}
_ => []
}
let check = Test(node, name, args)
if negated {
Not(check)
} else {
check
}
}
///|
/// Where a statement binds: a name, a tuple of targets, or `ns.attr`.
fn Parser::parse_target(
self : Parser,
extra~ : Array[String] = [],
field~ : Bool = false,
) -> Target raise @spec.Refused {
let items = []
let mut tuple = false
for ;; {
if !items.is_empty() {
self.expect_op(",")
}
if self.tuple_end(extra) {
break
}
items.push(self.parse_target_atom(field~))
if self.is_op(",") {
tuple = true
} else {
break
}
}
if !tuple && items.length() == 1 {
return items[0]
}
if items.is_empty() {
raise self.fail("expected a name to assign to")
}
Tuple(items)
}
///|
fn Parser::parse_target_atom(
self : Parser,
field~ : Bool,
) -> Target raise @spec.Refused {
if self.skip_op("(") {
let inner = self.parse_target(field~)
self.expect_op(")")
return inner
}
let name = self.expect_name()
if name is ("true" | "false" | "none" | "True" | "False" | "None") {
raise self.fail("cannot assign to \{name}")
}
if field && self.skip_op(".") {
return Field(name, self.expect_name())
}
Name(name)
}
///|
/// The statements both syntaxes share. The current token is the name after
/// `{%`; on return it is the end of the tag.
pub fn Parser::parse_statement(
self : Parser,
) -> Array[Node] raise @spec.Refused {
let token = self.current()
let word = match token.tok {
Name(n) => n
_ => raise self.fail("a statement starts with its name")
}
ignore(self.next())
let node : Node = match word {
"for" => self.parse_for()
"if" => self.parse_if()
"set" => self.parse_set()
"block" => self.parse_block()
"extends" => Extends(self.parse_expression())
"include" => {
let target = self.parse_expression()
let missing = if self.is_name("ignore") &&
self.look().tok is Name("missing") {
ignore(self.next())
ignore(self.next())
true
} else {
false
}
Include(
target,
missing~,
context=self.parse_context(true),
raw=false,
inline=false,
block=[],
)
}
"import" => {
let target = self.parse_expression()
ignore(self.expect_name(name="as"))
let name = self.expect_name()
Import(target, name, context=self.parse_context(false))
}
"from" => self.parse_from()
"macro" => {
let name = self.expect_name()
let params = self.parse_signature()
MacroDef({
name,
params,
body: self.statements(["endmacro"], drop=true),
pug: false,
})
}
"call" => {
let params = if self.is_op("(") { self.parse_signature() } else { [] }
let call = self.parse_expression()
if !(call is Call(_, _)) {
raise self.fail("a call block ends in a call")
}
CallBlock(params, call, self.statements(["endcall"], drop=true))
}
"filter" => {
let chain = self.parse_filter(None)
FilterBlock(chain, self.statements(["endfilter"], drop=true))
}
"with" => {
let pairs = []
while self.current().tok != BlockEnd {
if !pairs.is_empty() {
self.expect_op(",")
}
let target = self.parse_target()
self.expect_op("=")
pairs.push((target, self.parse_expression()))
}
With(pairs, self.statements(["endwith"], drop=true))
}
"autoescape" => {
let setting = self.parse_expression()
Autoescape(setting, self.statements(["endautoescape"], drop=true))
}
"print" => {
let items = []
while self.current().tok != BlockEnd {
if !items.is_empty() {
self.expect_op(",")
}
items.push(self.parse_expression())
}
return [Line(token.line), ..items.map(item => Node::Out(item))]
}
"do" => Do(self.parse_tuple())
"debug" => Debug
"break" => Break
"continue" => Continue
"show" => {
let cond = self.parse_expression()
Show(cond, self.statements(["endshow"], drop=true))
}
"once" => {
let key = if self.current().tok == BlockEnd {
None
} else {
Some(self.parse_expression())
}
Once(key, self.statements(["endonce"], drop=true))
}
"trans" => self.parse_trans()
_ =>
raise Syntax(
template=self.template,
line=token.line,
column=token.column,
reason="unknown statement \{word}",
)
}
[Line(token.line), node]
}
///|
/// `with context` or `without context`, else the default.
fn Parser::parse_context(self : Parser, default : Bool) -> Bool {
if (self.is_name("with") || self.is_name("without")) &&
self.look().tok is Name("context") {
let with_ = self.next().tok is Name("with")
ignore(self.next())
with_
} else {
default
}
}
///|
/// The body of a statement: the end of the head, then nodes up to one of
/// `ends`. With `drop`, the end tag's name is consumed.
pub fn Parser::statements(
self : Parser,
ends : Array[String],
drop~ : Bool = false,
) -> Array[Node] raise @spec.Refused {
ignore(self.skip_op(":"))
self.expect(BlockEnd)
let body = (self.body)(self, ends)
if drop {
ignore(self.next())
}
body
}
///|
fn Parser::parse_for(self : Parser) -> Node raise @spec.Refused {
let target = self.parse_target(extra=["in"])
ignore(self.expect_name(name="in"))
let iter = self.parse_tuple(condexpr=false, extra=["recursive"])
let cond = if self.skip_name("if") {
Some(self.parse_expression())
} else {
None
}
let recursive = self.skip_name("recursive")
let body = self.statements(["endfor", "else"])
let otherwise = if self.next().tok is Name("endfor") {
[]
} else {
self.statements(["endfor"], drop=true)
}
For(target~, iter~, cond~, recursive~, each=false, body~, otherwise~)
}
///|
fn Parser::parse_if(self : Parser) -> Node raise @spec.Refused {
let branches = []
let mut otherwise = []
for ;; {
let cond = self.parse_tuple(condexpr=false)
branches.push((cond, self.statements(["elif", "else", "endif"])))
match self.next().tok {
Name("elif") => continue
Name("else") => {
otherwise = self.statements(["endif"], drop=true)
break
}
_ => break
}
}
If(branches, otherwise)
}
///|
fn Parser::parse_set(self : Parser) -> Node raise @spec.Refused {
let target = self.parse_target(field=true)
if self.skip_op("=") {
return Set(target, self.parse_tuple())
}
let filter = if self.skip_op("|") {
Some(self.parse_filter(None))
} else {
None
}
SetBlock(target, filter, self.statements(["endset"], drop=true))
}
///|
fn Parser::parse_block(self : Parser) -> Node raise @spec.Refused {
let name = self.expect_name()
let scoped = self.skip_name("scoped")
let required = self.skip_name("required")
let placement = if self.skip_name("append") {
Append
} else if self.skip_name("prepend") {
Prepend
} else {
Replace
}
if self.is_op("-") {
raise self.fail(
"block names have to be valid identifiers and may not contain hyphens",
)
}
let body = self.statements(["endblock"], drop=true)
if required && !body.iter().all(only_space) {
raise self.fail("required blocks can only contain comments or whitespace")
}
ignore(self.skip_name(name))
BlockDef({ name, body, scoped, required, placement, })
}
///|
fn only_space(node : Node) -> Bool {
match node {
Line(_) => true
Text(t) => t.iter().all(ch => ch is (' ' | '\t' | '\n' | '\r'))
_ => false
}
}
///|
fn Parser::parse_from(self : Parser) -> Node raise @spec.Refused {
let target = self.parse_expression()
ignore(self.expect_name(name="import"))
let names = []
let mut context = false
fn take_context() {
if (self.is_name("with") || self.is_name("without")) &&
self.look().tok is Name("context") {
context = self.next().tok is Name("with")
ignore(self.next())
true
} else {
false
}
}
for ;; {
if !names.is_empty() {
self.expect_op(",")
}
if take_context() {
break
}
let name = self.expect_name()
if name.has_prefix("_") {
raise self.fail("names starting with an underscore cannot be imported")
}
let renamed = if self.skip_name("as") {
Some(self.expect_name())
} else {
None
}
names.push((name, renamed))
if take_context() || !self.is_op(",") {
break
}
}
FromImport(target, names, context~)
}
///|
/// `(a, b=1, ...rest)`; the rest parameter is the indentation syntax's.
pub fn Parser::parse_signature(
self : Parser,
) -> Array[Param] raise @spec.Refused {
self.expect_op("(")
let params = []
let mut defaults = false
while !self.is_op(")") {
if !params.is_empty() {
self.expect_op(",")
if self.is_op(")") {
break
}
}
if self.is_op(".") {
for _ in 0..<3 {
self.expect_op(".")
}
params.push({ name: self.expect_name(), default: None, rest: true, })
continue
}
let name = self.expect_name()
if params.iter().any(p => p.name == name) {
raise self.fail("duplicate parameter \{name}")
}
let default = if self.skip_op("=") {
defaults = true
Some(self.parse_expression())
} else if defaults {
raise self.fail(
"a parameter without a default follows one with a default",
)
} else {
None
}
params.push({ name, default, rest: false, })
}
self.expect_op(")")
params
}
///|
/// `{% trans %}`, as Jinja's i18n extension reads it.
fn Parser::parse_trans(self : Parser) -> Node raise @spec.Refused {
let context = match self.current().tok {
Str(s) => {
ignore(self.next())
Some(s)
}
_ => None
}
let vars : Array[(String, Expr)] = []
let mut count : Expr? = None
let mut trimmed : Bool? = None
while self.current().tok != BlockEnd {
if !vars.is_empty() {
self.expect_op(",")
}
if self.skip_op(":") {
break
}
let name = self.expect_name()
if vars.iter().any(pair => pair.0 == name) {
raise self.fail("translatable variable \{name} defined twice")
}
let value = if self.skip_op("=") {
self.parse_expression()
} else if trimmed is None && name is ("trimmed" | "notrimmed") {
trimmed = Some(name == "trimmed")
continue
} else {
Name(name)
}
vars.push((name, value))
if count is None {
count = Some(value)
}
}
self.expect(BlockEnd)
let (singular_names, singular) = self.trans_block(true)
if count is None && !singular_names.is_empty() {
count = Some(Name(singular_names[0]))
}
let mut plural = None
let referenced = singular_names
if self.skip_name("pluralize") {
if self.current().tok != BlockEnd {
let name = self.expect_name()
match vars.iter().find_first(pair => pair.0 == name) {
Some(pair) => count = Some(pair.1)
None => raise self.fail("unknown variable \{name} for pluralisation")
}
}
self.expect(BlockEnd)
let (plural_names, text) = self.trans_block(false)
plural = Some(text)
referenced.append(plural_names)
}
ignore(self.next())
for name in referenced {
if !vars.iter().any(pair => pair.0 == name) {
vars.push((name, Name(name)))
}
}
if plural is Some(_) && count is None {
raise self.fail("pluralize without variables")
}
let squeeze = fn(s : String) {
if trimmed == Some(true) {
s.split_whitespace()
} else {
s
}
}
let escaped = !referenced.is_empty()
let fix = fn(s : String) {
if escaped {
s
} else {
s.replace_all(old="%%", new="%")
}
}
TransOut({
context,
singular: fix(squeeze(singular)),
plural: plural.map(p => fix(squeeze(p))),
vars,
count: if plural is Some(_) {
count
} else {
None
},
})
}
///|
/// The text of a translatable section up to `pluralize` or `endtrans`, with
/// each `{{ name }}` written as `%(name)s`. The current token is left on the
/// closing tag's name.
fn Parser::trans_block(
self : Parser,
pluralize : Bool,
) -> (Array[String], String) raise @spec.Refused {
let names = []
let buf = StringBuilder()
for ;; {
match self.current().tok {
Data(text) => {
buf.write_string(text.replace_all(old="%", new="%%"))
ignore(self.next())
}
VarBegin => {
ignore(self.next())
let name = self.expect_name()
names.push(name)
buf.write_string("%(\{name})s")
self.expect(VarEnd)
}
BlockBegin => {
ignore(self.next())
match self.current().tok {
Name("endtrans") => break
Name("pluralize") if pluralize => break
Name("pluralize") =>
raise self.fail(
"a translatable section can have only one pluralize section",
)
Name("trans") =>
raise self.fail(
"trans blocks cannot be nested; did you mean endtrans?",
)
tok =>
raise self.fail(
"control structures in translatable sections are not allowed; saw \{describe(tok)}",
)
}
}
Eof => raise self.fail("the translation block is never closed")
tok => raise self.fail("unexpected \{describe(tok)}")
}
}
(names, buf.to_string())
}
///|
/// Collapse runs of whitespace to one space and trim the ends, as the
/// `trimmed` modifier does.
fn String::split_whitespace(self : String) -> String {
let parts = []
let word = StringBuilder()
for ch in self {
if ch is (' ' | '\t' | '\n' | '\r' | '\u{0B}' | '\u{0C}') {
if !word.is_empty() {
parts.push(word.to_string())
word.reset()
}
} else {
word.write_char(ch)
}
}
if !word.is_empty() {
parts.push(word.to_string())
}
parts.join(" ")
}