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
|
import streams
import strutils
import tables
type
Field* = ref object
label*: string
value*: string
Record* = ref object
fields: OrderedTableRef[string, seq[string]]
ParseState {.pure.} = enum
Initial
Comment
Label
Value
ValueSkipSpace
FieldReady
RecParser* = ref object
state: ParseState
field: Field
record: Record
RecParseError* = object of Exception
const
LabelFirstChar = {'a'..'z', 'A'..'Z', '%'}
LabelChar = {'a'..'z', 'A'..'Z', '0'..'9', '_'}
EofMarker = '\0'
proc newRecParser*(): RecParser =
new(result)
result.state = ParseState.Initial
proc newField(): Field =
new(result)
result.label = ""
result.value = ""
proc newField(label, value: string): Field =
new(result)
result.label = label
result.value = value
proc newRecord(): Record =
new(result)
result.fields = newOrderedTable[string, seq[string]]()
proc feed*(parser: RecParser, ch: char, record: var Record): bool =
while true:
case parser.state
of ParseState.Initial:
case ch
of '#':
parser.state = ParseState.Comment
of '\l', EofMarker:
if parser.record != nil:
result = true
record = parser.record
parser.record = nil
of LabelFirstChar:
parser.state = ParseState.Label
parser.field = newField()
parser.field.label &= ch
else:
raise newException(RecParseError, "parse error: expected a comment, a label or an empty line")
of ParseState.Comment:
case ch
of '\l':
parser.state = ParseState.Initial
else: discard
of ParseState.Label:
case ch
of ':':
parser.state = ParseState.ValueSkipSpace
of LabelChar:
parser.field.label &= ch
else:
raise newException(RecParseError,
"parse error: invalid label char: " & ch)
of ParseState.Value:
case ch
of '\l':
let valueLen = len(parser.field.value)
if valueLen > 0 and parser.field.value[valueLen-1] == '\\':
setLen(parser.field.value, valueLen - 1)
else:
parser.state = ParseState.FieldReady
of EofMarker:
raise newException(RecParseError,
"parse error: value must be terminated by a newline")
else:
parser.field.value &= ch
of ParseState.ValueSkipSpace:
case ch
of (WhiteSpace - NewLines):
discard
else:
parser.field.value &= ch
parser.state = ParseState.Value
of ParseState.FieldReady:
case ch
of '+':
parser.state = ParseState.ValueSkipSpace
parser.field.value &= '\l'
else:
if parser.record == nil:
parser.record = newRecord()
if hasKey(parser.record.fields, parser.field.label):
add(parser.record.fields[parser.field.label], parser.field.value)
else:
add(parser.record.fields, parser.field.label,
@[parser.field.value])
parser.field = nil
parser.state = ParseState.Initial
continue
break
proc `[]`*(record: Record, label: string): string =
result = record.fields[label][0]
proc len*(record: Record): int =
result = len(record.fields)
iterator records*(stream: Stream): Record =
let parser = newRecParser()
var record: Record
while true:
var ch = readChar(stream)
if feed(parser, ch, record):
yield record
if ch == EofMarker:
break
iterator pairs*(record: Record): (string, string) =
for label, values in record.fields:
for value in values:
yield (label, value)
iterator items*(record: Record): Field =
for label, value in record:
yield newField(label, value)
proc hasField*(record: Record, label: string): bool =
for field in record:
if field.label == label:
return true
proc contains*(record: Record, label: string): bool =
result = hasField(record, label)
|