Packages
gettext
0.17.4
1.0.2
1.0.1
retired
1.0.0
0.26.2
0.26.1
0.26.0
retired
0.25.0
0.24.0
0.23.1
0.23.0
0.22.3
0.22.2
0.22.1
0.22.0
0.21.0
0.20.0
0.19.1
0.19.0
0.18.2
0.18.1
0.18.0
0.17.4
0.17.3
0.17.2
0.17.1
0.17.0
0.16.1
0.16.0
0.15.0
0.14.1
0.14.0
0.13.1
0.13.0
0.12.2
0.12.1
0.12.0
0.11.0
0.10.0
0.9.0
0.8.0
0.7.0
0.6.1
0.6.0
0.5.0
Internationalization and localization through gettext
Current section
Files
Jump to
Current section
Files
lib/gettext/po/tokenizer.ex
defmodule Gettext.PO.Tokenizer do
@moduledoc false
# This module is responsible for turning a chunk of text (a string) into a
# list of tokens. For what "token" means, see the docs for `tokenize/1`.
@type line :: pos_integer
@type token ::
{:str, line, binary}
| {:plural_form, line, non_neg_integer}
| {:msgid, line}
| {:msgid_plural, line}
| {:msgstr, line}
| {:msgctxt, line}
| {:comment, line, binary}
# In this list of keywords *the order matters* because a function clause is
# generated for each keyword, and keywords have to be followed by whitespace.
# `msgid_plural` would cause an error if it didn't come before `msgid`.
# Also note that the `msgstr` keyword is missing here since it can be also
# followed by a plural form (for example, `[1]`).
@keywords ~w(
msgid_plural
msgid
msgctxt
)
@whitespace [?\n, ?\t, ?\r, ?\s]
@whitespace_no_nl [?\t, ?\r, ?\s]
@escapable_chars [?", ?n, ?t, ?r, ?\\]
@doc """
Converts a string into a list of tokens.
A "token" is a tuple formed by:
* the `:str` tag or a keyword tag (like `:msgid`)
* the line the token is at
* the value of the token if the token has a value (for example, a `:str`
token will have the contents of the string as a value)
Some examples of tokens are:
* `{:msgid, 33}`
* `{:str, 6, "foo"}`
"""
@spec tokenize(binary) :: {:ok, [token]} | {:error, pos_integer, binary}
def tokenize(str) do
tokenize_line(str, 1, [])
end
# Converts the first line in `str` into a list of tokens and then moves on to
# the next line.
@spec tokenize_line(binary, pos_integer, [token]) ::
{:ok, [token]} | {:error, pos_integer, binary}
defp tokenize_line(str, line, acc)
# End of file.
defp tokenize_line(<<>>, _line, acc) do
{:ok, Enum.reverse(acc)}
end
# Go to the next line.
defp tokenize_line(<<?\n, rest::binary>>, line, acc) do
tokenize_line(rest, line + 1, acc)
end
# Skip whitespace.
defp tokenize_line(<<char, rest::binary>>, line, acc)
when char in @whitespace_no_nl do
tokenize_line(rest, line, acc)
end
# Comments.
defp tokenize_line(<<?#, _::binary>> = rest, line, acc) do
{contents, rest} = to_eol_or_eof(rest, "")
acc = [{:comment, line, contents} | acc]
tokenize_line(rest, line, acc)
end
# Keywords.
for kw <- @keywords do
defp tokenize_line(unquote(kw) <> <<char, rest::binary>>, line, acc)
when char in @whitespace do
acc = [{unquote(String.to_atom(kw)), line} | acc]
tokenize_line(rest, line, acc)
end
defp tokenize_line(unquote(kw) <> _rest, line, _acc) do
{:error, line, "no space after '#{unquote(kw)}'"}
end
end
# `msgstr`.
defp tokenize_line("msgstr[" <> <<rest::binary>>, line, acc) do
case tokenize_plural_form(rest, "") do
{:ok, plural_form, rest} ->
# The order of the :plural_form and :msgstr tokens is inverted since
# the `acc` array of tokens will be reversed at the end.
acc = [{:plural_form, line, plural_form}, {:msgstr, line} | acc]
tokenize_line(rest, line, acc)
{:error, reason} ->
{:error, line, reason}
end
end
defp tokenize_line("msgstr" <> <<char, rest::binary>>, line, acc)
when char in @whitespace do
acc = [{:msgstr, line} | acc]
tokenize_line(rest, line, acc)
end
defp tokenize_line("msgstr" <> _rest, line, _acc) do
{:error, line, "no space after 'msgstr'"}
end
# String.
defp tokenize_line(<<?", rest::binary>>, line, acc) do
case tokenize_string(rest, "") do
{:ok, string, rest} ->
token = {:str, line, string}
tokenize_line(rest, line, [token | acc])
{:error, reason} ->
{:error, line, reason}
end
end
# Unknown keyword.
# At this point, there has to be a syntax error. Here, since the first byte is
# a letter (we don't take care of unicode ot fancy stuff, just ASCII letters),
# we assume there's an unknown keyword. We parse it with a regex
# (`next_word/1`) so that the error message is informative.
defp tokenize_line(<<letter, _::binary>> = binary, line, _acc)
when letter in ?a..?z or letter in ?A..?Z do
{:error, line, "unknown keyword '#{next_word(binary)}'"}
end
# Unknown token.
# Last resort: this is just a plain unexpected token. We take the first
# Unicode char of the given binary and build an informative error message
# (with the codepoint of the char).
defp tokenize_line(binary, line, _acc) when is_binary(binary) do
# To get the first Unicode char, we convert to char list first.
[char | _] = String.to_charlist(binary)
msg = :io_lib.format('unexpected token: "~ts" (codepoint U+~4.16.0B)', [[char], char])
{:error, line, :unicode.characters_to_binary(msg)}
end
# Parses the double-quotes-delimited string `str` into a single string. Note
# that `str` doesn't start with a double quote (since that was needed to
# identify the start of a string). Note that the rest of the original string
# doesn't include the closing double quote.
@spec tokenize_string(binary, binary) :: {:ok, binary, binary} | {:error, binary}
defp tokenize_string(str, acc)
defp tokenize_string(<<?", rest::binary>>, acc), do: {:ok, acc, rest}
defp tokenize_string(<<?\\, char, rest::binary>>, acc)
when char in @escapable_chars,
do: tokenize_string(rest, <<acc::binary, escape_char(char)>>)
defp tokenize_string(<<?\\, _char, _rest::binary>>, _acc),
do: {:error, "unsupported escape code"}
defp tokenize_string(<<?\n, _rest::binary>>, _acc), do: {:error, "newline in string"}
defp tokenize_string(<<char, rest::binary>>, acc),
do: tokenize_string(rest, <<acc::binary, char>>)
defp tokenize_string(<<>>, _acc), do: {:error, "missing token \""}
@spec tokenize_plural_form(binary, binary) :: {:ok, non_neg_integer, binary} | {:error, binary}
defp tokenize_plural_form(<<digit, rest::binary>>, acc) when digit in '0123456789' do
tokenize_plural_form(rest, <<acc::binary, digit>>)
end
defp tokenize_plural_form(<<?], char, rest::binary>>, acc)
when char in @whitespace and acc != <<>> do
{:ok, String.to_integer(acc), rest}
end
defp tokenize_plural_form(<<?], _rest::binary>>, acc) when acc != <<>> do
{:error, "missing space after 'msgstr[#{acc}]'"}
end
defp tokenize_plural_form(_binary, _acc) do
{:error, "invalid plural form"}
end
@spec escape_char(char) :: char
defp escape_char(?n), do: ?\n
defp escape_char(?t), do: ?\t
defp escape_char(?r), do: ?\r
defp escape_char(?"), do: ?"
defp escape_char(?\\), do: ?\\
@spec to_eol_or_eof(binary, binary) :: {binary, binary}
defp to_eol_or_eof(<<?\n, _::binary>> = rest, acc), do: {acc, rest}
defp to_eol_or_eof(<<>>, acc), do: {acc, ""}
defp to_eol_or_eof(<<char, rest::binary>>, acc), do: to_eol_or_eof(rest, <<acc::binary, char>>)
@spec next_word(binary) :: binary
defp next_word(binary), do: List.first(Regex.run(~r/\w+/u, binary))
end