aboutsummaryrefslogtreecommitdiff
path: root/nimrec.nim
blob: 034bbfc59146b2d0bff53461246fc76ba465d30c (plain)
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)