//// Look at //// //// or //// //// for more information. //// //// The tags and attributes should mostly match the names here. import gleam/dict import gleam/int import gleam/list import gleam/option.{type Option as O, None, Some} import gleam/result.{try} import gleam/string import glrss_parser/rss/date import party import xmlm.{ type Input, type Tag, Data, Dtd, ElementEnd, ElementStart, Name, Tag, } as xm pub type Rss { Rss(version: String, channel: Channel) } pub fn empty_rss() { Rss(version: "", channel: empty_channel()) } pub type Channel { Channel( title: String, link: String, description: String, language: O(String), copyright: O(String), managing_editor: O(String), web_master: O(String), pub_date: O(date.RssDate), last_build_date: O(date.RssDate), category: O(Category), generator: O(String), docs: O(String), cloud: O(Cloud), ttl: O(Int), image: O(Image), rating: O(String), text_input: O(TextInput), skip_hours: List(Int), skip_days: List(date.DayOfWeek), item: List(Item), ) } fn empty_channel() { Channel( "", "", "", None, None, None, None, None, None, None, None, None, None, None, None, None, None, [], [], [], ) } pub type Cloud { Cloud( domain: String, port: Int, path: String, register_procedure: String, protocol: String, ) } // fn empty_cloud() { // Cloud("", 0, "", "", "") // } pub type Image { Image( url: String, title: String, link: String, width: O(Int), height: O(Int), description: O(String), ) } fn empty_image() { Image("", "", "", None, None, None) } pub type TextInput { TextInput(title: String, description: String, name: String, link: String) } fn empty_text_input() { TextInput("", "", "", "") } pub type Item { Item( title: O(String), link: O(String), description: O(String), author: O(String), category: O(Category), comments: O(String), enclosure: O(Enclosure), guid: O(String), pub_date: O(date.RssDate), source: O(Source), ) } fn empty_item() { Item(None, None, None, None, None, None, None, None, None, None) } pub type Source { Source(url: String, name: O(String)) } // fn empty_source() { // Source("", None) // } pub type Enclosure { Enclosure(url: String, length: Int, type_: String) } // fn empty_enclosure() { // Enclosure("", 0, "") // } /// pub type Category { Category(domain: O(String), categories: List(String)) } // fn empty_category() { // Category(None, []) // } type PErr = Nil pub type ParseError { UnexpectedSignal(xm.Signal) UnexpectedAttribute(List(xm.Attribute)) UnexpectedData(String) XmlmError(xm.InputError) PartyError(party.ParseError(PErr)) NilError } type ParseResult(a) = Result(#(a, Input), ParseError) pub fn parse(rss_xml: String, skip_unknown_tags skip) -> Result(Rss, ParseError) { let input = rss_xml |> xm.from_string() |> xm.with_stripping(True) do_parse(input, empty_rss(), skip) |> result.map(fn(a) { a.0 }) } pub fn parse_bits( rss_xml: BitArray, skip_unknown_tags skip, ) -> Result(Rss, ParseError) { let input = rss_xml |> xm.from_bit_array() |> xm.with_stripping(True) do_parse(input, empty_rss(), skip) |> result.map(fn(a) { a.0 }) } fn do_parse(in: Input, acc: Rss, skip) -> ParseResult(Rss) { case xm.signal(in) { Ok(#(xm.Dtd(_), in)) -> do_parse(in, acc, skip) _ -> { use in, attrs, uri, name <- handle_exceptions(in, acc, skip, do_parse) case uri, name { "", "rss" -> input_rss(in, attrs, skip) _, _ -> Error(NilError) } } } } fn input_rss(in, attrs, skip) -> ParseResult(Rss) { case attrs { [xm.Attribute(name: Name(uri: "", local: "version"), value: version)] -> { use #(done, in) <- result.map(do_input_rss(in, empty_rss(), skip)) #(Rss(..done, version:), in) } _ -> Error(UnexpectedAttribute(attrs)) } } fn do_input_rss(in, acc, skip) -> Result(#(Rss, Input), ParseError) { use in, _attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_rss) case uri, name { "", "channel" -> { use #(channel, in) <- result.try(input_channel(in, skip)) do_input_rss(in, Rss(..acc, channel:), skip) } _, _ -> Error(NilError) } } fn input_channel(in, skip) { do_input_channel(in, empty_channel(), skip) } fn do_input_channel(in, acc, skip) -> Result(#(Channel, Input), ParseError) { use in, attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_channel) case uri { "" -> case name { "title" -> { use #(title, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, title:), skip) } "link" -> { use #(link, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, link:), skip) } "description" -> { use #(description, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, description:), skip) } "language" -> { use #(language, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, language: Some(language)), skip) } "copyright" -> { use #(copyright, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, copyright: Some(copyright)), skip) } "managingEditor" -> { use #(managing_editor, in) <- result.try(read_data(in, skip)) do_input_channel( in, Channel(..acc, managing_editor: Some(managing_editor)), skip, ) } "webMaster" -> { use #(web_master, in) <- result.try(read_data(in, skip)) do_input_channel( in, Channel(..acc, web_master: Some(web_master)), skip, ) } "pubDate" -> { use #(pub_date, in) <- result.try(read_data(in, skip)) use pub_date <- result.try(date.parse(pub_date) |> party_error()) do_input_channel(in, Channel(..acc, pub_date: Some(pub_date)), skip) } "lastBuildDate" -> { use #(last_build_date, in) <- result.try(read_data(in, skip)) use last_build_date <- result.try( date.parse(last_build_date) |> party_error(), ) do_input_channel( in, Channel(..acc, last_build_date: Some(last_build_date)), skip, ) } "category" -> { use #(categories, in) <- result.try(read_data(in, skip)) let domain = case attrs { [xm.Attribute(name: Name(uri: "", local: "domain"), value: domain)] -> Some(domain) _ -> None } let categories = string.split(categories, on: "/") let category = Some(Category(domain:, categories:)) do_input_channel(in, Channel(..acc, category:), skip) } "generator" -> { use #(generator, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, generator: Some(generator)), skip) } "docs" -> { use #(docs, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, docs: Some(docs)), skip) } "cloud" -> { use #(_, in) <- result.try(read_data(in, skip)) let attrs_d = attrs |> list.map(fn(attr) { #(attr.name.local, attr.value) }) |> dict.from_list() let g = fn(a) { attrs_d |> dict.get(a) } case g("domain"), g("port"), g("path"), g("registerProcedure"), g("protocol") { Ok(domain), Ok(port), Ok(path), Ok(register_procedure), Ok(protocol) -> { use port <- result.try( int.parse(port) |> result.map_error(fn(_) { UnexpectedAttribute(attrs) }), ) let cloud = Some(Cloud( domain:, port:, path:, register_procedure:, protocol:, )) do_input_channel(in, Channel(..acc, cloud:), skip) } _, _, _, _, _ -> Error(UnexpectedAttribute(attrs)) } } "ttl" -> { use #(ttl, in) <- result.try(read_data(in, skip)) use ttl <- result.try( int.parse(ttl) |> result.map_error(fn(_) { UnexpectedData(ttl) }), ) do_input_channel(in, Channel(..acc, ttl: Some(ttl)), skip) } "image" -> { use #(image, in) <- result.try(input_image(in, skip)) do_input_channel(in, Channel(..acc, image: Some(image)), skip) } "rating" -> { use #(rating, in) <- result.try(read_data(in, skip)) do_input_channel(in, Channel(..acc, rating: Some(rating)), skip) } "textInput" -> { use #(text_input, in) <- result.try(input_text_input(in, skip)) do_input_channel( in, Channel(..acc, text_input: Some(text_input)), skip, ) } "skipHours" -> { use #(skip_hours, in) <- result.try(read_multiple(in, skip)) use skip_hours <- result.try( list.try_map(skip_hours, fn(a) { a |> int.parse() |> result.replace_error(a) }) |> result.map_error(UnexpectedData), ) do_input_channel(in, Channel(..acc, skip_hours:), skip) } "skipDays" -> { use #(skip_days, in) <- result.try(read_multiple(in, skip)) use skip_days <- result.try( list.try_map(skip_days, fn(a) { a |> date.dow_from() |> result.replace_error(a) }) |> result.map_error(UnexpectedData), ) do_input_channel(in, Channel(..acc, skip_days:), skip) } "item" -> { use #(item, in) <- result.try(input_item(in, skip)) do_input_channel(in, Channel(..acc, item: [item, ..acc.item]), skip) } _ -> Error(NilError) } _ -> Error(NilError) } } fn input_image(in, skip) { do_input_image(in, empty_image(), skip) } fn do_input_image(in, acc: Image, skip) { use in, _attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_image) case uri { "" -> case name { "url" -> { use #(url, in) <- result.try(read_data(in, skip)) do_input_image(in, Image(..acc, url:), skip) } "title" -> { use #(title, in) <- result.try(read_data(in, skip)) do_input_image(in, Image(..acc, title:), skip) } "link" -> { use #(link, in) <- result.try(read_data(in, skip)) do_input_image(in, Image(..acc, link:), skip) } "width" -> { use #(width, in) <- result.try(read_data(in, skip)) use width <- result.try( int.parse(width) |> result.map_error(fn(_) { UnexpectedData(width) }), ) do_input_image(in, Image(..acc, width: Some(width)), skip) } "height" -> { use #(height, in) <- result.try(read_data(in, skip)) use height <- result.try( int.parse(height) |> result.map_error(fn(_) { UnexpectedData(height) }), ) do_input_image(in, Image(..acc, height: Some(height)), skip) } "description" -> { use #(description, in) <- result.try(read_data(in, skip)) do_input_image(in, Image(..acc, description: Some(description)), skip) } _ -> Error(NilError) } _ -> Error(NilError) } } fn input_text_input(in, skip) { do_input_text_input(in, empty_text_input(), skip) } fn do_input_text_input(in, acc, skip) { use in, _attrs, uri, name <- handle_exceptions( in, acc, skip, do_input_text_input, ) case uri { "" -> case name { "title" -> { use #(title, in) <- try(read_data(in, skip)) do_input_text_input(in, TextInput(..acc, title:), skip) } "description" -> { use #(description, in) <- try(read_data(in, skip)) do_input_text_input(in, TextInput(..acc, description:), skip) } "name" -> { use #(name, in) <- try(read_data(in, skip)) do_input_text_input(in, TextInput(..acc, name:), skip) } "link" -> { use #(link, in) <- try(read_data(in, skip)) do_input_text_input(in, TextInput(..acc, link:), skip) } _ -> Error(NilError) } _ -> Error(NilError) } } fn input_item(in, skip) { do_input_item(in, empty_item(), skip) } fn do_input_item(in, acc, skip) { use in, attrs, uri, name <- handle_exceptions(in, acc, skip, do_input_item) case uri { "" -> case name { "title" -> { use #(title, in) <- try(read_data(in, skip)) do_input_item(in, Item(..acc, title: Some(title)), skip) } "link" -> { use #(link, in) <- try(read_data(in, skip)) do_input_item(in, Item(..acc, link: Some(link)), skip) } "description" -> { use #(description, in) <- try(read_data(in, skip)) do_input_item(in, Item(..acc, description: Some(description)), skip) } "author" -> { use #(author, in) <- try(read_data(in, skip)) do_input_item(in, Item(..acc, author: Some(author)), skip) } "category" -> { use #(categories, in) <- result.try(read_data(in, skip)) let domain = case attrs { [xm.Attribute(name: Name(uri: "", local: "domain"), value: domain)] -> Some(domain) _ -> None } let categories = string.split(categories, on: "/") let category = Some(Category(domain:, categories:)) do_input_item(in, Item(..acc, category:), skip) } "comments" -> { use #(comments, in) <- try(read_data(in, skip)) do_input_item(in, Item(..acc, comments: Some(comments)), skip) } "enclosure" -> { use in <- result.try(skip_unknown(in)) let attrs_d = attrs |> list.map(fn(attr) { #(attr.name.local, attr.value) }) |> dict.from_list() let g = fn(a) { attrs_d |> dict.get(a) } case g("url"), g("length"), g("type") { Ok(url), Ok(length), Ok(type_) -> { use length <- result.try( int.parse(length) |> result.map_error(fn(_) { UnexpectedAttribute(attrs) }), ) let enclosure = Some(Enclosure(url:, length:, type_:)) do_input_item(in, Item(..acc, enclosure:), skip) } _, _, _ -> Error(UnexpectedAttribute(attrs)) } } "guid" -> { use #(guid, in) <- try(read_data(in, skip)) do_input_item(in, Item(..acc, guid: Some(guid)), skip) } "pubDate" -> { use #(pub_date, in) <- try(read_data(in, skip)) use pub_date <- result.try(date.parse(pub_date) |> party_error()) do_input_item(in, Item(..acc, pub_date: Some(pub_date)), skip) } "source" -> { use #(name, in) <- try(read_data(in, skip)) let name = case name { "" -> None a -> Some(a) } case attrs { [xm.Attribute(name: Name(uri: "", local: "url"), value: url)] -> { let source = Source(url:, name:) do_input_item(in, Item(..acc, source: Some(source)), skip) } _ -> Error(UnexpectedAttribute(attrs)) } } _ -> Error(NilError) } _ -> Error(NilError) } } fn read_data(in, skip) { do_read_data(in, "", skip) } fn do_read_data(in, acc, skip) { case xm.signal(in) { Ok(#(xm.Data(acc), in)) -> do_read_data(in, acc, skip) Ok(#(ElementEnd, in)) -> Ok(#(acc, in)) Ok(#(sig, in)) -> handle_unknown(in, acc, skip, sig, do_read_data) Error(e) -> Error(XmlmError(e)) } } fn party_error(res: Result(a, party.ParseError(PErr))) { res |> result.map_error(fn(err) { PartyError(err) }) } fn handle_exceptions(in, acc, skip, next, fun) { case xm.signal(in) { Ok(#( ElementStart(Tag(Name(uri:, local: name), attributes: attrs)) as sig, in_1, )) -> case fun(in_1, attrs, uri, name) { Ok(acc) -> Ok(acc) Error(NilError) -> handle_unknown(in, acc, skip, sig, next) Error(err) -> Error(err) } Ok(#(ElementEnd, in)) -> Ok(#(acc, in)) Ok(#(Dtd(..) as sig, in)) | Ok(#(Data(..) as sig, in)) -> handle_unknown(in, acc, skip, sig, next) //handle_unknown(in, acc, skip, sig, do_read_data) Error(e) -> Error(XmlmError(e)) } } /// If skip is true, next. else, error fn handle_unknown(in, acc: a, skip, sig, next) { case skip { True -> result.try(skip_unknown(in), fn(in) { next(in, acc, skip) }) False -> Error(UnexpectedSignal(sig)) } } fn skip_unknown(in) { case xm.signal(in) { Ok(#(ElementEnd, in)) -> Ok(in) Ok(#(ElementStart(..), in)) -> result.try(skip_unknown(in), skip_unknown) Ok(#(_sig, in)) -> skip_unknown(in) Error(e) -> Error(XmlmError(e)) } } fn read_multiple(in, skip) { do_read_multiple(in, [], skip) } fn do_read_multiple(in, acc, skip) { case xm.signal(in) { Ok(#(sig, in)) -> case sig { ElementStart(..) -> { use #(data, in) <- result.try(read_data(in, skip)) do_read_multiple(in, [data, ..acc], skip) } ElementEnd -> Ok(#(acc, in)) _ -> handle_unknown(in, acc, skip, sig, do_read_multiple) } Error(e) -> Error(XmlmError(e)) } }