diff --git a/.busted b/.busted index 5b98ae9..c836b45 100644 --- a/.busted +++ b/.busted @@ -1,5 +1,5 @@ return { - default = { - lpath = "./?/init.lua" - } + default = { + lpath = "./?/init.lua", + }, } diff --git a/.editorconfig b/.editorconfig index 5384a2f..ced7b8d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -6,7 +6,20 @@ insert_final_newline = true [{*.lua,*.rockspec,.busted,.luacheckrc}] indent_style = space -indent_size = 2 +indent_size = 3 +max_line_length = 120 + +# StyLua keys +collapse_simple_statement = never +space_after_function_names = definitions + +# EmmyLuaCodeStyle keys +continuation_indent = 6 +quote_style = double, +call_arg_parentheses = keep, +trailing_table_separator = always, +align_call_args = true, +end_statement_with_semicolon = same_line [*.ftl] indent_style = space diff --git a/.github/workflows/busted.yml b/.github/workflows/busted.yml index 637c248..206f7e1 100644 --- a/.github/workflows/busted.yml +++ b/.github/workflows/busted.yml @@ -1,15 +1,15 @@ name: Busted -on: [push, pull_request] +on: [ push, pull_request ] jobs: + busted: strategy: fail-fast: false matrix: - luaVersion: ["5.4", "5.3", "5.2", "5.1", "luajit", "luajit-openresty"] - - runs-on: ubuntu-latest + luaVersion: [ "5.4", "5.3", "5.2", "5.1", "luajit", "luajit-openresty" ] + runs-on: ubuntu-20.04 steps: - name: Checkout uses: actions/checkout@v2 @@ -21,7 +21,7 @@ jobs: uses: leafo/gh-actions-luarocks@v4 - name: Setup dependencies run: | - luarocks install --only-deps fluent-dev-0.rockspec + luarocks install --only-deps -- fluent-dev-0.rockspec luarocks install busted luarocks install dkjson luarocks install luafilesystem diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 49f9fda..8cd43f9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,73 +1,34 @@ name: Deploy -on: - push: - tags: - - v* - paths: - - rockspecs/*.rockspec +on: [ push, workflow_dispatch ] jobs: affected: - runs-on: ubuntu-20.04 - outputs: - rockspecs: '["${{ steps.changed-files.outputs.all_modified_files }}"]' - steps: - - name: Checkout - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - id: changed-files - uses: tj-actions/changed-files@v18 - with: - files: rockspecs/*.rockspec - separator: '", "' + uses: lunarmodules/.github/.github/workflows/list_affected_rockspecs.yml@main build: needs: affected if: ${{ needs.affected.outputs.rockspecs }} - strategy: - fail-fast: false - matrix: - # Upstream Issue: https://github.com/leafo/gh-actions-luarocks/issues/8 - luaVersion: ["5.4", "5.3", "5.2", "5.1"] #, "luajit", "luajit-openresty"] - # Upstream Issue: https://github.com/leafo/gh-actions-luarocks/issues/8 - luarocksVersion: ["3.1.3"] # , "2.4.2"] - rockspec: ${{ fromJSON(needs.affected.outputs.rockspecs) }} - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup ‘lua’ - uses: leafo/gh-actions-lua@v9 - with: - luaVersion: ${{ matrix.luaVersion }} - - name: Setup ‘luarocks’ - uses: leafo/gh-actions-luarocks@v4 - with: - luarocksVersion: ${{ matrix.luarocksVersion }} - - name: Confirm rockspec builds - run: | - luarocks --lua-version ${{ matrix.luaVersion }} --local build -- ${{ matrix.rockspec }} + uses: lunarmodules/.github/.github/workflows/test_build_rock.yml@main + with: + rockspecs: ${{ needs.affected.outputs.rockspecs }} upload: - needs: [affected, build] - if: ${{ needs.affected.outputs.rockspecs && startsWith(github.ref, 'refs/tags/') && github.repository == 'alerque/fluent-lua' }} - strategy: - fail-fast: false - matrix: - rockspec: ${{ fromJSON(needs.affected.outputs.rockspecs) }} - runs-on: ubuntu-20.04 - steps: - - name: Checkout - uses: actions/checkout@v3 - - name: Setup ‘lua’ - uses: leafo/gh-actions-lua@v9 - - name: Setup ‘luarocks’ - uses: leafo/gh-actions-luarocks@v4 - - name: Setup dependencies - run: | - luarocks install dkjson - - run: | - luarocks upload --force --api-key ${{ secrets.LUAROCKS_APIKEY }} -- ${{ matrix.rockspec }} + needs: [ affected, build ] + # Only run upload if: + # 1. We are on the canonical repository (no uploads from forks) + # 2. The current commit is either tagged or on the default branch (the workflow will upload dev/scm rockspecs any + # time they are touched, tagged ones whenever the edited rockspec and tag match) + # 3. Some rockspecs were changed — this implies the commit changing the rockspec is the same one that gets tagged + if: >- + ${{ + github.repository == 'alerque/fluent-lua' && + ( github.ref_name == 'master' || startsWith(github.ref, 'refs/tags/') ) && + needs.affected.outputs.rockspecs + }} + uses: lunarmodules/.github/.github/workflows/upload_to_luarocks.yml@main + with: + rockspecs: ${{ needs.affected.outputs.rockspecs }} + secrets: + apikey: ${{ secrets.LUAROCKS_APIKEY }} diff --git a/.github/workflows/luacheck.yml b/.github/workflows/luacheck.yml index 2f20456..10343b0 100644 --- a/.github/workflows/luacheck.yml +++ b/.github/workflows/luacheck.yml @@ -1,13 +1,13 @@ -name: Luacheck +name: luacheck -on: [push, pull_request] +on: [ push, pull_request ] jobs: luacheck: runs-on: ubuntu-20.04 steps: - - name: Checkout + - name: checkout uses: actions/checkout@v3 - - name: Luacheck + - name: luacheck uses: lunarmodules/luacheck@v0 diff --git a/.luacheckrc b/.luacheckrc index 0b87dfa..f42ed5f 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -1,17 +1,17 @@ std = "max" include_files = { - "**/*.lua", - "**/*.rockspec", - ".busted", - ".luacheckrc" + "**/*.lua", + "**/*.rockspec", + ".busted", + ".luacheckrc", } exclude_files = { - ".install", - ".lua", - ".luarocks", - "lua_modules" + ".install", + ".lua", + ".luarocks", + "lua_modules", } files["spec"] = { - std = "+busted" + std = "+busted", } max_line_length = false diff --git a/CHANGELOG.md b/CHANGELOG.md index 9667007..580e7ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,18 @@ All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org) for commit guidelines. +## [v0.2.0](https://github.com/alerque/fluent-lua/releases/v0.2.0) — 2022-04-16 + +### Bug Fixes + +- Inform Git about fixtures with special requirements +- Correct access protocol to GitHub in dev rockspec +- Correct logic errors revealed by new Luacheck release + +### Features + +- Add external access to file loader + ## [v0.1.3](https://github.com/alerque/fluent-lua/releases/v0.1.3) — 2022-03-11 ### Bug Fixes @@ -20,49 +32,49 @@ All notable changes to this project will be documented in this file. See [conven - Use rockspec syntax compatible with older LuaRocks -## [v0.1.0](https://github.com/alerque/fluent-lua/releases/v0.1.0) ­ 2022-03-11 +## [v0.1.0](https://github.com/alerque/fluent-lua/releases/v0.1.0) — 2022-03-11 -Completely overhaul the object handling using more robust Penlight class syntax variant. -Property and attribute indexing now works in many more situations. -The test coverage is greatly improved as well, so the things that work are provably functional. -Many edge case bugs where resources leaked between locales have been fixed. -Loading multiple locales into a bundle and updating existing messages works as expected. +- Completely overhaul the object handling using more robust Penlight class syntax variant. +- Property and attribute indexing now works in many more situations. +- The test coverage is greatly improved as well, so the things that work are provably functional. +- Many edge case bugs where resources leaked between locales have been fixed. +- Loading multiple locales into a bundle and updating existing messages works as expected. -## [v0.0.5](https://github.com/alerque/fluent-lua/releases/v0.0.5) ­ 2021-12-24 +## [v0.0.5](https://github.com/alerque/fluent-lua/releases/v0.0.5) — 2021-12-24 -Cut a safe-haven release for anybody (including myself) using this in production before I move on. -Looking at Git history it looks like refinements include improved PEG grammars, saner namespacing for easier project integration, better use of Penlight classing, and more advanced handling of terms. -Dependencies now include cldr-lua, and tests now use CLDR compatible locales. -Lua 5.4 support was also officially added. +- Cut a safe-haven release for anybody (including myself) using this in production before I move on. +- Looking at Git history it looks like refinements include improved PEG grammars, saner namespacing for easier project integration, better use of Penlight classing, and more advanced handling of terms. +- Dependencies now include cldr-lua, and tests now use CLDR compatible locales. +- Lua 5.4 support was also officially added. -## [v0.0.4](https://github.com/alerque/fluent-lua/releases/v0.0.1) ­ 2019-09-27 +## [v0.0.4](https://github.com/alerque/fluent-lua/releases/v0.0.1) — 2019-09-27 Add support for attributes plus access to messages using idiomatic Lua (table properties). -## [v0.0.3](https://github.com/alerque/fluent-lua/releases/v0.0.1) ­ 2019-09-26 +## [v0.0.3](https://github.com/alerque/fluent-lua/releases/v0.0.1) — 2019-09-26 -Add support for more types including format support for TextElement, StringLiteral, NumberLiteral, and VariableReference. -Variable substitutions can be done by passing a parameters table to `format()`. -Internally manipulating nodes in the API is now easier with meta methods. -For example merge comments with `Comment + Comment`, attach children with `Message * Comment`, etc. +- Add support for more types including format support for TextElement, StringLiteral, NumberLiteral, and VariableReference. +- Variable substitutions can be done by passing a parameters table to `format()`. +- Internally manipulating nodes in the API is now easier with meta methods. +- For example merge comments with `Comment + Comment`, attach children with `Message * Comment`, etc. -## [v0.0.2](https://github.com/alerque/fluent-lua/releases/v0.0.1) ­ 2019-09-25 +## [v0.0.2](https://github.com/alerque/fluent-lua/releases/v0.0.1) — 2019-09-25 -Massage the AST returned by the PEG grammar so that about 1/3 of the possible types look like the reference Fluent spec. -A basic Lua API is starting to take shape, modeled most closely to the Python implementation. -It is possible to load and parse almost any FTL file, and possible to format any messages that are plain strings (no parameters, attributes, functions, or other jazz yet). -Note there is no locale handling yet so it's only usable with separate instances per locale. -Also `add_messages()` likely only works once, so cram your whole FTL resource in there for now. +- Massage the AST returned by the PEG grammar so that about 1/3 of the possible types look like the reference Fluent spec. +- A basic Lua API is starting to take shape, modeled most closely to the Python implementation. +- It is possible to load and parse almost any FTL file, and possible to format any messages that are plain strings (no parameters, attributes, functions, or other jazz yet). +- Note there is no locale handling yet so it's only usable with separate instances per locale. +- Also `add_messages()` likely only works once, so cram your whole FTL resource in there for now. -## [v0.0.1](https://github.com/alerque/fluent-lua/releases/v0.0.1) ­ 2019-09-14 +## [v0.0.1](https://github.com/alerque/fluent-lua/releases/v0.0.1) — 2019-09-14 -Complete a PEG grammar based parser for the entire 1.0 Fluent file format spec. -All the pieces are there, but it's only partially tested. -It at least parses a few basic types of entries. -The AST it returns is straight out of *luaebnf* and probably needs massaging to match the reference ones (via capture groups?), then it needs testing against the upstream fixtures. +- Complete a PEG grammar based parser for the entire 1.0 Fluent file format spec. +- All the pieces are there, but it's only partially tested. +- It at least parses a few basic types of entries. +- The AST it returns is straight out of *luaebnf* and probably needs massaging to match the reference ones (via capture groups?), then it needs testing against the upstream fixtures. -## [v0.0.0](https://github.com/alerque/fluent-lua/releases/v0.0.0) ­ 2019-09-12 +## [v0.0.0](https://github.com/alerque/fluent-lua/releases/v0.0.0) — 2019-09-12 -Initialize project with some boilerplate Lua apparatus. +- Initialize project with some boilerplate Lua apparatus. diff --git a/Makefile b/Makefile index 28d3206..3d2cde3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -PACKAGE = fluent +PACKAGE_NAME = fluent SHELL := zsh .SHELLFLAGS := +o nomatch -e -c @@ -13,27 +13,30 @@ SHELL := zsh JOBS ?= $(shell nproc 2>- || sysctl -n hw.ncpu 2>- || echo 1) MAKEFLAGS += -j$(JOBS) -Otarget -VERSION != git describe --tags --all --abbrev=7 | sed 's/-/-r/' +VERSION != git describe --tags --always --abbrev=7 | sed 's/-/-r/' SEMVER != git describe --tags | sed 's/^v//;s/-.*//' ROCKREV = 0 TAG ?= v$(SEMVER) LUAROCKS_ARGS ?= --local --tree lua_modules -SCM_ROCK = $(PACKAGE)-dev-0.rockspec -REL_ROCK = rockspecs/$(PACKAGE)-$(SEMVER)-$(ROCKREV).rockspec -SCM_SRC = $(PACKAGE)-dev-0.src.rock -REL_SRC = $(PACKAGE)-$(SEMVER)-$(ROCKREV).src.rock +DEV_SPEC = $(PACKAGE_NAME)-dev-$(ROCKREV).rockspec +DEV_ROCK = $(PACKAGE_NAME)-dev-$(ROCKREV).src.rock +REL_SPEC = rockspecs/$(PACKAGE_NAME)-$(SEMVER)-$(ROCKREV).rockspec +REL_ROCK = $(PACKAGE_NAME)-$(SEMVER)-$(ROCKREV).src.rock .PHONY: all -all: $(SCM_ROCK) $(SCM_SRC) +all: rockspecs dist + +.PHONY: rockspecs +rockspecs: $(DEV_SPEC) $(REL_SPEC) .PHONY: dist -dist: $(REL_ROCK) $(REL_SRC) +dist: $(DEV_ROCK) $(REL_ROCK) .PHONY: install install: - luarocks $(LUAROCKS_ARGS) make $(SCM_ROCK) + luarocks $(LUAROCKS_ARGS) make $(DEV_SPEC) define rockpec_template = sed -e "s/@SEMVER@/$(SEMVER)/g" \ @@ -42,14 +45,18 @@ define rockpec_template = $< > $@ endef -$(SCM_ROCK): SEMVER = dev -$(SCM_ROCK): TAG = master -$(SCM_ROCK): $(PACKAGE).rockspec.in +$(DEV_SPEC): SEMVER = dev +$(DEV_SPEC): TAG = master +$(DEV_SPEC): $(PACKAGE_NAME).rockspec.in $(rockpec_template) + sed -i \ + "1i -- DO NOT EDIT! Modify template $< and rebuild with \`make $@\`" \ + -e '/tag =/s/tag/branch/' \ + $@ -rockspecs/$(PACKAGE)-%-0.rockspec: SEMVER = $* -rockspecs/$(PACKAGE)-%-0.rockspec: TAG = v$* -rockspecs/$(PACKAGE)-%-0.rockspec: $(PACKAGE).rockspec.in +rockspecs/$(PACKAGE_NAME)-%-$(ROCKREV).rockspec: SEMVER = $* +rockspecs/$(PACKAGE_NAME)-%-$(ROCKREV).rockspec: TAG = v$* +rockspecs/$(PACKAGE_NAME)-%-$(ROCKREV).rockspec: $(PACKAGE_NAME).rockspec.in $(rockpec_template) sed -i \ -e '/rockspec_format/s/3.0/1.0/' \ @@ -59,10 +66,10 @@ rockspecs/$(PACKAGE)-%-0.rockspec: $(PACKAGE).rockspec.in -e '/labels/d' \ $@ -$(PACKAGE)-dev-0.src.rock: $(SCM_ROCK) +$(PACKAGE_NAME)-dev-$(ROCKREV).src.rock: $(DEV_SPEC) luarocks $(LUAROCKS_ARGS) pack $< -$(PACKAGE)-%.src.rock: rockspecs/$(PACKAGE)-%.rockspec +$(PACKAGE_NAME)-%.src.rock: rockspecs/$(PACKAGE_NAME)-%.rockspec luarocks $(LUAROCKS_ARGS) pack $< .PHONY: check diff --git a/cliff.toml b/cliff.toml index 3a50f5c..04cd4a6 100644 --- a/cliff.toml +++ b/cliff.toml @@ -23,17 +23,17 @@ footer = """ [git] commit_parsers = [ - { message = "^feat:", group = "Features" }, - { message = "^fix:", group = "Bug Fixes" }, - { message = "^docs:", skip = true }, - { message = "^perf:", group = "Optimizations" }, - { message = "^refactor:", skip = true }, - { message = "^style:", skip = true}, - { message = "^test:", skip = true}, - { message = "^ci:", skip = true }, - { message = "^chore:", skip = true }, + { message = "^feat(.*)?:", group = "Features" }, + { message = "^fix(.*)?:", group = "Bug Fixes" }, + { message = "^docs(.*)?:", skip = true }, + { message = "^perf(.*)?:", group = "Optimizations" }, + { message = "^refactor(.*)?:", skip = true }, + { message = "^style(.*)?:", skip = true}, + { message = "^test(.*)?:", skip = true}, + { message = "^ci(.*)?:", skip = true }, + { message = "^chore(.*)?:", skip = true }, { body = ".*security", group = "Security" }, ] filter_commits = true tag_pattern = "v[0-9]*.[0-9]*.[0-9]*" -skip_tags = "v0.0.[0-5]" +skip_tags = "v0.(0.[0-5]|1.0)" diff --git a/fluent-dev-0.rockspec b/fluent-dev-0.rockspec index 4535357..85e1af2 100644 --- a/fluent-dev-0.rockspec +++ b/fluent-dev-0.rockspec @@ -4,7 +4,7 @@ version = "dev-0" source = { url = "git+https://github.com/alerque/fluent-lua.git", - tag = "master" + branch = "master", } description = { @@ -15,22 +15,22 @@ description = { homepage = "https://github.com/alerque/fluent-lua", issues_url = "https://github.com/alerque/fluent-lua/issues", maintainer = "Caleb Maclennan ", - labels = { "i18n" } + labels = { "i18n" }, } dependencies = { "lua >= 5.1", "cldr", "luaepnf", - "penlight" + "penlight", } test_dependencies = { - "busted", + "busted", } test = { - type = "busted", + type = "busted", } build = { @@ -40,6 +40,6 @@ build = { ["fluent.messages"] = "fluent/messages.lua", ["fluent.parser"] = "fluent/parser.lua", ["fluent.resource"] = "fluent/resource.lua", - ["fluent.syntax"] = "fluent/syntax.lua" - } + ["fluent.syntax"] = "fluent/syntax.lua", + }, } diff --git a/fluent/init.lua b/fluent/init.lua index 9e34740..7736acf 100644 --- a/fluent/init.lua +++ b/fluent/init.lua @@ -10,64 +10,81 @@ local FluentResource = require("fluent.resource") local FluentBundle = class() function FluentBundle:_init (locale) - self.locales = {} - self:set_locale(locale) - -- Work around Penlight #307 - -- self:catch(self.get_message) - self:_patch_init() - return self + self.locales = {} + self:set_locale(locale) + -- Work around Penlight #307 + -- self:catch(self.get_message) + self:_patch_init() + return self end function FluentBundle:_patch_init () - if type(rawget(getmetatable(self), "__index")) ~= "function" then - self:catch(function(_, identifier) return self:get_message(identifier) end) - end + if type(rawget(getmetatable(self), "__index")) ~= "function" then + self:catch(function (_, identifier) + return self:get_message(identifier) + end) + end end function FluentBundle:set_locale (locale) - self.locale = CLDR.locales[locale] and locale or "und" - if not self.locales[self.locale] then - self.locales[self.locale] = FluentResource() - end - return self:get_locale() + self.locale = CLDR.locales[locale] and locale or "und" + if not self.locales[self.locale] then + self.locales[self.locale] = FluentResource() + end + return self:get_locale() end function FluentBundle:get_locale () - return self.locale + return self.locale end function FluentBundle:get_resource (locale) - local locales = self.locales - local resource = locales[locale or self:get_locale()] - resource._patch_init(resource) - return resource + local locales = self.locales + local resource = locales[locale or self:get_locale()] + resource._patch_init(resource) + return resource end function FluentBundle:get_message (identifier) - local resource = self:get_resource() - -- TODO iterate over fallback locales if not found in current one - return resource:get_message(identifier) + local resource = self:get_resource() + -- TODO iterate over fallback locales if not found in current one + return resource:get_message(identifier) end function FluentBundle:add_messages (input, locale) - -- Work around Penlight #307 - -- self:_patch_init() - locale = locale or self:get_locale() - local syntax = FluentSyntax() - local messages = - type(input) == "string" - and syntax:parsestring(input) - or tablex.reduce('+', tablex.imap(function (v) - return syntax:parsestring(v) - end, input)) - local resource = self:get_resource(locale) - return resource + messages + -- Work around Penlight #307 + -- self:_patch_init() + locale = locale or self:get_locale() + local syntax = FluentSyntax() + local messages = type(input) == "string" and syntax:parsestring(input) + or tablex.reduce( + "+", + tablex.imap(function (v) + return syntax:parsestring(v) + end, input) + ) + local resource = self:get_resource(locale) + return resource + messages +end + +function FluentBundle:load_file (fname, locale) + locale = locale or self:get_locale() + local syntax = FluentSyntax() + local messages = type(fname) == "string" and syntax:parsefile(fname) + or tablex.reduce( + "+", + tablex.imap(function (v) + return syntax:parsefile(v) + end, fname) + ) + local resource = self:get_resource(locale) + return resource + messages end function FluentBundle:format (identifier, parameters) - local resource = self:get_resource() - local message = resource:get_message(identifier) - return message:format(parameters) + local resource = self:get_resource() + local message = resource:get_message(identifier) + return message:format(parameters) end return FluentBundle diff --git a/fluent/messages.lua b/fluent/messages.lua index 1a9230d..77617e5 100644 --- a/fluent/messages.lua +++ b/fluent/messages.lua @@ -7,104 +7,108 @@ local FTL = {} -- Utility function to cast ast nodes from the syntax parser to corresponding class instances local leaf_to_node = function (leaf, resource) - if type(leaf) == "table" and type(leaf.id) == "string" then - return FTL[leaf.id](leaf, resource) - end + if type(leaf) == "table" and type(leaf.id) == "string" then + return FTL[leaf.id](leaf, resource) + end end local FluentNode = class() FluentNode._name = "FluentNode" function FluentNode:_init (ast, resource) - self:set_parent(resource) - for key, value in pairs(ast) do - if type(key) == "string" then - if key == "id" then - self.type = value - elseif key == "value" then - self[key] = string.gsub(string.gsub(value, "\r\n?","\n"), "^\n+ +", "") - elseif key ~= "pos" and key ~= "sigil" then - self[key] = value + self:set_parent(resource) + for key, value in pairs(ast) do + if type(key) == "string" then + if key == "id" then + self.type = value + elseif key == "value" then + self[key] = string.gsub(string.gsub(value, "\r\n?", "\n"), "^\n+ +", "") + elseif key ~= "pos" and key ~= "sigil" then + self[key] = value + end end - end - end - tablex.foreachi(ast, function (leaf) + end + tablex.foreachi(ast, function (leaf) local node = leaf_to_node(leaf, resource) self:inject(node) - end) - return self + end) + return self end function FluentNode:set_parent (resource) - if self._name == "MessageReference" or self._name == "TermReference" then - rawset(getmetatable(self), "_resource", resource) - end + if self._name == "MessageReference" or self._name == "TermReference" then + rawset(getmetatable(self), "_resource", resource) + end end function FluentNode:get_parent () - return rawget(getmetatable(self), "_resource") + return rawget(getmetatable(self), "_resource") end function FluentNode:inject (node) - if type(node) ~= "table" then return nil end - local elements = rawget(self, "elements") - if not (elements and #elements >= 1 and elements[#elements]:append(node)) - and not self:modify(node) - and not self:attach(node) then - if not elements then - error("Undefined injection "..node.type .. " into " .. self.type) - end - table.insert(elements, node) - end + if type(node) ~= "table" then + return nil + end + local elements = rawget(self, "elements") + if + not (elements and #elements >= 1 and elements[#elements]:append(node)) + and not self:modify(node) + and not self:attach(node) + then + if not elements then + error("Undefined injection " .. node.type .. " into " .. self.type) + end + table.insert(elements, node) + end end function FluentNode:dump_ast () - local ast = { type = self.type } - for k, v in pairs(self) do - ast[k] = v - end - return ast + local ast = { type = self.type } + for k, v in pairs(self) do + ast[k] = v + end + return ast end function FluentNode:append (node) - local func = node and rawget(getmetatable(node), "__add") - return node and type(func) == "function" and node + self + local func = node and rawget(getmetatable(node), "__add") + return node and type(func) == "function" and node + self end function FluentNode:modify (node) - local func = node and rawget(getmetatable(node), "__mod") - return node and type(func) == "function" and node % self + local func = node and rawget(getmetatable(node), "__mod") + return node and type(func) == "function" and node % self end function FluentNode:attach (node) - local func = node and rawget(getmetatable(node), "__mul") - return node and type(func) == "function" and node * self + local func = node and rawget(getmetatable(node), "__mul") + return node and type(func) == "function" and node * self end function FluentNode:__call (...) - return self:format(...) + return self:format(...) end function FluentNode:__tostring () - return self:format({}) + return self:format({}) end function FluentNode.__concat (a, b) - return tostring(a) .. tostring(b) + return tostring(a) .. tostring(b) end FTL.blank_block = class(FluentNode) FTL.blank_block._name = "blank_block" function FTL.blank_block:_init (ast, resource) - self:super(ast, resource) - local _, count = string.gsub(ast[1], "\n", "") - getmetatable(self).discardable = count == 0 - return self + self:super(ast, resource) + local _, count = string.gsub(ast[1], "\n", "") + getmetatable(self).discardable = count == 0 + return self end FTL.Entry = function (ast, resource) - return leaf_to_node(ast[1], resource) + return leaf_to_node(ast[1], resource) end FTL.Junk = class(FluentNode) @@ -114,45 +118,47 @@ FTL.Message = class(FluentNode) FTL.Message._name = "Message" function FTL.Message:_init (ast, resource) - self.attributes = setmetatable({}, { map = {} }) - self:super(ast, resource) - -- Work around Penlight #307 - -- self:catch(self.get_attribute) - self:_patch_init() - return self + self.attributes = setmetatable({}, { map = {} }) + self:super(ast, resource) + -- Work around Penlight #307 + -- self:catch(self.get_attribute) + self:_patch_init() + return self end function FTL.Message:_patch_init () - if type(rawget(getmetatable(self), "__index")) ~= "function" then - self:catch(function(_, attribute) return self:get_attribute(attribute) end) - end + if type(rawget(getmetatable(self), "__index")) ~= "function" then + self:catch(function (_, attribute) + return self:get_attribute(attribute) + end) + end end function FTL.Message:set_attribute (attribute) - local attributes = rawget(self, "attributes") - local id = attribute.id.name - local map = getmetatable(attributes).map - local k = #attributes + 1 - attributes[k] = attribute - map[id] = k - -- TODO Fix with working catch? - -- getmetatable(self)[id] = attribute + local attributes = rawget(self, "attributes") + local id = attribute.id.name + local map = getmetatable(attributes).map + local k = #attributes + 1 + attributes[k] = attribute + map[id] = k + -- TODO Fix with working catch? + -- getmetatable(self)[id] = attribute end function FTL.Message:get_attribute (attribute) - -- local raw = rawget(self, attribute) - -- if raw then return raw end - -- if not rawget(self, "id") then - -- error("Penlight bug, please use get_attribute() rather than table index syntax for now") - -- end - local attributes = rawget(self, "attributes") - local map = getmetatable(attributes).map - local k = map[attribute] - return attributes[k] + -- local raw = rawget(self, attribute) + -- if raw then return raw end + -- if not rawget(self, "id") then + -- error("Penlight bug, please use get_attribute() rather than table index syntax for now") + -- end + local attributes = rawget(self, "attributes") + local map = getmetatable(attributes).map + local k = map[attribute] + return attributes[k] end function FTL.Message:format (parameters) - return self.value:format(parameters) + return self.value:format(parameters) end FTL.Term = FTL.Message @@ -161,144 +167,148 @@ FTL.Identifier = class(FluentNode) FTL.Identifier._name = "Identifier" function FTL.Identifier:__mod (node) - if node:is_a(FTL.VariantKey) then - node.key = self - else - node.id = self - end - return node + if node:is_a(FTL.VariantKey) then + node.key = self + else + node.id = self + end + return node end function FTL.Identifier:format () - return self.name + return self.name end FTL.Pattern = class(FluentNode) FTL.Pattern._name = "Pattern" function FTL.Pattern:_init (ast, resource) - self.elements = {} - self:super(ast, resource) - self:dedent() - return self + self.elements = {} + self:super(ast, resource) + self:dedent() + return self end function FTL.Pattern:dedent () - local mindent = function (node) - local indents = {} - if type(node.value) == "string" then - for indent in string.gmatch(node.value, "\n *%S") do - table.insert(indents, #indent-2) - end - end - return tablex.reduce(math.min, indents) or 0 - end - local striplen = tablex.reduce(math.min, tablex.imap(mindent, self.elements)) or 0 - local i, strippref = 1, "\n" - while i <= striplen do - strippref = strippref .. " " - i = i + 1 - end - local strip = function (node, key, len) - if type(node.value) == "string" then - local value = node.value - if len >= 1 then - value = string.gsub(value, strippref, "\n\n") + local mindent = function (node) + local indents = {} + if type(node.value) == "string" then + for indent in string.gmatch(node.value, "\n *%S") do + table.insert(indents, #indent - 2) + end end - value = key == 1 and string.gsub(value, "^[\n ]+", "") or value - value = key == #self.elements and string.gsub(value, "[\n ]+$", "") or value - if string.len(value) == 0 then - self.elements[key] = nil - else - self.elements[key].value = value + return tablex.reduce(math.min, indents) or 0 + end + local striplen = tablex.reduce(math.min, tablex.imap(mindent, self.elements)) or 0 + local i, strippref = 1, "\n" + while i <= striplen do + strippref = strippref .. " " + i = i + 1 + end + local strip = function (node, key, len) + if type(node.value) == "string" then + local value = node.value + if len >= 1 then + value = string.gsub(value, strippref, "\n\n") + end + value = key == 1 and string.gsub(value, "^[\n ]+", "") or value + value = key == #self.elements and string.gsub(value, "[\n ]+$", "") or value + if string.len(value) == 0 then + self.elements[key] = nil + else + self.elements[key].value = value + end end - end - end - tablex.foreachi(self.elements, strip, striplen) + end + tablex.foreachi(self.elements, strip, striplen) end function FTL.Pattern:__mul (node) - if node:is_a(FTL.Message) or node:is_a(FTL.Attribute) or node:is_a(FTL.Variant) then - node.value = self - return node - end + if node:is_a(FTL.Message) or node:is_a(FTL.Attribute) or node:is_a(FTL.Variant) then + node.value = self + return node + end end function FTL.Pattern:format (parameters) - local values = tablex.map_named_method('format', self.elements, parameters) - return table.concat(values) + local values = tablex.map_named_method("format", self.elements, parameters) + return table.concat(values) end FTL.TextElement = class(FluentNode) -FTL.TextElement._name ="TextElement" +FTL.TextElement._name = "TextElement" function FTL.TextElement:_init (ast, resource) - getmetatable(self).appendable = true - ast.id = "TextElement" - return self:super(ast, resource) + getmetatable(self).appendable = true + ast.id = "TextElement" + return self:super(ast, resource) end function FTL.TextElement:__add (node) - if self:is_a(node:is_a()) and self.appendable and node.appendable then - node.value = (node.value or "") .. "\n" .. (self.value or "") - return node - end + if self:is_a(node:is_a()) and self.appendable and node.appendable then + node.value = (node.value or "") .. "\n" .. (self.value or "") + return node + end end function FTL.TextElement:format () - return self.value + return self.value end FTL.Placeable = class(FluentNode) FTL.Placeable._name = "Placeable" function FTL.Placeable:_init (ast, resource) - getmetatable(self).appendable = true - ast.id = "Placeable" - ast.expression = leaf_to_node(ast.expression, resource) - return self:super(ast, resource) + getmetatable(self).appendable = true + ast.id = "Placeable" + ast.expression = leaf_to_node(ast.expression, resource) + return self:super(ast, resource) end function FTL.Placeable:__mod (node) - if node:is_a(FTL.Pattern) then - table.insert(node.elements, self) - return node - elseif node:is_a(FTL.Placeable) or node:is_a(FTL.SelectExpression) then - node.expression = self - return node - else error("Undefined attach "..self.type.." to "..node.type) end + if node:is_a(FTL.Pattern) then + table.insert(node.elements, self) + return node + elseif node:is_a(FTL.Placeable) or node:is_a(FTL.SelectExpression) then + node.expression = self + return node + else + error("Undefined attach " .. self.type .. " to " .. node.type) + end end function FTL.Placeable:format (parameters) - return self.expression:format(parameters) + return self.expression:format(parameters) end FTL.PatternElement = function (ast, resource) - if ast.value then - return FTL.TextElement(ast, resource) - else - return FTL.Placeable(ast, resource) - end + if ast.value then + return FTL.TextElement(ast, resource) + else + return FTL.Placeable(ast, resource) + end end FTL.StringLiteral = class(FluentNode) FTL.StringLiteral._name = "StringLiteral" function FTL.StringLiteral:format () - return self.value + return self.value end function FTL.StringLiteral:__mod (node) - if node:is_a(FTL.SelectExpression) then - node.selector = self - return node - elseif node:is_a(FTL.Placeable) then - node.expression = self - return node - elseif node:is_a(FTL.VariantKey) then - node.key = self - return node - else error("Undefined attach "..self.type.." to "..node.type) end + if node:is_a(FTL.SelectExpression) then + node.selector = self + return node + elseif node:is_a(FTL.Placeable) then + node.expression = self + return node + elseif node:is_a(FTL.VariantKey) then + node.key = self + return node + else + error("Undefined attach " .. self.type .. " to " .. node.type) + end end FTL.NumberLiteral = class(FluentNode) @@ -311,34 +321,33 @@ FTL.VariableReference._name = "VariableReference" FTL.VariableReference.__mod = FTL.StringLiteral.__mod function FTL.VariableReference:format (parameters) - return parameters[self.id.name] + return parameters[self.id.name] end - FTL.MessageReference = class(FluentNode) FTL.MessageReference._name = "MessageReference" function FTL.MessageReference:format (parameters) - return self:get_parent():get_message(self.id.name):format(parameters) + return self:get_parent():get_message(self.id.name):format(parameters) end FTL.TermReference = class(FluentNode) FTL.TermReference._name = "TermReference" function FTL.TermReference:_init (ast, resource) - ast.id = "TermReference" - return self:super(ast, resource) + ast.id = "TermReference" + return self:super(ast, resource) end function FTL.TermReference:__mul (node) - if node:is_a(FTL.SelectExpression) then - node.selector = self - return node - end + if node:is_a(FTL.SelectExpression) then + node.selector = self + return node + end end function FTL.TermReference:format (parameters) - return self:get_parent():get_term(self.id.name):format(parameters) + return self:get_parent():get_term(self.id.name):format(parameters) end FTL._TermReference = FTL.TermReference @@ -349,45 +358,55 @@ FTL.FunctionReference.__mod = FTL.StringLiteral.__mod -- TODO: this needs locale data! local tocldr = function (number) - number = tonumber(number) - if not number then return nil - elseif number == 1 then return "one" - else return "other" end + number = tonumber(number) + if not number then + return nil + elseif number == 1 then + return "one" + else + return "other" + end end FTL.SelectExpression = class(FluentNode) FTL.SelectExpression._name = "SelectExpression" function FTL.SelectExpression:_init (ast, resource) - ast.id = "SelectExpression" - self.selector = {} - self.variants = {} - return self:super(ast, resource) + ast.id = "SelectExpression" + self.selector = {} + self.variants = {} + return self:super(ast, resource) end function FTL.SelectExpression:format (parameters) - local variant, result, default - if parameters then - if self.selector:is_a(FTL.VariableReference) then - variant = parameters[tostring(self.selector.id)] - else error("Undefined format "..self.type.." selector "..self.selector) end - end - for _, element in ipairs(self.variants) do - if element.default then default = element end - if variant - and tostring(element.key) == tostring(variant) - or tostring(element.key) == tocldr(tostring(variant)) - then result = element end - end - return (result or default).value:format(parameters) - end - -FTL.InlineExpression = function(ast, resource) - if ast[1].id == "InlineExpression" then - return FTL.Placeable(ast, resource) - else - return leaf_to_node(ast[1], resource) - end + local variant, result, default + if parameters then + if self.selector:is_a(FTL.VariableReference) then + variant = parameters[tostring(self.selector.id)] + else + error("Undefined format " .. self.type .. " selector " .. self.selector) + end + end + for _, element in ipairs(self.variants) do + if element.default then + default = element + end + if + variant and tostring(element.key) == tostring(variant) + or tostring(element.key) == tocldr(tostring(variant)) + then + result = element + end + end + return (result or default).value:format(parameters) +end + +FTL.InlineExpression = function (ast, resource) + if ast[1].id == "InlineExpression" then + return FTL.Placeable(ast, resource) + else + return leaf_to_node(ast[1], resource) + end end FTL._InlineExpression = FTL.InlineExpression @@ -396,55 +415,59 @@ FTL.variant_list = class(FluentNode) FTL.variant_list._name = "variant_list" function FTL.variant_list:_init (ast, resource) - self.elements = {} - return self:super(ast, resource) + self.elements = {} + return self:super(ast, resource) end function FTL.variant_list:__mod (node) - if node:is_a(FTL.SelectExpression) then - tablex.insertvalues(node.variants, self.elements) - return node - else error("Undefined attach "..self.type.." to "..node.type) end + if node:is_a(FTL.SelectExpression) then + tablex.insertvalues(node.variants, self.elements) + return node + else + error("Undefined attach " .. self.type .. " to " .. node.type) + end end FTL.Variant = class(FluentNode) FTL.Variant._name = "Variant" function FTL.Variant:_init (ast, resource) - ast.id = "Variant" - ast.default = ast.default or false - return self:super(ast, resource) + ast.id = "Variant" + ast.default = ast.default or false + return self:super(ast, resource) end FTL.VariantKey = class(FluentNode) FTL.VariantKey._name = "VariantKey" function FTL.VariantKey:__mod (node) - if node:is_a(FTL.Variant) then - node.key = self.key - return node - else error("Undefined attach "..self.type.." to "..node.type) end + if node:is_a(FTL.Variant) then + node.key = self.key + return node + else + error("Undefined attach " .. self.type .. " to " .. node.type) + end end FTL.DefaultVariant = function (ast, resource) - ast.default = true - return FTL.Variant(ast, resource) + ast.default = true + return FTL.Variant(ast, resource) end FTL.CallArguments = class(FluentNode) FTL.CallArguments._name = "CallArguments" function FTL.CallArguments:_init (ast, resource) - self.named = {} - self.positional = {} - return self:super(ast, resource) + self.named = {} + self.positional = {} + return self:super(ast, resource) end function FTL.CallArguments:__mul (node) - if node:is_a(FTL.FunctionReference) then - node.arguments = self - return node - end + if node:is_a(FTL.FunctionReference) then + node.arguments = self + return node + end end FTL.NamedArgument = class(FluentNode) @@ -454,22 +477,22 @@ FTL.Comment = class(FluentNode) FTL.Comment._name = "Comment" function FTL.Comment:_init (ast, resource) - getmetatable(self).appendable = true - return self:super(ast, resource) + getmetatable(self).appendable = true + return self:super(ast, resource) end function FTL.Comment:__add (node) - if node:is_a(self:is_a()) and node.appendable and self.appendable then - node.content = (node.content or "") .. "\n" .. (self.content or "") - return node - end + if node:is_a(self:is_a()) and node.appendable and self.appendable then + node.content = (node.content or "") .. "\n" .. (self.content or "") + return node + end end function FTL.Comment:__mul (node) - if node:is_a(FTL.Message) then - node.comment = self - return node - end + if node:is_a(FTL.Message) then + node.comment = self + return node + end end FTL.GroupComment = class(FluentNode) @@ -486,34 +509,34 @@ FTL.Attribute = class(FluentNode) FTL.Attribute._name = "Attribute" function FTL.Attribute:__mul (node) - if node:is_a(FTL.Message) then - node:set_attribute(self) - return node - elseif self:is_a(FTL.Pattern) then - node.value = self - return node - end + if node:is_a(FTL.Message) then + node:set_attribute(self) + return node + elseif self:is_a(FTL.Pattern) then + node.value = self + return node + end end function FTL.Attribute:format (parameters) - return self.value:format(parameters) + return self.value:format(parameters) end FTL.AttributeAccessor = class(FluentNode) FTL.AttributeAccessor._name = "AttributeAccessor" function FTL.AttributeAccessor:__mul (node) - if node:is_a(FTL.TermReference) then - node.attribute = self.id - return node - else error("Undefined attach "..self.type.." to "..node.type) end + if node:is_a(FTL.TermReference) then + node.attribute = self.id + return node + else + error("Undefined attach " .. self.type .. " to " .. node.type) + end end FTL.CommentLine = function (ast, resource) - ast.id = #ast.sigil == 1 and "Comment" - or #ast.sigil == 2 and "GroupComment" - or #ast.sigil == 3 and "ResourceComment" - return FTL[ast.id](ast, resource) + ast.id = #ast.sigil == 1 and "Comment" or #ast.sigil == 2 and "GroupComment" or #ast.sigil == 3 and "ResourceComment" + return FTL[ast.id](ast, resource) end return FTL diff --git a/fluent/parser.lua b/fluent/parser.lua index db3e063..2be5c9a 100644 --- a/fluent/parser.lua +++ b/fluent/parser.lua @@ -6,97 +6,99 @@ local nulleof = "NULL\000" -- UTF8 code points up to four-byte encodings local function f1 (s) - return string.byte(s) + return string.byte(s) end local function f2 (s) - local c1, c2 = string.byte(s, 1, 2) - return c1 * 64 + c2 - 12416 + local c1, c2 = string.byte(s, 1, 2) + return c1 * 64 + c2 - 12416 end local function f3 (s) - local c1, c2, c3 = string.byte(s, 1, 3) - return (c1 * 64 + c2) * 64 + c3 - 925824 + local c1, c2, c3 = string.byte(s, 1, 3) + return (c1 * 64 + c2) * 64 + c3 - 925824 end local function f4 (s) - local c1, c2, c3, c4 = string.byte(s, 1, 4) - return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 + local c1, c2, c3, c4 = string.byte(s, 1, 4) + return ((c1 * 64 + c2) * 64 + c3) * 64 + c4 - 63447168 end local cont = "\128\191" -- luacheck: push ignore +-- stylua: ignore start local ftl_eof = epnf.define(function (_ENV) - eol_eof = 1^0 * P(nulleof) * -1 - START("eol_eof") + eol_eof = 1^0 * P(nulleof) * -1 + START("eol_eof") end) local ftl_grammar = epnf.define(function (_ENV) - local blank_inline = P" "^1 - local line_end = P"\r\n" + P"\n" + P(nulleof) - blank_block = C((blank_inline^-1 * line_end)^1); local blank_block = (blank_inline^-1 * line_end)^1 - local blank = (blank_inline + line_end)^1 - local digits = R"09"^1 - local special_text_char = P"{" + P"}" - 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 - local text_char = any_char - special_text_char - line_end - local special_quoted_char = P'"' + P"\\" - local special_escape = P"\\" * special_quoted_char - local unicode_escape = (P"\\u" * P(4) * R("09", "af", "AF")^4) + (P"\\u" * P(6) * R("09", "af", "AF")^6) - local quoted_char = (any_char - special_quoted_char - line_end) + special_escape + unicode_escape - local indented_char = text_char - P"[" - P"*" - P"." - Identifier = Cg(R("az", "AZ") * (R("az", "AZ", "09") + P"_" + P"-")^0, "name") - variant_list = V"Variant"^0 * V"DefaultVariant" * V"Variant"^0 * line_end - Variant = line_end * blank^-1 * V"VariantKey" * blank_inline^-1 * V"Pattern" - DefaultVariant = line_end * blank^-1 * P"*" * V"VariantKey" * blank_inline^-1 * V"Pattern" - VariantKey = P"[" * blank^-1 * (V"NumberLiteral" + V"Identifier") * blank^-1 * P"]" - NumberLiteral = Cg(C(P"-"^-1 * digits * (P"." * digits)^-1), "value") - local inline_placeable = P"{" * blank^-1 * (V"SelectExpression" + V"InlineExpression") * blank^-1 * P"}" - local block_placeable = blank_block * blank_inline^-1 * inline_placeable - local inline_text = text_char^1 - local block_text = blank_block * blank_inline * indented_char * inline_text^-1 - StringLiteral = P'"' * Cg(C(quoted_char^0), "value") * P'"' - FunctionReference = V"Identifier" * V"CallArguments" - MessageReference = V"Identifier" * V"AttributeAccessor"^-1 - TermReference = P"-" * V"Identifier" * V"AttributeAccessor"^-1 * V"CallArguments"^-1 - _TermReference = P"-" * V"Identifier" * V"AttributeAccessor" * V"CallArguments"^-1 - VariableReference = P"$" * V"Identifier" - AttributeAccessor = P"." * V"Identifier" - NamedArgument = V"Identifier" * blank^-1 * P":" * blank^-1 * (V"StringLiteral" + V"NumberLiteral") - Argument = V"NamedArgument" + V"InlineExpression" - local argument_list = (V"Argument" * blank^-1 * P"," * blank^-1)^0 * V"Argument"^-1 - CallArguments = blank^-1 * P"(" * blank^-1 * argument_list * blank^-1 * P")" - InlineExpression = V"StringLiteral" + V"NumberLiteral" + V"FunctionReference" + V"MessageReference" + V"TermReference" + V"VariableReference" + inline_placeable - _InlineExpression = V"StringLiteral" + V"NumberLiteral" + V"FunctionReference" + V"_TermReference" + V"VariableReference" - SelectExpression = V"_InlineExpression" * blank^-1 * P"->" * blank_inline^-1 * V"variant_list" - PatternElement = Cg(C(inline_text + block_text), "value") + Cg(inline_placeable + block_placeable, "expression") - Pattern = V"PatternElement"^1 - Attribute = line_end * blank^-1 * P"." * V"Identifier" * blank_inline^-1 * "=" * blank_inline^-1 * V"Pattern" - local junk_line = (1-P"\n"-P(nulleof))^0 * (P"\n" + P(nulleof)) - Junk = Cg(junk_line * (junk_line - P"#" - P"-" - R("az","AZ"))^0, "content") - local comment_char = any_char - line_end - CommentLine = Cg(P"###" + P"##" + P"#", "sigil") * (" " * Cg(C(comment_char^0), "content"))^-1 * line_end - Term = P"-" * V"Identifier" * blank_inline^-1 * "=" * blank_inline^-1 * V"Pattern" * V"Attribute"^0 - Message = V"Identifier" * blank_inline^-1 * P"=" * blank_inline^-1 * ((V"Pattern" * V"Attribute"^0) + V"Attribute"^1) - Entry = (V"Message" * line_end) + (V"Term" * line_end) + V"CommentLine" - Resource = (V"Entry" + V"blank_block" + V"Junk")^0 * (P(nulleof) + EOF"unparsable input") - START("Resource") + local blank_inline = P" "^1 + local line_end = P"\r\n" + P"\n" + P(nulleof) + blank_block = C((blank_inline^-1 * line_end)^1); local blank_block = (blank_inline^-1 * line_end)^1 + local blank = (blank_inline + line_end)^1 + local digits = R"09"^1 + local special_text_char = P"{" + P"}" + 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 + local text_char = any_char - special_text_char - line_end + local special_quoted_char = P'"' + P"\\" + local special_escape = P"\\" * special_quoted_char + local unicode_escape = (P"\\u" * P(4) * R("09", "af", "AF")^4) + (P"\\u" * P(6) * R("09", "af", "AF")^6) + local quoted_char = (any_char - special_quoted_char - line_end) + special_escape + unicode_escape + local indented_char = text_char - P"[" - P"*" - P"." + Identifier = Cg(R("az", "AZ") * (R("az", "AZ", "09") + P"_" + P"-")^0, "name") + variant_list = V"Variant"^0 * V"DefaultVariant" * V"Variant"^0 * line_end + Variant = line_end * blank^-1 * V"VariantKey" * blank_inline^-1 * V"Pattern" + DefaultVariant = line_end * blank^-1 * P"*" * V"VariantKey" * blank_inline^-1 * V"Pattern" + VariantKey = P"[" * blank^-1 * (V"NumberLiteral" + V"Identifier") * blank^-1 * P"]" + NumberLiteral = Cg(C(P"-"^-1 * digits * (P"." * digits)^-1), "value") + local inline_placeable = P"{" * blank^-1 * (V"SelectExpression" + V"InlineExpression") * blank^-1 * P"}" + local block_placeable = blank_block * blank_inline^-1 * inline_placeable + local inline_text = text_char^1 + local block_text = blank_block * blank_inline * indented_char * inline_text^-1 + StringLiteral = P'"' * Cg(C(quoted_char^0), "value") * P'"' + FunctionReference = V"Identifier" * V"CallArguments" + MessageReference = V"Identifier" * V"AttributeAccessor"^-1 + TermReference = P"-" * V"Identifier" * V"AttributeAccessor"^-1 * V"CallArguments"^-1 + _TermReference = P"-" * V"Identifier" * V"AttributeAccessor" * V"CallArguments"^-1 + VariableReference = P"$" * V"Identifier" + AttributeAccessor = P"." * V"Identifier" + NamedArgument = V"Identifier" * blank^-1 * P":" * blank^-1 * (V"StringLiteral" + V"NumberLiteral") + Argument = V"NamedArgument" + V"InlineExpression" + local argument_list = (V"Argument" * blank^-1 * P"," * blank^-1)^0 * V"Argument"^-1 + CallArguments = blank^-1 * P"(" * blank^-1 * argument_list * blank^-1 * P")" + InlineExpression = V"StringLiteral" + V"NumberLiteral" + V"FunctionReference" + V"MessageReference" + V"TermReference" + V"VariableReference" + inline_placeable + _InlineExpression = V"StringLiteral" + V"NumberLiteral" + V"FunctionReference" + V"_TermReference" + V"VariableReference" + SelectExpression = V"_InlineExpression" * blank^-1 * P"->" * blank_inline^-1 * V"variant_list" + PatternElement = Cg(C(inline_text + block_text), "value") + Cg(inline_placeable + block_placeable, "expression") + Pattern = V"PatternElement"^1 + Attribute = line_end * blank^-1 * P"." * V"Identifier" * blank_inline^-1 * "=" * blank_inline^-1 * V"Pattern" + local junk_line = (1-P"\n"-P(nulleof))^0 * (P"\n" + P(nulleof)) + Junk = Cg(junk_line * (junk_line - P"#" - P"-" - R("az","AZ"))^0, "content") + local comment_char = any_char - line_end + CommentLine = Cg(P"###" + P"##" + P"#", "sigil") * (" " * Cg(C(comment_char^0), "content"))^-1 * line_end + Term = P"-" * V"Identifier" * blank_inline^-1 * "=" * blank_inline^-1 * V"Pattern" * V"Attribute"^0 + Message = V"Identifier" * blank_inline^-1 * P"=" * blank_inline^-1 * ((V"Pattern" * V"Attribute"^0) + V"Attribute"^1) + Entry = (V"Message" * line_end) + (V"Term" * line_end) + V"CommentLine" + Resource = (V"Entry" + V"blank_block" + V"Junk")^0 * (P(nulleof) + EOF"unparsable input") + START("Resource") end) -- luacheck: pop +-- stylua: ignore end local function addtrailingnewine (input) - local hasnulleof = epnf.parsestring(ftl_eof, input) - return type(hasnulleof) == "nil" and input .. nulleof or input + local hasnulleof = epnf.parsestring(ftl_eof, input) + return type(hasnulleof) == "nil" and input .. nulleof or input end -- TODO: if this doesn't need any state information make in a function not a class local FluentParser = class() function FluentParser:_init (input) - return type(input) == "string" and self:parsestring(input) or error("unknown input type") + return type(input) == "string" and self:parsestring(input) or error("unknown input type") end -- luacheck: ignore 212 function FluentParser:parsestring (input) - input = addtrailingnewine(input) - return epnf.parsestring(ftl_grammar, input) + input = addtrailingnewine(input) + return epnf.parsestring(ftl_grammar, input) end return FluentParser diff --git a/fluent/resource.lua b/fluent/resource.lua index 51fd69e..b1ababe 100644 --- a/fluent/resource.lua +++ b/fluent/resource.lua @@ -9,111 +9,119 @@ FluentResource._name = "FluentResource" FluentResource.type = "Resource" function FluentResource:_init (ast) - ast = ast or {} - self.body = setmetatable({}, { - map = {} - }) - local _stash - local flush = function () - if _stash then - self:load_node(_stash) - _stash = nil - end - end - local stash = function (node) - if not _stash then - _stash = node - elseif not _stash:append(node) then - flush() - _stash = node - end - end - for _, leaf in ipairs(ast) do - local node = FluentMessages[leaf.id](leaf, self) - if node:is_a(FluentMessages.blank_block) then - if not node.discardable then - flush() + ast = ast or {} + self.body = setmetatable({}, { + map = {}, + }) + local _stash + local flush = function () + if _stash then + self:load_node(_stash) + _stash = nil end - elseif node:attach(_stash) then - _stash = nil - stash(node) - else - stash(node) - end - end - flush() - -- Work around Penlight #307 - -- self:catch(self.get_message) - self:_patch_init() - return self + end + local stash = function (node) + if not _stash then + _stash = node + elseif not _stash:append(node) then + flush() + _stash = node + end + end + for _, leaf in ipairs(ast) do + local node = FluentMessages[leaf.id](leaf, self) + if node:is_a(FluentMessages.blank_block) then + if not node.discardable then + flush() + end + elseif node:attach(_stash) then + _stash = nil + stash(node) + else + stash(node) + end + end + flush() + -- Work around Penlight #307 + -- self:catch(self.get_message) + self:_patch_init() + return self end -- Work around Penlight #307 function FluentResource:_patch_init () - if type(rawget(getmetatable(self), "__index")) ~= "function" then - self:catch(function(_, identifier) return self:get_message(identifier) end) - end + if type(rawget(getmetatable(self), "__index")) ~= "function" then + self:catch(function (_, identifier) + return self:get_message(identifier) + end) + end end function FluentResource:load_node (node) - self:_patch_init() - local body = self.body - local k = #body + 1 - body[k] = node - local id_name = node.id and node.id.name or nil - if id_name then - local map = getmetatable(body).map - if node:is_a(FluentMessages.Message) and node.type == "Term" then - id_name = "-" .. id_name - end - map[id_name] = k - -- TODO Fix with working catch? - -- getmetatable(self)[id_name] = node - end + self:_patch_init() + local body = self.body + local k = #body + 1 + body[k] = node + local id_name = node.id and node.id.name or nil + if id_name then + local map = getmetatable(body).map + if node:is_a(FluentMessages.Message) and node.type == "Term" then + id_name = "-" .. id_name + end + map[id_name] = k + -- TODO Fix with working catch? + -- getmetatable(self)[id_name] = node + end end function FluentResource:get_message (identifier, isterm) - -- local raw = rawget(self, identifier) - -- if raw then return raw end - -- if not rawget(self, "id") then - -- error("Penlight bug, please use get_message() rather than table index syntax for now") - -- end - local id = string.match(identifier, "^(%a[-_%a%d]+)") - if not id then return end - local name = (isterm and "-" or "") .. id - local body = self.body - local map = getmetatable(body).map - local k = map[name] - local entry = body[k] - if not entry then return end - local attribute = string.match(identifier, "%.([(%a[-_%a%d]+)$") - return attribute and entry:get_attribute(attribute) or entry + -- local raw = rawget(self, identifier) + -- if raw then return raw end + -- if not rawget(self, "id") then + -- error("Penlight bug, please use get_message() rather than table index syntax for now") + -- end + local id = string.match(identifier, "^(%a[-_%a%d]+)") + if not id then + return + end + local name = (isterm and "-" or "") .. id + local body = self.body + local map = getmetatable(body).map + local k = map[name] + local entry = body[k] + if not entry then + return + end + local attribute = string.match(identifier, "%.([(%a[-_%a%d]+)$") + return attribute and entry:get_attribute(attribute) or entry end function FluentResource:format (identifier, parameters) - local message = self:get_message(identifier) - return message:format(parameters) + local message = self:get_message(identifier) + return message:format(parameters) end function FluentResource:get_term (identifier) - return self:get_message(identifier, true) + return self:get_message(identifier, true) end function FluentResource:dump_ast () - local ast = { type = "Resource", body = {} } - for _, node in ipairs(self.body) do - table.insert(ast.body, node:dump_ast()) - end - return ast + local ast = { type = "Resource", body = {} } + for _, node in ipairs(self.body) do + table.insert(ast.body, node:dump_ast()) + end + return ast end function FluentResource:__add (other) - if not self:is_a(other:is_a()) then error("Cannot merge unlike types") end - for _, node in ipairs(other.body) do - node:set_parent(self) - self:load_node(node) - end - return self + if not self:is_a(other:is_a()) then + error("Cannot merge unlike types") + end + for _, node in ipairs(other.body) do + node:set_parent(self) + self:load_node(node) + end + return self end return FluentResource diff --git a/fluent/syntax.lua b/fluent/syntax.lua index 43e1f34..c226e51 100644 --- a/fluent/syntax.lua +++ b/fluent/syntax.lua @@ -10,33 +10,33 @@ local FluentSyntax = class() -- luacheck: ignore 212 function FluentSyntax:_init (input) - -- TODO: handle file pointers, filnames, tables of pointers? - return self + -- TODO: handle file pointers, filnames, tables of pointers? + return self end function FluentSyntax:parsestring (input) - if not self or type(self) ~= "table" then - error("FluentSyntax.parse error: must be invoked as a method") - elseif not input or type(input) ~= "string" then - error("FluentSyntax.parse error: input must be a string") - end - local ast = FluentParser(input) - return FluentResource(ast) + if not self or type(self) ~= "table" then + error("FluentSyntax.parse error: must be invoked as a method") + elseif not input or type(input) ~= "string" then + error("FluentSyntax.parse error: input must be a string") + end + local ast = FluentParser(input) + return FluentResource(ast) end function FluentSyntax:parsefile (fname) - -- TODO: check if filename or file handle - if not self or type(self) ~= "table" then - error("FluentSyntax.parse error: must be invoked as a method") - elseif not fname or type(fname) ~= "string" then - error("FluentSyntax.parse error: fname must be a string") - end - -- TODO: add loader that leverages epnf.parsefile() - local f = assert(io.open(fname, "rb")) - local content = f:read("*all") - f:close() - local ast = FluentParser(content) - return FluentResource(ast) + -- TODO: check if filename or file handle + if not self or type(self) ~= "table" then + error("FluentSyntax.parse error: must be invoked as a method") + elseif not fname or type(fname) ~= "string" then + error("FluentSyntax.parse error: fname must be a string") + end + -- TODO: add loader that leverages epnf.parsefile() + local f = assert(io.open(fname, "rb")) + local content = f:read("*all") + f:close() + local ast = FluentParser(content) + return FluentResource(ast) end return FluentSyntax diff --git a/rockspecs/fluent-0.2.0-0.rockspec b/rockspecs/fluent-0.2.0-0.rockspec new file mode 100644 index 0000000..621015f --- /dev/null +++ b/rockspecs/fluent-0.2.0-0.rockspec @@ -0,0 +1,35 @@ +rockspec_format = "1.0" +package = "fluent" +version = "0.2.0-0" + +source = { + url = "git+https://github.com/alerque/fluent-lua.git", + dir = "fluent-lua", + tag = "v0.2.0" +} + +description = { + summary = "Lua implementation of Project Fluent", + detailed = [[A Lua port of Project Fluent, a localization paradigm designed to unleash + the entire expressive power of natural language translations.]], + license = "MIT", + homepage = "https://github.com/alerque/fluent-lua", +} + +dependencies = { + "lua >= 5.1", + "cldr", + "luaepnf", + "penlight" +} + +build = { + type = "builtin", + modules = { + ["fluent.init"] = "fluent/init.lua", + ["fluent.messages"] = "fluent/messages.lua", + ["fluent.parser"] = "fluent/parser.lua", + ["fluent.resource"] = "fluent/resource.lua", + ["fluent.syntax"] = "fluent/syntax.lua" + } +} diff --git a/spec/fixtures_spec.lua b/spec/fixtures_spec.lua index 563de9d..34fcab7 100644 --- a/spec/fixtures_spec.lua +++ b/spec/fixtures_spec.lua @@ -6,189 +6,199 @@ local json = require("dkjson") local FluentSyntax = require("fluent.syntax") local function filetostring (fname) - local f = assert(io.open(fname, "rb")) - local content = f:read("*all") - f:close() - return content + local f = assert(io.open(fname, "rb")) + local content = f:read("*all") + f:close() + return content end -describe('upstream reference fixture', function () - local syntax = FluentSyntax() - - local fixtures_dir = lfs.currentdir() .. "/spec/fixtures/" - for object in lfs.dir(fixtures_dir) do - local fname = fixtures_dir .. object - if fname:match(".ftl$") and ( - false - or fname:match("/any_char.ftl$") - -- or fname:match("/astral.ftl$") - -- or fname:match("/callee_expressions.ftl$") - -- or fname:match("/call_expressions.ftl$") - or fname:match("/comments.ftl$") - or fname:match("/cr.ftl$") - or fname:match("/crlf.ftl$") - or fname:match("/eof_comment.ftl$") - or fname:match("/eof_empty.ftl$") - -- or fname:match("/eof_id_equals.ftl$") - -- or fname:match("/eof_id.ftl$") - -- or fname:match("/eof_junk.ftl$") - or fname:match("/eof_value.ftl$") - -- or fname:match("/escaped_characters.ftl$") - or fname:match("/junk.ftl$") - -- or fname:match("/leading_dots.ftl$") - or fname:match("/literal_expressions.ftl$") - -- or fname:match("/member_expressions.ftl$") - or fname:match("/messages.ftl$") - or fname:match("/mixed_entries.ftl$") - -- or fname:match("/multiline_values.ftl$") - or fname:match("/numbers.ftl$") - or fname:match("/obsolete.ftl$") - or fname:match("/placeables.ftl$") - or fname:match("/reference_expressions.ftl$") - or fname:match("/select_expressions.ftl$") - or fname:match("/select_indent.ftl$") - -- or fname:match("/sparse_entries.ftl$") - or fname:match("/tab.ftl$") - -- or fname:match("/term_parameters.ftl$") - or fname:match("/terms.ftl$") - or fname:match("/variables.ftl$") - or fname:match("/variant_keys.ftl$") - or fname:match("/whitespace_in_value.ftl$") - or fname:match("/zero_length.ftl$") - ) then - describe(object, function () - local ftl = filetostring(fname) - local jsonref = filetostring(fname:gsub(".ftl$", ".json")) - local reference = json.decode(jsonref) - - local resource - - it('test should have a reference', function() - assert.equal("table", type(reference)) - end) - - it('should parse from string without blowing up', function () - assert.no.error(function () resource = syntax:parsestring(ftl) end) - end) - - it('should parse from file pointer without blowing up', function () - assert.no.error(function () resource = syntax:parsefile(fname) end) - end) - - it('should have a Resource as the AST root', function () - assert.equal("Resource", resource.type) - end) - - it('should match the referece result', function () - assert.same(reference, resource:dump_ast()) - end) - - end) - end - end - +describe("upstream reference fixture", function () + local syntax = FluentSyntax() + + local fixtures_dir = lfs.currentdir() .. "/spec/fixtures/" + for object in lfs.dir(fixtures_dir) do + local fname = fixtures_dir .. object + if + fname:match(".ftl$") + and ( + false + or fname:match("/any_char.ftl$") + -- or fname:match("/astral.ftl$") + -- or fname:match("/callee_expressions.ftl$") + -- or fname:match("/call_expressions.ftl$") + or fname:match("/comments.ftl$") + or fname:match("/cr.ftl$") + or fname:match("/crlf.ftl$") + or fname:match("/eof_comment.ftl$") + or fname:match("/eof_empty.ftl$") + -- or fname:match("/eof_id_equals.ftl$") + -- or fname:match("/eof_id.ftl$") + -- or fname:match("/eof_junk.ftl$") + or fname:match("/eof_value.ftl$") + -- or fname:match("/escaped_characters.ftl$") + or fname:match("/junk.ftl$") + -- or fname:match("/leading_dots.ftl$") + or fname:match("/literal_expressions.ftl$") + -- or fname:match("/member_expressions.ftl$") + or fname:match("/messages.ftl$") + or fname:match("/mixed_entries.ftl$") + -- or fname:match("/multiline_values.ftl$") + or fname:match("/numbers.ftl$") + or fname:match("/obsolete.ftl$") + or fname:match("/placeables.ftl$") + or fname:match("/reference_expressions.ftl$") + or fname:match("/select_expressions.ftl$") + or fname:match("/select_indent.ftl$") + -- or fname:match("/sparse_entries.ftl$") + or fname:match("/tab.ftl$") + -- or fname:match("/term_parameters.ftl$") + or fname:match("/terms.ftl$") + or fname:match("/variables.ftl$") + or fname:match("/variant_keys.ftl$") + or fname:match("/whitespace_in_value.ftl$") + or fname:match("/zero_length.ftl$") + ) + then + describe(object, function () + local ftl = filetostring(fname) + local jsonref = filetostring(fname:gsub(".ftl$", ".json")) + local reference = json.decode(jsonref) + + local resource + + it("test should have a reference", function () + assert.equal("table", type(reference)) + end) + + it("should parse from string without blowing up", function () + assert.no.error(function () + resource = syntax:parsestring(ftl) + end) + end) + + it("should parse from file pointer without blowing up", function () + assert.no.error(function () + resource = syntax:parsefile(fname) + end) + end) + + it("should have a Resource as the AST root", function () + assert.equal("Resource", resource.type) + end) + + it("should match the referece result", function () + assert.same(reference, resource:dump_ast()) + end) + end) + end + end end) -describe('upstream structure fixture', function () - local syntax = FluentSyntax() - - local fixtures_dir = lfs.currentdir() .. "/spec/structure/" - for object in lfs.dir(fixtures_dir) do - local fname = fixtures_dir .. object - if fname:match(".ftl$") and ( - false - or fname:match("/attribute_expression_with_wrong_attr.ftl") - -- or fname:match("/attribute_of_private_as_placeable.ftl") - or fname:match("/attribute_of_public_as_selector.ftl") - or fname:match("/attribute_starts_from_nl.ftl") - -- or fname:match("/attribute_with_empty_pattern.ftl") - -- or fname:match("/attribute_without_equal_sign.ftl") - -- or fname:match("/blank_lines.ftl") - or fname:match("/broken_number.ftl") - -- or fname:match("/call_expression_errors.ftl") - or fname:match("/comment_with_eof.ftl") - or fname:match("/crlf.ftl") - -- or fname:match("/dash_at_eof.ftl") - or fname:match("/elements_indent.ftl") - or fname:match("/empty_resource.ftl") - or fname:match("/empty_resource_with_ws.ftl") - -- or fname:match("/escape_sequences.ftl") - -- or fname:match("/expressions_call_args.ftl") - -- or fname:match("/indent.ftl") - or fname:match("/junk.ftl") - -- or fname:match("/leading_dots.ftl") - or fname:match("/leading_empty_lines.ftl") - or fname:match("/leading_empty_lines_with_ws.ftl") - or fname:match("/message_reference_as_selector.ftl") - -- or fname:match("/message_with_empty_multiline_pattern.ftl") - -- or fname:match("/message_with_empty_pattern.ftl") - or fname:match("/multiline-comment.ftl") - or fname:match("/multiline_pattern.ftl") - or fname:match("/multiline_string.ftl") - or fname:match("/multiline_with_non_empty_first_line.ftl") - -- or fname:match("/multiline_with_placeables.ftl") - -- or fname:match("/non_id_attribute_name.ftl") - -- or fname:match("/placeable_at_eol.ftl") - or fname:match("/placeable_at_line_extremes.ftl") - -- or fname:match("/placeable_in_placeable.ftl") - or fname:match("/placeable_without_close_bracket.ftl") - or fname:match("/resource_comment.ftl") - or fname:match("/resource_comment_trailing_line.ftl") - or fname:match("/second_attribute_starts_from_nl.ftl") - or fname:match("/select_expressions.ftl") - or fname:match("/select_expression_without_arrow.ftl") - or fname:match("/select_expression_without_variants.ftl") - or fname:match("/select_expression_with_two_selectors.ftl") - or fname:match("/simple_message.ftl") - or fname:match("/single_char_id.ftl") - or fname:match("/sparse-messages.ftl") - or fname:match("/standalone_comment.ftl") - or fname:match("/standalone_identifier.ftl") - -- or fname:match("/term.ftl") - or fname:match("/term_with_empty_pattern.ftl") - or fname:match("/unclosed_empty_placeable_error.ftl") - -- -- or fname:match("/unclosed.ftl") -- see https://github.com/projectfluent/fluent/issues/296 - or fname:match("/unknown_entry_start.ftl") - or fname:match("/variant_ends_abruptly.ftl") - or fname:match("/variant_keys.ftl") - or fname:match("/variant_starts_from_nl.ftl") - or fname:match("/variants_with_two_defaults.ftl") - or fname:match("/variant_with_digit_key.ftl") - or fname:match("/variant_with_empty_pattern.ftl") - or fname:match("/variant_with_leading_space_in_name.ftl") - or fname:match("/variant_with_symbol_with_space.ftl") - or fname:match("/whitespace_leading.ftl") - or fname:match("/whitespace_trailing.ftl") - ) then - describe(object, function () - local ftl = filetostring(fname) - local jsonref = filetostring(fname:gsub(".ftl$", ".json")) - local reference = json.decode(jsonref) - - local resource - - it('should have a reference', function() - assert.equal("table", type(reference)) - end) - - it('should parse from string without blowing up', function () - assert.no.error(function () resource = syntax:parsestring(ftl) end) - end) - - it('should parse from file pointer without blowing up', function () - assert.no.error(function () resource = syntax:parsefile(fname) end) - end) - - it('should have a Resource as the AST root', function () - assert.equal("Resource", resource.type) - end) - - it('should match the referece result', function () - assert.same(reference, resource:dump_ast()) - end) - - end) - end - end - +describe("upstream structure fixture", function () + local syntax = FluentSyntax() + + local fixtures_dir = lfs.currentdir() .. "/spec/structure/" + for object in lfs.dir(fixtures_dir) do + local fname = fixtures_dir .. object + if + fname:match(".ftl$") + and ( + false + or fname:match("/attribute_expression_with_wrong_attr.ftl") + -- or fname:match("/attribute_of_private_as_placeable.ftl") + or fname:match("/attribute_of_public_as_selector.ftl") + or fname:match("/attribute_starts_from_nl.ftl") + -- or fname:match("/attribute_with_empty_pattern.ftl") + -- or fname:match("/attribute_without_equal_sign.ftl") + -- or fname:match("/blank_lines.ftl") + or fname:match("/broken_number.ftl") + -- or fname:match("/call_expression_errors.ftl") + or fname:match("/comment_with_eof.ftl") + or fname:match("/crlf.ftl") + -- or fname:match("/dash_at_eof.ftl") + or fname:match("/elements_indent.ftl") + or fname:match("/empty_resource.ftl") + or fname:match("/empty_resource_with_ws.ftl") + -- or fname:match("/escape_sequences.ftl") + -- or fname:match("/expressions_call_args.ftl") + -- or fname:match("/indent.ftl") + or fname:match("/junk.ftl") + -- or fname:match("/leading_dots.ftl") + or fname:match("/leading_empty_lines.ftl") + or fname:match("/leading_empty_lines_with_ws.ftl") + or fname:match("/message_reference_as_selector.ftl") + -- or fname:match("/message_with_empty_multiline_pattern.ftl") + -- or fname:match("/message_with_empty_pattern.ftl") + or fname:match("/multiline-comment.ftl") + or fname:match("/multiline_pattern.ftl") + or fname:match("/multiline_string.ftl") + or fname:match("/multiline_with_non_empty_first_line.ftl") + -- or fname:match("/multiline_with_placeables.ftl") + -- or fname:match("/non_id_attribute_name.ftl") + -- or fname:match("/placeable_at_eol.ftl") + or fname:match("/placeable_at_line_extremes.ftl") + -- or fname:match("/placeable_in_placeable.ftl") + or fname:match("/placeable_without_close_bracket.ftl") + or fname:match("/resource_comment.ftl") + or fname:match("/resource_comment_trailing_line.ftl") + or fname:match("/second_attribute_starts_from_nl.ftl") + or fname:match("/select_expressions.ftl") + or fname:match("/select_expression_without_arrow.ftl") + or fname:match("/select_expression_without_variants.ftl") + or fname:match("/select_expression_with_two_selectors.ftl") + or fname:match("/simple_message.ftl") + or fname:match("/single_char_id.ftl") + or fname:match("/sparse-messages.ftl") + or fname:match("/standalone_comment.ftl") + or fname:match("/standalone_identifier.ftl") + -- or fname:match("/term.ftl") + or fname:match("/term_with_empty_pattern.ftl") + or fname:match("/unclosed_empty_placeable_error.ftl") + -- -- or fname:match("/unclosed.ftl") -- see https://github.com/projectfluent/fluent/issues/296 + or fname:match("/unknown_entry_start.ftl") + or fname:match("/variant_ends_abruptly.ftl") + or fname:match("/variant_keys.ftl") + or fname:match("/variant_starts_from_nl.ftl") + or fname:match("/variants_with_two_defaults.ftl") + or fname:match("/variant_with_digit_key.ftl") + or fname:match("/variant_with_empty_pattern.ftl") + or fname:match("/variant_with_leading_space_in_name.ftl") + or fname:match("/variant_with_symbol_with_space.ftl") + or fname:match("/whitespace_leading.ftl") + or fname:match("/whitespace_trailing.ftl") + ) + then + describe(object, function () + local ftl = filetostring(fname) + local jsonref = filetostring(fname:gsub(".ftl$", ".json")) + local reference = json.decode(jsonref) + + local resource + + it("should have a reference", function () + assert.equal("table", type(reference)) + end) + + it("should parse from string without blowing up", function () + assert.no.error(function () + resource = syntax:parsestring(ftl) + end) + end) + + it("should parse from file pointer without blowing up", function () + assert.no.error(function () + resource = syntax:parsefile(fname) + end) + end) + + it("should have a Resource as the AST root", function () + assert.equal("Resource", resource.type) + end) + + it("should match the referece result", function () + assert.same(reference, resource:dump_ast()) + end) + end) + end + end end) diff --git a/spec/fluent_spec.lua b/spec/fluent_spec.lua index 12a2fbd..5c7aafe 100644 --- a/spec/fluent_spec.lua +++ b/spec/fluent_spec.lua @@ -1,70 +1,75 @@ -- Internal modules local FluentBundle = require("fluent") -describe('fluent.bundle', function () +describe("fluent.bundle", function () + it("should instantiate without any locale", function () + local bundle = FluentBundle() + assert.is_true(bundle:is_a(FluentBundle)) + assert.same("und", bundle.locale) + end) - it('should instantiate without any locale', function () - local bundle = FluentBundle() - assert.is_true(bundle:is_a(FluentBundle)) - assert.same("und", bundle.locale) - end) + it("should instantiate with a locale", function () + local locale = "en" + local bundle = FluentBundle(locale) + assert.is_true(bundle:is_a(FluentBundle)) + assert.same(locale, bundle.locale) + end) - it('should instantiate with a locale', function () - local locale = "en" - local bundle = FluentBundle(locale) - assert.is_true(bundle:is_a(FluentBundle)) - assert.same(locale, bundle.locale) - end) + it("should parse and format messages loaded from a file", function () + local bundle = FluentBundle("en") + bundle:load_file("spec/spec.ftl") + assert.same("bar", bundle:format("foo")) + end) - it('should parse and format single simple messages', function () - local bundle = FluentBundle("en") - bundle:add_messages("foo = bar") - assert.same("bar", bundle:format("foo")) - end) + it("should parse and format single simple messages", function () + local bundle = FluentBundle("en") + bundle:add_messages("foo = bar") + assert.same("bar", bundle:format("foo")) + end) - it('should parse and format multiple simple messages', function () - local bundle = FluentBundle("en") - bundle:add_messages("foo = bar\nbar = baz") - assert.same("bar", bundle:format("foo")) - assert.same("baz", bundle:format("bar")) - end) + it("should parse and format multiple simple messages", function () + local bundle = FluentBundle("en") + bundle:add_messages("foo = bar\nbar = baz") + assert.same("bar", bundle:format("foo")) + assert.same("baz", bundle:format("bar")) + end) - it('should parse and format a table of strings', function () - local bundle = FluentBundle("en") - bundle:add_messages({ "foo = bar", "bar = baz" }) - assert.same("bar", bundle:format("foo")) - assert.same("baz", bundle:format("bar")) - end) + it("should parse and format a table of strings", function () + local bundle = FluentBundle("en") + bundle:add_messages({ "foo = bar", "bar = baz" }) + assert.same("bar", bundle:format("foo")) + assert.same("baz", bundle:format("bar")) + end) - it('should parse and format literals', function () - local bundle = FluentBundle("en") - bundle:add_messages('foo = bar {"baz"} quz {-3.14}') - assert.same("bar baz quz -3.14", bundle:format("foo")) - end) + it("should parse and format literals", function () + local bundle = FluentBundle("en") + bundle:add_messages('foo = bar {"baz"} quz {-3.14}') + assert.same("bar baz quz -3.14", bundle:format("foo")) + end) - it('should parse and format a variable substitution', function () - local bundle = FluentBundle("en") - bundle:add_messages('foo = bar { $baz }') - assert.same("bar qux", bundle:format("foo", { baz = "qux" })) - end) + it("should parse and format a variable substitution", function () + local bundle = FluentBundle("en") + bundle:add_messages("foo = bar { $baz }") + assert.same("bar qux", bundle:format("foo", { baz = "qux" })) + end) - it('should parse and format a message reference', function () - local bundle = FluentBundle("en") - bundle:add_messages('foo = bar\nbaz = a { foo }') - assert.same("bar", bundle:format("foo")) - assert.same("a bar", bundle:format("baz")) - end) + it("should parse and format a message reference", function () + local bundle = FluentBundle("en") + bundle:add_messages("foo = bar\nbaz = a { foo }") + assert.same("bar", bundle:format("foo")) + assert.same("a bar", bundle:format("baz")) + end) - it('should parse and format a term reference', function () - local bundle = FluentBundle("en") - bundle:add_messages('-foo = bar\nfoo = public\nbaz = a { -foo }') - assert.same("public", bundle:format("foo")) - assert.same("a bar", bundle:format("baz")) - end) + it("should parse and format a term reference", function () + local bundle = FluentBundle("en") + bundle:add_messages("-foo = bar\nfoo = public\nbaz = a { -foo }") + assert.same("public", bundle:format("foo")) + assert.same("a bar", bundle:format("baz")) + end) - it('should parse and format an attribute', function () - local bundle = FluentBundle("en") - bundle:add_messages([[ + it("should parse and format an attribute", function () + local bundle = FluentBundle("en") + bundle:add_messages([[ foo = { $ab -> *[a] bar @@ -72,16 +77,16 @@ foo = [c] qiz } ]]) - assert.same("bar", bundle:format("foo")) - assert.same("bar", bundle:format("foo", { ab = "a" })) - assert.same("baz", bundle:format("foo", { ab = "b" })) - assert.same("qiz", bundle:format("foo", { ab = "c" })) - assert.same("bar", bundle:format("foo", { ab = "d" })) - end) + assert.same("bar", bundle:format("foo")) + assert.same("bar", bundle:format("foo", { ab = "a" })) + assert.same("baz", bundle:format("foo", { ab = "b" })) + assert.same("qiz", bundle:format("foo", { ab = "c" })) + assert.same("bar", bundle:format("foo", { ab = "d" })) + end) - it('should parse and format an attribute based on numbers', function () - local bundle = FluentBundle("en") - bundle:add_messages([[ + it("should parse and format an attribute based on numbers", function () + local bundle = FluentBundle("en") + bundle:add_messages([[ foo = { $num -> *[0] no bar @@ -89,172 +94,188 @@ foo = [other] {$num} bars } ]]) - assert.same("no bar", bundle:format("foo")) - assert.same("one bar", bundle:format("foo", { num = 1 })) - assert.same("2 bars", bundle:format("foo", { num = 2 })) - assert.same("37 bars", bundle:format("foo", { num = 37 })) - end) + assert.same("no bar", bundle:format("foo")) + assert.same("one bar", bundle:format("foo", { num = 1 })) + assert.same("2 bars", bundle:format("foo", { num = 2 })) + assert.same("37 bars", bundle:format("foo", { num = 37 })) + end) - describe('messages', function () - local bundle, en + describe("messages", function () + local bundle, en - before_each(function () - bundle = FluentBundle("en") - en = bundle:get_resource() - bundle:add_messages("hi = Hello { $name }!\nfoo = bar\nbar = baz\n .bax = qux") - end) + before_each(function () + bundle = FluentBundle("en") + en = bundle:get_resource() + bundle:add_messages("hi = Hello { $name }!\nfoo = bar\nbar = baz\n .bax = qux") + end) - after_each(function () - bundle, en = nil, nil - end) + after_each(function () + bundle, en = nil, nil + end) - it('can be accessed with getter', function () - assert.same("bar", bundle:get_message("foo"):format()) - assert.same("bar", tostring(bundle:get_message("foo"))) - assert.same("baz", bundle:get_message("bar"):format()) - assert.same("baz", tostring(bundle:get_message("bar"))) - end) + it("can be accessed with getter", function () + assert.same("bar", bundle:get_message("foo"):format()) + assert.same("bar", tostring(bundle:get_message("foo"))) + assert.same("baz", bundle:get_message("bar"):format()) + assert.same("baz", tostring(bundle:get_message("bar"))) + end) - it('can be accessed as properties', function () - assert.same("bar", bundle.foo:format()) - assert.same("bar", en.foo:format()) - assert.same("bar", bundle["foo"]:format()) - assert.same("baz", bundle.bar:format()) - assert.same("baz", en.bar:format()) - assert.same("baz", bundle["bar"]:format()) - assert.error(function () return bundle.apple:format() end) - assert.error(function () return en.apple:format() end) - assert.error(function () return bundle["apple"]:format() end) - end) + it("can be accessed as properties", function () + assert.same("bar", bundle.foo:format()) + assert.same("bar", en.foo:format()) + assert.same("bar", bundle["foo"]:format()) + assert.same("baz", bundle.bar:format()) + assert.same("baz", en.bar:format()) + assert.same("baz", bundle["bar"]:format()) + assert.error(function () + return bundle.apple:format() + end) + assert.error(function () + return en.apple:format() + end) + assert.error(function () + return bundle["apple"]:format() + end) + end) - it('attributes can be accessed as properties', function () - assert.same("qux", bundle["bar.bax"]()) - assert.same("qux", en["bar.bax"]()) - assert.same("qux", bundle.bar["bax"]()) - assert.same("qux", en.bar["bax"]()) - assert.same("qux", bundle.bar.bax()) - assert.same("qux", en.bar.bax()) - end) + it("attributes can be accessed as properties", function () + assert.same("qux", bundle["bar.bax"]()) + assert.same("qux", en["bar.bax"]()) + assert.same("qux", bundle.bar["bax"]()) + assert.same("qux", en.bar["bax"]()) + assert.same("qux", bundle.bar.bax()) + assert.same("qux", en.bar.bax()) + end) - it('preserves messages when messages are added', function () - assert.same("baz", bundle:format("bar")) - assert.error(function () return bundle:format("aa") end) - bundle:add_messages("aa = bb") - assert.same("baz", bundle:format("bar")) - assert.same("bb", bundle:format("aa")) - end) + it("preserves messages when messages are added", function () + assert.same("baz", bundle:format("bar")) + assert.error(function () + return bundle:format("aa") + end) + bundle:add_messages("aa = bb") + assert.same("baz", bundle:format("bar")) + assert.same("bb", bundle:format("aa")) + end) - it('updates messages when messages are readded', function () - assert.same("bar", bundle.foo()) - assert.same("bar", bundle:format("foo")) - assert.same("baz", bundle.bar()) - assert.same("baz", bundle:format("bar")) - bundle:add_messages("bar = rebar") - assert.same("bar", bundle.foo()) - assert.same("bar", bundle:format("foo")) - assert.same("rebar", bundle.bar()) - assert.same("rebar", bundle:format("bar")) - end) + it("updates messages when messages are readded", function () + assert.same("bar", bundle.foo()) + assert.same("bar", bundle:format("foo")) + assert.same("baz", bundle.bar()) + assert.same("baz", bundle:format("bar")) + bundle:add_messages("bar = rebar") + assert.same("bar", bundle.foo()) + assert.same("bar", bundle:format("foo")) + assert.same("rebar", bundle.bar()) + assert.same("rebar", bundle:format("bar")) + end) - it('preserves attributes when messages are added', function () - assert.same("baz", bundle.bar()) - assert.same("baz", en.bar()) - assert.same("baz", bundle:format("bar")) - assert.same("baz", en:format("bar")) - assert.same("qux", bundle:get_message("bar"):get_attribute("bax")()) - assert.same("qux", en:get_message("bar"):get_attribute("bax")()) - assert.same("qux", bundle["bar.bax"]()) - assert.same("qux", en["bar.bax"]()) - assert.same("qux", bundle.bar.bax()) - assert.same("qux", en.bar.bax()) - bundle:add_messages("aa = bb") - assert.same("baz", bundle.bar()) - assert.same("baz", en.bar()) - assert.same("baz", bundle:format("bar")) - assert.same("baz", en:format("bar")) - assert.same("qux", bundle:get_message("bar"):get_attribute("bax")()) - assert.same("qux", en:get_message("bar"):get_attribute("bax")()) - assert.same("qux", bundle["bar.bax"]()) - assert.same("qux", en["bar.bax"]()) - -- TODO Penlight hack doesn't work around this? - -- assert.same("qux", bundle.bar.bax()) - -- assert.same("qux", en.bar.bax()) - bundle:add_messages("bar = rebar") - assert.same("rebar", bundle.bar()) - assert.same("rebar", en.bar()) - assert.same("rebar", bundle:format("bar")) - assert.same("rebar", en:format("bar")) - assert.error(function() return bundle:get_message("bar"):get_attribute("bax")() end) - assert.error(function() return en:get_message("bar"):get_attribute("bax")() end) - assert.error(function() return bundle.bar.bax() end) - assert.error(function() return en.bar.bax() end) - end) - - it('can be called', function () - assert.same("bar", bundle.foo()) - assert.same("bar", en.foo()) - assert.same("bar", bundle["foo"]()) - assert.same("bar", en["foo"]()) - assert.same("baz", bundle.bar()) - assert.same("baz", en.bar()) - assert.same("baz", bundle["bar"]()) - assert.same("baz", en["bar"]()) - end) + it("preserves attributes when messages are added", function () + assert.same("baz", bundle.bar()) + assert.same("baz", en.bar()) + assert.same("baz", bundle:format("bar")) + assert.same("baz", en:format("bar")) + assert.same("qux", bundle:get_message("bar"):get_attribute("bax")()) + assert.same("qux", en:get_message("bar"):get_attribute("bax")()) + assert.same("qux", bundle["bar.bax"]()) + assert.same("qux", en["bar.bax"]()) + assert.same("qux", bundle.bar.bax()) + assert.same("qux", en.bar.bax()) + bundle:add_messages("aa = bb") + assert.same("baz", bundle.bar()) + assert.same("baz", en.bar()) + assert.same("baz", bundle:format("bar")) + assert.same("baz", en:format("bar")) + assert.same("qux", bundle:get_message("bar"):get_attribute("bax")()) + assert.same("qux", en:get_message("bar"):get_attribute("bax")()) + assert.same("qux", bundle["bar.bax"]()) + assert.same("qux", en["bar.bax"]()) + -- TODO Penlight hack doesn't work around this? + -- assert.same("qux", bundle.bar.bax()) + -- assert.same("qux", en.bar.bax()) + bundle:add_messages("bar = rebar") + assert.same("rebar", bundle.bar()) + assert.same("rebar", en.bar()) + assert.same("rebar", bundle:format("bar")) + assert.same("rebar", en:format("bar")) + assert.error(function () + return bundle:get_message("bar"):get_attribute("bax")() + end) + assert.error(function () + return en:get_message("bar"):get_attribute("bax")() + end) + assert.error(function () + return bundle.bar.bax() + end) + assert.error(function () + return en.bar.bax() + end) + end) - it('can be called with parameters', function () - assert.same("Hello World!", bundle.hi({name = "World"})) - assert.same("Hello World!", en.hi({name = "World"})) - assert.same("Hello World!", bundle["hi"]({name = "World"})) - assert.same("Hello World!", en["hi"]({name = "World"})) - assert.same("Hello World!", bundle["hi"]:format({name = "World"})) - assert.same("Hello World!", en["hi"]:format({name = "World"})) - end) + it("can be called", function () + assert.same("bar", bundle.foo()) + assert.same("bar", en.foo()) + assert.same("bar", bundle["foo"]()) + assert.same("bar", en["foo"]()) + assert.same("baz", bundle.bar()) + assert.same("baz", en.bar()) + assert.same("baz", bundle["bar"]()) + assert.same("baz", en["bar"]()) + end) - it('can be cast to strings', function () - assert.same("baz", tostring(bundle.bar)) - assert.same("baz", tostring(en.bar)) - assert.same("bar", tostring(bundle["foo"])) - assert.same("bar", tostring(en["foo"])) - assert.same("xbar", "x" .. bundle.foo) - assert.same("xbar", "x" .. en.foo) - assert.same("xbaz", "x" .. bundle["bar"]) - assert.same("xbaz", "x" .. en["bar"]) - assert.same("barx", bundle.foo .. "x") - assert.same("barx", en.foo .. "x") - assert.same("bazx", bundle["bar"] .. "x") - assert.same("bazx", en["bar"] .. "x") - end) + it("can be called with parameters", function () + assert.same("Hello World!", bundle.hi({ name = "World" })) + assert.same("Hello World!", en.hi({ name = "World" })) + assert.same("Hello World!", bundle["hi"]({ name = "World" })) + assert.same("Hello World!", en["hi"]({ name = "World" })) + assert.same("Hello World!", bundle["hi"]:format({ name = "World" })) + assert.same("Hello World!", en["hi"]:format({ name = "World" })) + end) - end) + it("can be cast to strings", function () + assert.same("baz", tostring(bundle.bar)) + assert.same("baz", tostring(en.bar)) + assert.same("bar", tostring(bundle["foo"])) + assert.same("bar", tostring(en["foo"])) + assert.same("xbar", "x" .. bundle.foo) + assert.same("xbar", "x" .. en.foo) + assert.same("xbaz", "x" .. bundle["bar"]) + assert.same("xbaz", "x" .. en["bar"]) + assert.same("barx", bundle.foo .. "x") + assert.same("barx", en.foo .. "x") + assert.same("bazx", bundle["bar"] .. "x") + assert.same("bazx", en["bar"] .. "x") + end) + end) - it('should keep bundle instances separate', function () - local bundle1 = FluentBundle("en") - local bundle2 = FluentBundle("tr") - assert.not_same(bundle1, bundle2) - bundle1:add_messages("hi = hello") - bundle2:add_messages("hi = merhaba") - assert.same("hello", bundle1:format("hi")) - assert.same("merhaba", bundle2:format("hi")) - end) - - it('should not clobber itself when adding locales', function () - local bundle = FluentBundle("en") - -- confirm we have a clean instance, not the previous test - assert.error(function () return bundle:format("hi") end) - bundle:add_messages("hi = hello") - assert.same("hello", bundle:format("hi")) - bundle:set_locale("tr") - bundle:add_messages("hi = merhaba2") - assert.same("merhaba2", bundle:format("hi")) - bundle:set_locale("en") - bundle:add_messages("hi = hello2") - local en = bundle:get_resource("en") - local tr = bundle:get_resource("tr") - assert.same("merhaba2", tr:format("hi")) - assert.same("hello2", en:format("hi")) - -- TODO Penlight hack doesn't work around this? - assert.same("merhaba2", tr.hi()) - -- assert.same("hello2", en.hi()) - end) + it("should keep bundle instances separate", function () + local bundle1 = FluentBundle("en") + local bundle2 = FluentBundle("tr") + assert.not_same(bundle1, bundle2) + bundle1:add_messages("hi = hello") + bundle2:add_messages("hi = merhaba") + assert.same("hello", bundle1:format("hi")) + assert.same("merhaba", bundle2:format("hi")) + end) + it("should not clobber itself when adding locales", function () + local bundle = FluentBundle("en") + -- confirm we have a clean instance, not the previous test + assert.error(function () + return bundle:format("hi") + end) + bundle:add_messages("hi = hello") + assert.same("hello", bundle:format("hi")) + bundle:set_locale("tr") + bundle:add_messages("hi = merhaba2") + assert.same("merhaba2", bundle:format("hi")) + bundle:set_locale("en") + bundle:add_messages("hi = hello2") + local en = bundle:get_resource("en") + local tr = bundle:get_resource("tr") + assert.same("merhaba2", tr:format("hi")) + assert.same("hello2", en:format("hi")) + -- TODO Penlight hack doesn't work around this? + assert.same("merhaba2", tr.hi()) + -- assert.same("hello2", en.hi()) + end) end) diff --git a/spec/fluent_syntax_spec.lua b/spec/fluent_syntax_spec.lua index eceb0d4..7918560 100644 --- a/spec/fluent_syntax_spec.lua +++ b/spec/fluent_syntax_spec.lua @@ -1,122 +1,131 @@ -- Internal modules local FluentSyntax = require("fluent.syntax") -describe('fluent.syntax', function () - local syntax = FluentSyntax() - - it('should be independently instantiatable', function () - assert.is_true(syntax:is_a(FluentSyntax)) - end) - - describe('parsestring', function () - - it('should be called as a method', function () - assert.error(function () syntax.parsestring() end) - assert.error(function () syntax.parsestring("") end) - end) - - it('should require a string', function () - assert.error(function () syntax:parsestring() end) - assert.error(function () syntax:parsestring(false) end) - assert.error(function () syntax:parsestring(1) end) - assert.error(function () syntax:parsestring({}) end) - end) - - it('should return an empty AST on no input', function () - assert.equals("Resource", syntax:parsestring("").type) - end) - - it('should handle blank blocks', function () - assert.equals(0, #syntax:parsestring(" ")) - assert.equals(0, #syntax:parsestring(" \n \n")) - end) - - it('should handle a simple entry', function () - local foo = syntax:parsestring("foo = bar") - assert.equals("Identifier", foo.body[1].id.type) - assert.equals("Pattern", foo.body[1].value.type) - end) - - it('should handle term entries', function () - local baz = syntax:parsestring("-baz = qux") - assert.equals("Identifier", baz.body[1].id.type) - assert.equals("Pattern", baz.body[1].value.type) - end) - - it('should handle a entry with an attribute', function () - local foobaz = syntax:parsestring("foo = bar\n .baz = qux") - -- assert.equals("Attribute", foobaz.body[1].attributes[1].type) - assert.equals("Pattern", foobaz.body[1].value.type) - end) - - it('should handle multiline entries', function () - local foo = syntax:parsestring("foo = türkçe\n görüşürüz") - assert.equals("Pattern", foo.body[1].value.type) - end) - - it('should handle literal string placables', function () - local foo = syntax:parsestring('foo = bar {"baz"}') - assert.equals(2, #foo.body[1].value.elements) - assert.equals("TextElement", foo.body[1].value.elements[1].type) - assert.equals("StringLiteral", foo.body[1].value.elements[2].expression.type) - end) - - it('should handle literal number placables', function () - local foo = syntax:parsestring('foo = bar {-54.3}') - assert.equals(2, #foo.body[1].value.elements) - assert.equals("TextElement", foo.body[1].value.elements[1].type) - assert.equals("NumberLiteral", foo.body[1].value.elements[2].expression.type) - end) - - it('should handle variable reference placables', function () - local foo = syntax:parsestring('foo = bar { $baz }') - assert.equals(2, #foo.body[1].value.elements) - assert.equals("TextElement", foo.body[1].value.elements[1].type) - assert.equals("VariableReference", foo.body[1].value.elements[2].expression.type) - end) - - it('should handle message reference placables', function () - local foo = syntax:parsestring('foo = bar { baz }') - assert.equals(2, #foo.body[1].value.elements) - assert.equals("TextElement", foo.body[1].value.elements[1].type) - assert.equals("MessageReference", foo.body[1].value.elements[2].expression.type) - end) - - it('should handle term reference placables', function () - local foo = syntax:parsestring('foo = bar { -baz }') - assert.equals(2, #foo.body[1].value.elements) - assert.equals("TextElement", foo.body[1].value.elements[1].type) - assert.equals("TermReference", foo.body[1].value.elements[2].expression.type) - end) - - it('should handle message plus attributes', function () - local foo = syntax:parsestring('foo = bar\n .baz = qux') - assert.equals(1, #foo.body[1].value.elements) - assert.equals(1, #foo.body[1].attributes) - assert.equals("TextElement", foo.body[1].value.elements[1].type) - assert.equals("TextElement", foo.body[1].attributes[1].value.elements[1].type) - end) - - it('should handle just attributes', function () - local foo = syntax:parsestring('foo =\n .baz = qux') - assert.equals("nil", type(foo.body[1].value)) - assert.equals(1, #foo.body[1].attributes) - assert.equals("TextElement", foo.body[1].attributes[1].value.elements[1].type) - end) - - it('should handle simple comments', function () - assert.equals("Comment", syntax:parsestring("# foo").body[1].type) - assert.equals("GroupComment", syntax:parsestring("## foo").body[1].type) - assert.equals("ResourceComment", syntax:parsestring("### foo").body[1].type) - end) - - it('should handle junk', function () - assert.equals("Junk", syntax:parsestring("foo{").body[1].type) - assert.equals("Junk", syntax:parsestring("ą=b").body[1].type) - assert.equals("Junk", syntax:parsestring("!").body[1].type) - assert.equals("Junk", syntax:parsestring("#foo").body[1].type) - end) - - end) - +describe("fluent.syntax", function () + local syntax = FluentSyntax() + + it("should be independently instantiatable", function () + assert.is_true(syntax:is_a(FluentSyntax)) + end) + + describe("parsestring", function () + it("should be called as a method", function () + assert.error(function () + syntax.parsestring() + end) + assert.error(function () + syntax.parsestring("") + end) + end) + + it("should require a string", function () + assert.error(function () + syntax:parsestring() + end) + assert.error(function () + syntax:parsestring(false) + end) + assert.error(function () + syntax:parsestring(1) + end) + assert.error(function () + syntax:parsestring({}) + end) + end) + + it("should return an empty AST on no input", function () + assert.equals("Resource", syntax:parsestring("").type) + end) + + it("should handle blank blocks", function () + assert.equals(0, #syntax:parsestring(" ")) + assert.equals(0, #syntax:parsestring(" \n \n")) + end) + + it("should handle a simple entry", function () + local foo = syntax:parsestring("foo = bar") + assert.equals("Identifier", foo.body[1].id.type) + assert.equals("Pattern", foo.body[1].value.type) + end) + + it("should handle term entries", function () + local baz = syntax:parsestring("-baz = qux") + assert.equals("Identifier", baz.body[1].id.type) + assert.equals("Pattern", baz.body[1].value.type) + end) + + it("should handle a entry with an attribute", function () + local foobaz = syntax:parsestring("foo = bar\n .baz = qux") + -- assert.equals("Attribute", foobaz.body[1].attributes[1].type) + assert.equals("Pattern", foobaz.body[1].value.type) + end) + + it("should handle multiline entries", function () + local foo = syntax:parsestring("foo = türkçe\n görüşürüz") + assert.equals("Pattern", foo.body[1].value.type) + end) + + it("should handle literal string placables", function () + local foo = syntax:parsestring('foo = bar {"baz"}') + assert.equals(2, #foo.body[1].value.elements) + assert.equals("TextElement", foo.body[1].value.elements[1].type) + assert.equals("StringLiteral", foo.body[1].value.elements[2].expression.type) + end) + + it("should handle literal number placables", function () + local foo = syntax:parsestring("foo = bar {-54.3}") + assert.equals(2, #foo.body[1].value.elements) + assert.equals("TextElement", foo.body[1].value.elements[1].type) + assert.equals("NumberLiteral", foo.body[1].value.elements[2].expression.type) + end) + + it("should handle variable reference placables", function () + local foo = syntax:parsestring("foo = bar { $baz }") + assert.equals(2, #foo.body[1].value.elements) + assert.equals("TextElement", foo.body[1].value.elements[1].type) + assert.equals("VariableReference", foo.body[1].value.elements[2].expression.type) + end) + + it("should handle message reference placables", function () + local foo = syntax:parsestring("foo = bar { baz }") + assert.equals(2, #foo.body[1].value.elements) + assert.equals("TextElement", foo.body[1].value.elements[1].type) + assert.equals("MessageReference", foo.body[1].value.elements[2].expression.type) + end) + + it("should handle term reference placables", function () + local foo = syntax:parsestring("foo = bar { -baz }") + assert.equals(2, #foo.body[1].value.elements) + assert.equals("TextElement", foo.body[1].value.elements[1].type) + assert.equals("TermReference", foo.body[1].value.elements[2].expression.type) + end) + + it("should handle message plus attributes", function () + local foo = syntax:parsestring("foo = bar\n .baz = qux") + assert.equals(1, #foo.body[1].value.elements) + assert.equals(1, #foo.body[1].attributes) + assert.equals("TextElement", foo.body[1].value.elements[1].type) + assert.equals("TextElement", foo.body[1].attributes[1].value.elements[1].type) + end) + + it("should handle just attributes", function () + local foo = syntax:parsestring("foo =\n .baz = qux") + assert.equals("nil", type(foo.body[1].value)) + assert.equals(1, #foo.body[1].attributes) + assert.equals("TextElement", foo.body[1].attributes[1].value.elements[1].type) + end) + + it("should handle simple comments", function () + assert.equals("Comment", syntax:parsestring("# foo").body[1].type) + assert.equals("GroupComment", syntax:parsestring("## foo").body[1].type) + assert.equals("ResourceComment", syntax:parsestring("### foo").body[1].type) + end) + + it("should handle junk", function () + assert.equals("Junk", syntax:parsestring("foo{").body[1].type) + assert.equals("Junk", syntax:parsestring("ą=b").body[1].type) + assert.equals("Junk", syntax:parsestring("!").body[1].type) + assert.equals("Junk", syntax:parsestring("#foo").body[1].type) + end) + end) end) diff --git a/spec/spec.ftl b/spec/spec.ftl new file mode 100644 index 0000000..d5dd18c --- /dev/null +++ b/spec/spec.ftl @@ -0,0 +1 @@ +foo = bar