alpha
Login
or
Join now
kwando.tngl.sh
/
gcal
Star
1
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
I calendar parsing
Star
1
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
parse using splitter instead
author
Hannes Nevalainen
date
17 hours ago
(Jul 26, 2026, 11:00 PM +0200)
commit
8d64c623
8d64c623e38c6870776efa839fb7c9965939e0a9
parent
2f077b6e
2f077b6e3a151c7d59a36fe88da1b8861ee1dd9a
+149
-61
3 changed files
Expand all
Collapse all
Unified
Split
gleam.toml
manifest.toml
src
gcal.gleam
+1
gleam.toml
View file
Reviewed
···
15
15
[dependencies]
16
16
gleam_stdlib = ">= 1.0.0 and < 2.0.0"
17
17
simplifile = ">= 2.6.0 and < 3.0.0"
18
18
+
splitter = ">= 1.2.0 and < 2.0.0"
18
19
19
20
[dev_dependencies]
20
21
gleeunit = ">= 1.0.0 and < 2.0.0"
+2
manifest.toml
View file
Reviewed
···
11
11
{ name = "gleam_stdlib", version = "1.0.3", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "1F543AFBA5D33DA493E6087F4E4C4F20D899411343512686C98A8ABB2963CF22" },
12
12
{ name = "gleeunit", version = "1.11.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "EC31ABA74256AEA531EDF8169931D775BBB384FED0A8A1BDC4DD9354E3E21826" },
13
13
{ name = "simplifile", version = "2.6.0", build_tools = ["gleam"], requirements = ["filepath", "gleam_stdlib"], otp_app = "simplifile", source = "hex", outer_checksum = "A33C345F0A4FFB91DCCD4220114534A58C387964A5F17B3E472CEBD1ADA9FFB4" },
14
14
+
{ name = "splitter", version = "1.2.0", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "splitter", source = "hex", outer_checksum = "3DFD6B6C49E61EDAF6F7B27A42054A17CFF6CA2135FF553D0CB61C234D281DD0" },
14
15
]
15
16
16
17
[requirements]
17
18
gleam_stdlib = { version = ">= 1.0.0 and < 2.0.0" }
18
19
gleeunit = { version = ">= 1.0.0 and < 2.0.0" }
19
20
simplifile = { version = ">= 2.6.0 and < 3.0.0" }
21
21
+
splitter = { version = ">= 1.2.0 and < 2.0.0" }
+146
-61
src/gcal.gleam
View file
Reviewed
···
1
1
import gleam/list
2
2
import gleam/result
3
3
import gleam/string
4
4
+
import splitter
4
5
5
6
pub type Calendar {
6
7
Calendar(version: String, prodid: String, events: List(Event))
···
32
33
Component(kind: String, properties: List(Property), children: List(Component))
33
34
}
34
35
36
36
+
type Splitters {
37
37
+
Splitters(
38
38
+
lines: splitter.Splitter,
39
39
+
begin: splitter.Splitter,
40
40
+
colon: splitter.Splitter,
41
41
+
semi: splitter.Splitter,
42
42
+
eq: splitter.Splitter,
43
43
+
)
44
44
+
}
45
45
+
35
46
pub fn parse(input: String) -> Result(Calendar, Error) {
36
36
-
let lines =
37
37
-
input
38
38
-
|> string.replace("\r\n", "\n")
39
39
-
|> string.replace("\r", "\n")
40
40
-
|> string.split("\n")
41
41
-
|> unfold_lines
42
42
-
|> list.filter(fn(line) { line != "" })
47
47
+
let splitters =
48
48
+
Splitters(
49
49
+
lines: splitter.new(["\r\n", "\n"]),
50
50
+
begin: splitter.new(["BEGIN:"]),
51
51
+
colon: splitter.new([":"]),
52
52
+
semi: splitter.new([";"]),
53
53
+
eq: splitter.new(["="]),
54
54
+
)
43
55
44
44
-
case parse_all_components(lines, []) {
56
56
+
let lines = unfold_lines(input, splitters.lines)
57
57
+
let non_empty = list.filter(lines, fn(line) { line != "" })
58
58
+
59
59
+
case parse_all_components(non_empty, [], splitters) {
45
60
Ok(components) -> build_calendar(components)
46
61
Error(err) -> Error(err)
47
62
}
48
63
}
49
64
50
50
-
fn unfold_lines(lines: List(String)) -> List(String) {
51
51
-
lines
52
52
-
|> list.fold([], fn(acc: List(String), line: String) {
53
53
-
case acc {
54
54
-
[] -> [line]
55
55
-
[prev, ..rest] ->
56
56
-
case string.starts_with(line, " ") || string.starts_with(line, "\t") {
57
57
-
True ->
58
58
-
case string.pop_grapheme(line) {
59
59
-
Ok(#(_, remaining)) -> [prev <> remaining, ..rest]
60
60
-
Error(_) -> [prev, ..acc]
61
61
-
}
62
62
-
False -> [line, prev, ..rest]
63
63
-
}
64
64
-
}
65
65
-
})
65
65
+
fn unfold_lines(
66
66
+
input: String,
67
67
+
line_splitter: splitter.Splitter,
68
68
+
) -> List(String) {
69
69
+
do_unfold_lines(input, line_splitter, [])
66
70
|> list.reverse
67
71
}
68
72
73
73
+
fn do_unfold_lines(
74
74
+
input: String,
75
75
+
line_splitter: splitter.Splitter,
76
76
+
acc: List(String),
77
77
+
) -> List(String) {
78
78
+
case splitter.split(line_splitter, input) {
79
79
+
#(line, "", "") ->
80
80
+
case acc {
81
81
+
[] -> [line]
82
82
+
[prev, ..rest] ->
83
83
+
case string.starts_with(line, " ") || string.starts_with(line, "\t") {
84
84
+
True ->
85
85
+
case string.pop_grapheme(line) {
86
86
+
Ok(#(_, remaining)) -> [prev <> remaining, ..rest]
87
87
+
Error(_) -> [prev, ..acc]
88
88
+
}
89
89
+
False -> [line, prev, ..rest]
90
90
+
}
91
91
+
}
92
92
+
#(line, _, remaining) ->
93
93
+
case acc {
94
94
+
[] -> do_unfold_lines(remaining, line_splitter, [line])
95
95
+
[prev, ..rest] ->
96
96
+
case string.starts_with(line, " ") || string.starts_with(line, "\t") {
97
97
+
True ->
98
98
+
case string.pop_grapheme(line) {
99
99
+
Ok(#(_, remaining_line)) ->
100
100
+
do_unfold_lines(remaining, line_splitter, [
101
101
+
prev <> remaining_line,
102
102
+
..rest
103
103
+
])
104
104
+
Error(_) ->
105
105
+
do_unfold_lines(remaining, line_splitter, [prev, ..acc])
106
106
+
}
107
107
+
False ->
108
108
+
do_unfold_lines(remaining, line_splitter, [line, prev, ..rest])
109
109
+
}
110
110
+
}
111
111
+
}
112
112
+
}
113
113
+
69
114
fn parse_all_components(
70
115
lines: List(String),
71
116
acc: List(Component),
117
117
+
splitters: Splitters,
72
118
) -> Result(List(Component), Error) {
73
119
case lines {
74
120
[] -> Ok(list.reverse(acc))
75
75
-
["BEGIN:" <> kind, ..rest] ->
76
76
-
case parse_component(kind, rest, [], []) {
77
77
-
Ok(#(component, remaining)) ->
78
78
-
parse_all_components(remaining, [component, ..acc])
79
79
-
Error(err) -> Error(err)
121
121
+
[line, ..rest] ->
122
122
+
case splitter.split(splitters.begin, line) {
123
123
+
#("", "BEGIN:", kind) ->
124
124
+
case parse_component(kind, rest, [], [], splitters) {
125
125
+
Ok(#(component, remaining)) ->
126
126
+
parse_all_components(remaining, [component, ..acc], splitters)
127
127
+
Error(err) -> Error(err)
128
128
+
}
129
129
+
_ -> Error(ParseError("Unexpected line: " <> line))
80
130
}
81
81
-
[line, ..] -> Error(ParseError("Unexpected line: " <> line))
82
131
}
83
132
}
84
133
···
87
136
lines: List(String),
88
137
props: List(Property),
89
138
children: List(Component),
139
139
+
splitters: Splitters,
90
140
) -> Result(#(Component, List(String)), Error) {
91
141
let end_marker = "END:" <> kind
92
142
case lines {
···
99
149
rest,
100
150
))
101
151
False ->
102
102
-
case string.starts_with(line, "BEGIN:") {
103
103
-
True -> {
104
104
-
let subkind =
105
105
-
case string.split_once(line, "BEGIN:") {
106
106
-
Ok(#(_, k)) -> k
107
107
-
Error(_) -> ""
108
108
-
}
109
109
-
case parse_component(subkind, rest, [], []) {
152
152
+
case splitter.split(splitters.begin, line) {
153
153
+
#("", "BEGIN:", subkind) ->
154
154
+
case parse_component(subkind, rest, [], [], splitters) {
110
155
Ok(#(child, remaining)) ->
111
111
-
parse_component(kind, remaining, props, [child, ..children])
156
156
+
parse_component(
157
157
+
kind,
158
158
+
remaining,
159
159
+
props,
160
160
+
[child, ..children],
161
161
+
splitters,
162
162
+
)
112
163
Error(err) -> Error(err)
113
164
}
114
114
-
}
115
115
-
False ->
116
116
-
case parse_property(line) {
165
165
+
_ ->
166
166
+
case parse_property(line, splitters) {
117
167
Ok(prop) ->
118
118
-
parse_component(kind, rest, [prop, ..props], children)
168
168
+
parse_component(
169
169
+
kind,
170
170
+
rest,
171
171
+
[prop, ..props],
172
172
+
children,
173
173
+
splitters,
174
174
+
)
119
175
Error(err) -> Error(err)
120
176
}
121
177
}
···
123
179
}
124
180
}
125
181
126
126
-
fn parse_property(line: String) -> Result(Property, Error) {
127
127
-
case string.split_once(line, ":") {
128
128
-
Ok(#(name_part, value)) -> {
129
129
-
let #(name, params) = parse_name_and_params(name_part)
182
182
+
fn parse_property(
183
183
+
line: String,
184
184
+
splitters: Splitters,
185
185
+
) -> Result(Property, Error) {
186
186
+
case splitter.split(splitters.colon, line) {
187
187
+
#(name_part, ":", value) -> {
188
188
+
let #(name, params) = parse_name_and_params(name_part, splitters)
130
189
Ok(Property(name, params, unescape_text(value)))
131
190
}
132
132
-
Error(_) -> Error(ParseError("Invalid property line: " <> line))
191
191
+
_ -> Error(ParseError("Invalid property line: " <> line))
192
192
+
}
193
193
+
}
194
194
+
195
195
+
fn parse_name_and_params(
196
196
+
part: String,
197
197
+
splitters: Splitters,
198
198
+
) -> #(String, List(Parameter)) {
199
199
+
case splitter.split(splitters.semi, part) {
200
200
+
#(name, ";", params_str) -> #(name, parse_params(params_str, splitters))
201
201
+
_ -> #(part, [])
133
202
}
134
203
}
135
204
136
136
-
fn parse_name_and_params(part: String) -> #(String, List(Parameter)) {
137
137
-
case string.split_once(part, ";") {
138
138
-
Ok(#(name, params_str)) -> #(name, parse_params(params_str))
139
139
-
Error(_) -> #(part, [])
205
205
+
fn parse_params(params_str: String, splitters: Splitters) -> List(Parameter) {
206
206
+
do_parse_params(params_str, [], splitters)
207
207
+
}
208
208
+
209
209
+
fn do_parse_params(
210
210
+
input: String,
211
211
+
acc: List(Parameter),
212
212
+
splitters: Splitters,
213
213
+
) -> List(Parameter) {
214
214
+
case splitter.split(splitters.semi, input) {
215
215
+
#(param, ";", remaining) ->
216
216
+
case parse_single_param(param, splitters) {
217
217
+
Ok(p) -> do_parse_params(remaining, [p, ..acc], splitters)
218
218
+
Error(_) -> do_parse_params(remaining, acc, splitters)
219
219
+
}
220
220
+
#(param, "", "") ->
221
221
+
case parse_single_param(param, splitters) {
222
222
+
Ok(p) -> list.reverse([p, ..acc])
223
223
+
Error(_) -> list.reverse(acc)
224
224
+
}
225
225
+
_ -> list.reverse(acc)
140
226
}
141
227
}
142
228
143
143
-
fn parse_params(params_str: String) -> List(Parameter) {
144
144
-
params_str
145
145
-
|> string.split(";")
146
146
-
|> list.filter_map(fn(param) {
147
147
-
case string.split_once(param, "=") {
148
148
-
Ok(#(name, value)) -> Ok(Parameter(name, value))
149
149
-
Error(_) -> Error(Nil)
150
150
-
}
151
151
-
})
229
229
+
fn parse_single_param(
230
230
+
param: String,
231
231
+
splitters: Splitters,
232
232
+
) -> Result(Parameter, Error) {
233
233
+
case splitter.split(splitters.eq, param) {
234
234
+
#(name, "=", value) -> Ok(Parameter(name, value))
235
235
+
_ -> Error(ParseError("Invalid parameter: " <> param))
236
236
+
}
152
237
}
153
238
154
239
fn unescape_text(text: String) -> String {