|
| 1 | +-- External dependencies |
| 2 | +local class = require("pl.class") |
| 3 | +local epnf = require("epnf") |
| 4 | + |
| 5 | +local nulleof = "NULL\000" |
| 6 | +local eol = function () return "\n" end |
| 7 | + |
| 8 | +-- UTF8 code points up to four-byte encodings |
| 9 | +local function f1 (s) |
| 10 | + return string.byte(s) |
| 11 | +end |
| 12 | +local function f2 (s) |
| 13 | + local c1, c2 = string.byte(s, 1, 2) |
| 14 | + return c1 * 64 + c2 - 12416 |
| 15 | +end |
| 16 | +local function f3 (s) |
| 17 | + local c1, c2, c3 = string.byte(s, 1, 3) |
| 18 | + return (c1 * 64 + c2) * 64 + c3 - 925824 |
| 19 | +end |
| 20 | +local function f4 (s) |
| 21 | + local c1, c2, c3, c4 = string.byte(s, 1, 4) |
| 22 | + return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 |
| 23 | +end |
| 24 | +local cont = "\128\191" |
| 25 | + |
| 26 | +-- luacheck: push ignore |
| 27 | +local ftl_eof = epnf.define(function (_ENV) |
| 28 | + eol_eof = 1^0 * P(nulleof) * -1 |
| 29 | + START("eol_eof") |
| 30 | +end) |
| 31 | + |
| 32 | +local ftl_grammar = epnf.define(function (_ENV) |
| 33 | + local blank_inline = P" "^1 |
| 34 | + local line_end = P"\r\n" / eol + P"\n" + P(nulleof) |
| 35 | + blank_block = C((blank_inline^-1 * line_end)^1); local blank_block = V"blank_block" |
| 36 | + local blank = (blank_inline + line_end)^1 |
| 37 | + local digits = R"09"^1 |
| 38 | + local special_text_char = P"{" + P"}" |
| 39 | + local any_char = R("\0\127") / f1 + R("\194\223") * R(cont) / f2 + R("\224\239") * R(cont) * R(cont) / f3 + R("\240\244") * R(cont) * R(cont) * R(cont) / f4 |
| 40 | + local text_char = any_char - special_text_char - line_end |
| 41 | + local special_quoted_char = P'"' + P"\\" |
| 42 | + local special_escape = P"\\" * special_quoted_char |
| 43 | + local unicode_escape = (P"\\u" * P(4) * R("09", "af", "AF")^4) + (P"\\u" * P(6) * R("09", "af", "AF")^6) |
| 44 | + local quoted_char = (any_char - special_quoted_char - line_end) + special_escape + unicode_escape |
| 45 | + local indented_char = text_char - P"{" - P"*" - P"." |
| 46 | + Identifier = Cg(R("az", "AZ") * (R("az", "AZ", "09") + P"_" + P"-")^0, "name") |
| 47 | + local variant_list = V"Variant"^0 * V"DefaultVariant" * V"Variant" * line_end |
| 48 | + Variant = line_end * blank^-1 * V"VariantKey" * blank_inline^-1 * V"Pattern" |
| 49 | + DefaultVariant = line_end * blank^-1 * P"*" * V"VariantKey" * blank_inline^-1 * V"Pattern" |
| 50 | + VariantKey = P"[" * blank^-1 * (V"NumberLiteral" + V"Identifier") * blank^-1 * P"]" |
| 51 | + NumberLiteral = P"-"^-1 * digits * (P"." * digits)^-1 |
| 52 | + local inline_placeable = P"{" * blank^-1 * (V"SelectExpression" + V"InlineExpression") * blank^-1 * P"}" |
| 53 | + local block_placeable = blank_block * blank_inline^-1 * inline_placeable |
| 54 | + local inline_text = text_char^1 |
| 55 | + local block_text = blank_block * blank_inline * indented_char * inline_text^-1 |
| 56 | + StringLiteral = P'"' * quoted_char^0 * P'"' |
| 57 | + FunctionReference = V"Identifier" * V"CallArguments" |
| 58 | + MessageReference = V"Identifier" * V"AttributeAccessor"^-1 |
| 59 | + TermReference = P"-" * V"Identifier" * V"AttributeAccessor"^-1 * V"CallArguments"^-1 |
| 60 | + VariableReference = P"$" * V"Identifier" |
| 61 | + AttributeAccessor = P"." * V"Identifier" |
| 62 | + NamedArgument = V"Identifier" * blank^-1 * P":" * blank^-1 * (V"StringLiteral" + V"NumberLiteral") |
| 63 | + Argument = V"NamedArgument" + V"InlineExpression" |
| 64 | + local argument_list = (V"Argument" * blank^-1 * P"," * blank^-1)^0 * V"Argument"^-1 |
| 65 | + CallArguments = blank^-1 * P"(" * blank^-1 * argument_list * blank^-1 * P")" |
| 66 | + SelectExpression = V"InlineExpression" * blank^-1 * P"->" * blank_inline^-1 * variant_list |
| 67 | + InlineExpression = V"StringLiteral" + V"NumberLiteral" + V"FunctionReference" + V"MessageReference" + V"TermReference" + V"VariableReference" + inline_placeable |
| 68 | + PatternElement = Cg(C(inline_text + block_text + inline_placeable + block_placeable), "value") |
| 69 | + Pattern = V"PatternElement"^1 |
| 70 | + Attribute = line_end * blank^-1 * P"." * V"Identifier" * blank_inline^-1 * "=" * blank_inline^-1 * V"Pattern" |
| 71 | + local junk_line = (1-line_end)^0 * (P"\n" + P(nulleof)) |
| 72 | + Junk = Cg(junk_line * (junk_line - P"#" - P"-" - R("az","AZ"))^0, "content") |
| 73 | + local comment_char = any_char - line_end |
| 74 | + CommentLine = Cg(P"###" + P"##" + P"#", "sigil") * (" " * Cg(C(comment_char^0), "content"))^-1 * line_end |
| 75 | + Term = P"-" * V"Identifier" * blank_inline^-1 * "=" * blank_inline^-1 * V"Pattern" * V"Attribute"^0 |
| 76 | + Message = V"Identifier" * blank_inline^-1 * P"=" * blank_inline^-1 * ((V"Pattern" * V"Attribute"^0) + V"Attribute"^1) |
| 77 | + Entry = (V"Message" * line_end) + (V"Term" * line_end) + V"CommentLine" |
| 78 | + Resource = (V"Entry" + blank_block + V"Junk")^0 * (P(nulleof) + EOF"unparsable input") |
| 79 | + START("Resource") |
| 80 | +end) |
| 81 | +-- luacheck: pop |
| 82 | + |
| 83 | +-- TODO: if this doesn't need any state information make in a function not a class |
| 84 | +local FluentParser = class({ |
| 85 | + _init = function (self, input) |
| 86 | + return type(input) == "string" and self:parsestring(input) or error("unknown input type") |
| 87 | + end, |
| 88 | + |
| 89 | + addtrailingnewine = function(input) |
| 90 | + local hasnulleof = epnf.parsestring(ftl_eof, input) |
| 91 | + return type(hasnulleof) == "nil" and input..nulleof or input |
| 92 | + end, |
| 93 | + |
| 94 | + parsestring = function (self, input) |
| 95 | + input = self.addtrailingnewine(input) |
| 96 | + return epnf.parsestring(ftl_grammar, input) |
| 97 | + end |
| 98 | + }) |
| 99 | + |
| 100 | +return FluentParser |
0 commit comments