-
Notifications
You must be signed in to change notification settings - Fork 0
/
PropertiesReader.lua
170 lines (158 loc) · 5.06 KB
/
PropertiesReader.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
---@version 5.2
local Properties = require 'Properties'
local PropertiesReader = {}
---Reads a physical line from a file, with left whitespace and comments trimmed
---out.
---
---Takes an iterator returning string, and returns:
--- - nil on iterator end
--- - string on empty line
--- - the line with left whitespace trimmed, if it has meaningful content.
---@param reader fun():string|nil
---@return string|nil line
local readPhysicalLine = function(reader)
local line = reader()
if not line then
return nil
end
return line:match '^[ \t\f]*(.*)'
end
---Continuously tries reading physical lines until there's meaningful content.
---This function will always return either nil or a string with content.
---@param reader fun():string|nil
---@return string|nil line
local readPhysLinesUntilContent = function(reader)
local physLine = nil
repeat
physLine = readPhysicalLine(reader)
if physLine ~= nil and physLine:match('^[ \t\f]*[#!]') then
physLine = ''
end
until physLine ~= ''
return physLine
end
-- Constants related to escaping
local BACKSLASH = string.byte '\\'
local ESCAPES = {
[string.byte 't'] = '\t',
[string.byte 'r'] = '\r',
[string.byte 'n'] = '\n',
[string.byte 'f'] = '\f',
}
local UNICODE_ESCAPE = string.byte 'u'
---Processes escape sequences vaguely according to the way the Java impl
---of Properties proccesses them.
---
---**Notice about implementation details**: Unicode libraries are not available
---on Lua 5.2, which this code targets. Therefore, the `\uxxxx` escape sequence
---is *not* implemented, and will be pasted in verbatim, including backslash.
---
---The following escapes are treated here:
--- - `\t` into tab stop
--- - `\r` into carriage return
--- - `\n` into line feed
--- - `\f` into form feed
--- - arbitrary character escaping
---@param str string
---@returns string
local renderEscapes = function(str)
local s = ''
local i = 1
while i <= #str do
local escape
if str:byte(i) ~= BACKSLASH then
s = s .. str:sub(i, i)
goto continue
end
i = i + 1
if i > #str then
break
end
escape = str:byte(i)
if escape == UNICODE_ESCAPE then
-- NOTE: Unicode escapes are NOT supported --
s = s .. str:sub(i - 1, i)
goto continue
end
s = s .. (ESCAPES[escape] or str:sub(i, i))
::continue::
i = i + 1
end
return s
end
---Finds the indices of the end of the key and the start of the value. The
---returned indices take into account the existence of the separator.
---@param line string
---@return number keyEnd, number valueStart
local findSeparator = function(line)
-- FIXME: Make logic simpler and more readable
local keyEndGraph, valueStartGraph = line:find '[^\\][=:]'
local keyEndSpace, valueStartSpace = line:find '[^\\][ \t\f]'
if keyEndGraph and keyEndSpace then
if keyEndGraph < keyEndSpace then
return keyEndGraph, valueStartGraph + 1
else
if line:sub(keyEndSpace, valueStartGraph):match '[^\\][ \t\f]+[=:]' then
return keyEndSpace, valueStartGraph + 1
end
end
end
if not keyEndSpace then
if not keyEndGraph then
return #line, #line + 1
else
return keyEndGraph, valueStartGraph + 1
end
else
if keyEndGraph and keyEndGraph == keyEndSpace + 1 then
return keyEndGraph, valueStartGraph + 1
else
return keyEndSpace, valueStartSpace + 1
end
end
end
---Reads one or more physical lines from an iterator-style reader function and
---returns a key-value pair, according to the way Properties files are parsed
---in Java.
---
---This function returns:
--- - nil if it's reached the end of the file
--- - two strings representing a key-value pair.
---@param reader fun():string|nil
---@return string|nil key
---@return string|nil value
local readVirtualLine = function(reader)
local line = readPhysLinesUntilContent(reader)
if line == nil then
return nil
end
local rest = '' ---@type string|nil
while line:match '[^\\]\\$' and rest ~= nil do
line = line:sub(1, #line - 1)
rest = readPhysicalLine(reader)
if rest ~= nil then
line = line .. rest
end
end
local keyEnd, valueStart = findSeparator(line)
local key = line:sub(1, keyEnd) or ''
local value = line:sub(valueStart, #line) or ''
value = value:match '^[ \t\f]*(.*)'
key = renderEscapes(key)
value = renderEscapes(value)
return key, value
end
---Creates a new Properties given an iterator that behaves like `file:read '*l'`
---or `file:lines '*l'`.
---
---This function can be invoked, for example, like
---`PropertiesReader.new(propFile:lines())`.
---@param reader fun():string|nil
PropertiesReader.read = function(reader)
local props = Properties.new()
for key, value in function() return readVirtualLine(reader) end do
props[key] = value
end
return props
end
return PropertiesReader