I calendar parsing
1

Configure Feed

Select the types of activity you want to include in your feed.

parse using splitter instead

+149 -61
+1
gleam.toml
··· 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 + 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
··· 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 + { 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 + splitter = { version = ">= 1.2.0 and < 2.0.0" }
+146 -61
src/gcal.gleam
··· 1 1 import gleam/list 2 2 import gleam/result 3 3 import gleam/string 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 + type Splitters { 37 + Splitters( 38 + lines: splitter.Splitter, 39 + begin: splitter.Splitter, 40 + colon: splitter.Splitter, 41 + semi: splitter.Splitter, 42 + eq: splitter.Splitter, 43 + ) 44 + } 45 + 35 46 pub fn parse(input: String) -> Result(Calendar, Error) { 36 - let lines = 37 - input 38 - |> string.replace("\r\n", "\n") 39 - |> string.replace("\r", "\n") 40 - |> string.split("\n") 41 - |> unfold_lines 42 - |> list.filter(fn(line) { line != "" }) 47 + let splitters = 48 + Splitters( 49 + lines: splitter.new(["\r\n", "\n"]), 50 + begin: splitter.new(["BEGIN:"]), 51 + colon: splitter.new([":"]), 52 + semi: splitter.new([";"]), 53 + eq: splitter.new(["="]), 54 + ) 43 55 44 - case parse_all_components(lines, []) { 56 + let lines = unfold_lines(input, splitters.lines) 57 + let non_empty = list.filter(lines, fn(line) { line != "" }) 58 + 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 - fn unfold_lines(lines: List(String)) -> List(String) { 51 - lines 52 - |> list.fold([], fn(acc: List(String), line: String) { 53 - case acc { 54 - [] -> [line] 55 - [prev, ..rest] -> 56 - case string.starts_with(line, " ") || string.starts_with(line, "\t") { 57 - True -> 58 - case string.pop_grapheme(line) { 59 - Ok(#(_, remaining)) -> [prev <> remaining, ..rest] 60 - Error(_) -> [prev, ..acc] 61 - } 62 - False -> [line, prev, ..rest] 63 - } 64 - } 65 - }) 65 + fn unfold_lines( 66 + input: String, 67 + line_splitter: splitter.Splitter, 68 + ) -> List(String) { 69 + do_unfold_lines(input, line_splitter, []) 66 70 |> list.reverse 67 71 } 68 72 73 + fn do_unfold_lines( 74 + input: String, 75 + line_splitter: splitter.Splitter, 76 + acc: List(String), 77 + ) -> List(String) { 78 + case splitter.split(line_splitter, input) { 79 + #(line, "", "") -> 80 + case acc { 81 + [] -> [line] 82 + [prev, ..rest] -> 83 + case string.starts_with(line, " ") || string.starts_with(line, "\t") { 84 + True -> 85 + case string.pop_grapheme(line) { 86 + Ok(#(_, remaining)) -> [prev <> remaining, ..rest] 87 + Error(_) -> [prev, ..acc] 88 + } 89 + False -> [line, prev, ..rest] 90 + } 91 + } 92 + #(line, _, remaining) -> 93 + case acc { 94 + [] -> do_unfold_lines(remaining, line_splitter, [line]) 95 + [prev, ..rest] -> 96 + case string.starts_with(line, " ") || string.starts_with(line, "\t") { 97 + True -> 98 + case string.pop_grapheme(line) { 99 + Ok(#(_, remaining_line)) -> 100 + do_unfold_lines(remaining, line_splitter, [ 101 + prev <> remaining_line, 102 + ..rest 103 + ]) 104 + Error(_) -> 105 + do_unfold_lines(remaining, line_splitter, [prev, ..acc]) 106 + } 107 + False -> 108 + do_unfold_lines(remaining, line_splitter, [line, prev, ..rest]) 109 + } 110 + } 111 + } 112 + } 113 + 69 114 fn parse_all_components( 70 115 lines: List(String), 71 116 acc: List(Component), 117 + splitters: Splitters, 72 118 ) -> Result(List(Component), Error) { 73 119 case lines { 74 120 [] -> Ok(list.reverse(acc)) 75 - ["BEGIN:" <> kind, ..rest] -> 76 - case parse_component(kind, rest, [], []) { 77 - Ok(#(component, remaining)) -> 78 - parse_all_components(remaining, [component, ..acc]) 79 - Error(err) -> Error(err) 121 + [line, ..rest] -> 122 + case splitter.split(splitters.begin, line) { 123 + #("", "BEGIN:", kind) -> 124 + case parse_component(kind, rest, [], [], splitters) { 125 + Ok(#(component, remaining)) -> 126 + parse_all_components(remaining, [component, ..acc], splitters) 127 + Error(err) -> Error(err) 128 + } 129 + _ -> Error(ParseError("Unexpected line: " <> line)) 80 130 } 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 + 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 - case string.starts_with(line, "BEGIN:") { 103 - True -> { 104 - let subkind = 105 - case string.split_once(line, "BEGIN:") { 106 - Ok(#(_, k)) -> k 107 - Error(_) -> "" 108 - } 109 - case parse_component(subkind, rest, [], []) { 152 + case splitter.split(splitters.begin, line) { 153 + #("", "BEGIN:", subkind) -> 154 + case parse_component(subkind, rest, [], [], splitters) { 110 155 Ok(#(child, remaining)) -> 111 - parse_component(kind, remaining, props, [child, ..children]) 156 + parse_component( 157 + kind, 158 + remaining, 159 + props, 160 + [child, ..children], 161 + splitters, 162 + ) 112 163 Error(err) -> Error(err) 113 164 } 114 - } 115 - False -> 116 - case parse_property(line) { 165 + _ -> 166 + case parse_property(line, splitters) { 117 167 Ok(prop) -> 118 - parse_component(kind, rest, [prop, ..props], children) 168 + parse_component( 169 + kind, 170 + rest, 171 + [prop, ..props], 172 + children, 173 + splitters, 174 + ) 119 175 Error(err) -> Error(err) 120 176 } 121 177 } ··· 123 179 } 124 180 } 125 181 126 - fn parse_property(line: String) -> Result(Property, Error) { 127 - case string.split_once(line, ":") { 128 - Ok(#(name_part, value)) -> { 129 - let #(name, params) = parse_name_and_params(name_part) 182 + fn parse_property( 183 + line: String, 184 + splitters: Splitters, 185 + ) -> Result(Property, Error) { 186 + case splitter.split(splitters.colon, line) { 187 + #(name_part, ":", value) -> { 188 + let #(name, params) = parse_name_and_params(name_part, splitters) 130 189 Ok(Property(name, params, unescape_text(value))) 131 190 } 132 - Error(_) -> Error(ParseError("Invalid property line: " <> line)) 191 + _ -> Error(ParseError("Invalid property line: " <> line)) 192 + } 193 + } 194 + 195 + fn parse_name_and_params( 196 + part: String, 197 + splitters: Splitters, 198 + ) -> #(String, List(Parameter)) { 199 + case splitter.split(splitters.semi, part) { 200 + #(name, ";", params_str) -> #(name, parse_params(params_str, splitters)) 201 + _ -> #(part, []) 133 202 } 134 203 } 135 204 136 - fn parse_name_and_params(part: String) -> #(String, List(Parameter)) { 137 - case string.split_once(part, ";") { 138 - Ok(#(name, params_str)) -> #(name, parse_params(params_str)) 139 - Error(_) -> #(part, []) 205 + fn parse_params(params_str: String, splitters: Splitters) -> List(Parameter) { 206 + do_parse_params(params_str, [], splitters) 207 + } 208 + 209 + fn do_parse_params( 210 + input: String, 211 + acc: List(Parameter), 212 + splitters: Splitters, 213 + ) -> List(Parameter) { 214 + case splitter.split(splitters.semi, input) { 215 + #(param, ";", remaining) -> 216 + case parse_single_param(param, splitters) { 217 + Ok(p) -> do_parse_params(remaining, [p, ..acc], splitters) 218 + Error(_) -> do_parse_params(remaining, acc, splitters) 219 + } 220 + #(param, "", "") -> 221 + case parse_single_param(param, splitters) { 222 + Ok(p) -> list.reverse([p, ..acc]) 223 + Error(_) -> list.reverse(acc) 224 + } 225 + _ -> list.reverse(acc) 140 226 } 141 227 } 142 228 143 - fn parse_params(params_str: String) -> List(Parameter) { 144 - params_str 145 - |> string.split(";") 146 - |> list.filter_map(fn(param) { 147 - case string.split_once(param, "=") { 148 - Ok(#(name, value)) -> Ok(Parameter(name, value)) 149 - Error(_) -> Error(Nil) 150 - } 151 - }) 229 + fn parse_single_param( 230 + param: String, 231 + splitters: Splitters, 232 + ) -> Result(Parameter, Error) { 233 + case splitter.split(splitters.eq, param) { 234 + #(name, "=", value) -> Ok(Parameter(name, value)) 235 + _ -> Error(ParseError("Invalid parameter: " <> param)) 236 + } 152 237 } 153 238 154 239 fn unescape_text(text: String) -> String {