///|
fn markup(node : Node) -> Markup {
let attributes = node.attributes.map(a => MarkupAttribute::{
namespace_uri: a.name.namespace_uri,
local_name: a.name.local_name,
value: a.value,
})
let content = node.content.map(part => {
match part {
PartText(value) => MarkupContent::Text(value)
PartElement(child) => MarkupContent::Element(markup(child))
}
})
{
namespace_uri: node.name.namespace_uri,
local_name: node.name.local_name,
attributes,
content,
base_uri: node.base_uri,
language: node.language,
}
}
///|
fn raw_attr(node : Node, key : String) -> String? {
for a in node.attributes {
if a.name.namespace_uri is None && a.name.local_name == key {
return Some(a.value)
}
}
None
}
///|
fn needed_attr(
node : Node,
key : String,
path : String,
) -> String raise ParseError {
match raw_attr(node, key) {
Some(value) => value
None => raise InvalidRequiredField(path + "." + key, "missing attribute")
}
}
///|
fn scalar(node : Node, path : String) -> String raise ParseError {
if !node.children.is_empty() {
raise InvalidRequiredField(path, "child elements are not allowed")
}
node.text.to_string()
}
///|
fn descendant_text(node : Node) -> String {
let out = StringBuilder()
for part in node.content {
match part {
PartText(s) => out.write_string(s)
PartElement(child) => out.write_string(descendant_text(child))
}
}
out.to_string()
}
///|
fn required_node(
parent : Node,
name : String,
path : String,
) -> Node raise ParseError {
match one(parent, name, path) {
Some(node) => node
None => raise InvalidRequiredField(path + "." + name, "missing element")
}
}
///|
fn reference(
node : Node,
value : String,
path : String,
) -> IriReference raise ParseError {
resolve_reference(node.base_uri, value) catch {
_ => raise InvalidRequiredField(path, "invalid IRI reference")
}
}
///|
fn absolute_iri(value : String, path : String) -> String raise ParseError {
let parsed = iri_parts(value) catch {
_ => raise InvalidRequiredField(path, "invalid absolute IRI")
}
if parsed.scheme is None {
raise InvalidRequiredField(path, "absolute IRI required")
}
// IDs are not base-resolved, case-folded, percent-decoded or normalized.
value
}
///|
fn date_value(node : Node, path : String) -> AtomDate raise ParseError {
let value = scalar(node, path)
let a = value.to_array()
if a.length() < 20 || a[10] != 'T' || value.contains("z") {
raise InvalidRequiredField(
path, "Atom dates require uppercase T/Z and RFC 3339 syntax",
)
}
atom_date(value, path)
}
///|
fn xhtml_div(node : Node, path : String) -> Node raise ParseError {
if node.children.length() != 1 ||
!node.text.to_string().trim(chars=" \t\r\n").is_empty() {
raise InvalidRequiredField(path, "expected one XHTML div")
}
let div = node.children[0]
if div.name.namespace_uri != Some("http://www.w3.org/1999/xhtml") ||
div.name.local_name != "div" {
raise InvalidRequiredField(path, "expected XHTML namespace div")
}
div
}
///|
fn strict_text(node : Node, path : String) -> AtomText raise ParseError {
let kind = raw_attr(node, "type").unwrap_or("text")
let (value, xhtml) = match kind {
"text" | "html" => (scalar(node, path), None)
"xhtml" => {
let div = xhtml_div(node, path)
(descendant_text(div), Some(markup(div)))
}
_ =>
raise InvalidRequiredField(
path + ".type",
"text construct type must be text, html or xhtml",
)
}
{ kind, value, xhtml, language: node.language, base_uri: node.base_uri, }
}
///|
fn optional_construct(
parent : Node,
name : String,
path : String,
) -> AtomText? raise ParseError {
match one(parent, name, path) {
Some(node) => Some(strict_text(node, path + "." + name))
None => None
}
}
///|
fn media_type(value : String, path : String) -> String raise ParseError {
parse_media_type(value, path).essence
}
///|
fn strict_person(node : Node, path : String) -> AtomPerson raise ParseError {
container_structure(node, "person", path)
let name = scalar(required_node(node, "name", path), path + ".name")
let uri = match one(node, "uri", path) {
Some(n) => Some(reference(n, scalar(n, path + ".uri"), path + ".uri"))
None => None
}
let email = match one(node, "email", path) {
Some(n) => {
let value = scalar(n, path + ".email")
validate_email(value, path + ".email")
Some(value)
}
None => None
}
{ name, uri, email, raw: markup(node), }
}
///|
fn strict_people(
node : Node,
name : String,
path : String,
) -> Array[AtomPerson] raise ParseError {
let result = []
for i, person in atom_children(node, name) {
result.push(strict_person(person, path + "." + name + "[\{i}]"))
}
result
}
///|
fn is_alternate(rel : String) -> Bool {
rel == "alternate" ||
rel == "http://www.iana.org/assignments/relation/alternate"
}
///|
fn strict_links(node : Node, path : String) -> Array[AtomLink] raise ParseError {
let result : Array[AtomLink] = []
for i, n in atom_children(node, "link") {
let p = path + ".link[\{i}]"
let href = reference(n, needed_attr(n, "href", p), p + ".href")
let rel = raw_attr(n, "rel").unwrap_or("alternate")
let parts = iri_parts(rel) catch {
_ => raise InvalidRequiredField(p + ".rel", "invalid relation")
}
if rel.is_empty() ||
(
parts.scheme is None &&
(rel.contains("/") || rel.contains("?") || rel.contains("#"))
) {
raise InvalidRequiredField(
p + ".rel",
"relation must be a simple name or absolute IRI",
)
}
let type_ = raw_attr(n, "type")
if type_ is Some(value) {
let _ = media_type(value, p + ".type")
}
let hreflang = raw_attr(n, "hreflang")
if hreflang is Some(value) {
validate_language(value, p + ".hreflang", false)
}
for previous in result {
if is_alternate(rel) &&
is_alternate(previous.rel) &&
same_media_type(previous.media_type, type_, p + ".type") &&
previous.hreflang.map(s => s.to_lower()) ==
hreflang.map(s => s.to_lower()) {
raise InvalidRequiredField(
p, "duplicate alternate type/hreflang combination",
)
}
}
let length = match raw_attr(n, "length") {
Some(value) => {
if value.is_empty() || !value.to_array().all(ascii_digit) {
raise InvalidRequiredField(
p + ".length",
"expected nonnegative decimal integer",
)
}
Some(@string.parse_uint64(value)) catch {
_ => None
}
}
None => None
}
result.push({
href,
rel,
media_type: type_,
hreflang,
title: raw_attr(n, "title"),
length,
length_raw: raw_attr(n, "length"),
raw: markup(n),
})
}
result
}
///|
fn strict_categories(
node : Node,
path : String,
) -> Array[AtomCategory] raise ParseError {
let result = []
for i, n in atom_children(node, "category") {
let p = path + ".category[\{i}]"
let scheme = match raw_attr(n, "scheme") {
Some(s) => {
let _ = absolute_iri(s, p + ".scheme")
Some(reference(n, s, p + ".scheme"))
}
None => None
}
result.push({
term: needed_attr(n, "term", p),
scheme,
label: raw_attr(n, "label"),
raw: markup(n),
})
}
result
}
///|
fn metadata(
node : Node,
path : String,
required : Bool,
) -> AtomMetadata raise ParseError {
let id = match one(node, "id", path) {
Some(n) => Some(absolute_iri(scalar(n, path + ".id"), path + ".id"))
None =>
if required {
raise InvalidRequiredField(path + ".id", "missing element")
} else {
None
}
}
let title = optional_construct(node, "title", path)
let updated = match one(node, "updated", path) {
Some(n) => Some(date_value(n, path + ".updated"))
None => None
}
if required && title is None {
raise InvalidRequiredField(path + ".title", "missing element")
}
if required && updated is None {
raise InvalidRequiredField(path + ".updated", "missing element")
}
let generator = match one(node, "generator", path) {
Some(n) =>
Some(AtomGenerator::{
value: scalar(n, path + ".generator"),
uri: match raw_attr(n, "uri") {
Some(v) => Some(reference(n, v, path + ".generator.uri"))
None => None
},
version: raw_attr(n, "version"),
})
None => None
}
let icon = match one(node, "icon", path) {
Some(n) => Some(reference(n, scalar(n, path + ".icon"), path + ".icon"))
None => None
}
let logo = match one(node, "logo", path) {
Some(n) => Some(reference(n, scalar(n, path + ".logo"), path + ".logo"))
None => None
}
{
id,
title,
updated,
authors: strict_people(node, "author", path),
contributors: strict_people(node, "contributor", path),
categories: strict_categories(node, path),
links: strict_links(node, path),
rights: optional_construct(node, "rights", path),
subtitle: optional_construct(node, "subtitle", path),
generator,
icon,
logo,
raw: markup(node),
}
}
///|
fn binary_media(essence : String) -> Bool {
essence != "text" &&
essence != "html" &&
essence != "xhtml" &&
!essence.has_prefix("text/") &&
!essence.has_suffix("/xml") &&
!essence.has_suffix("+xml")
}
///|
fn decode_base64(value : String, path : String) -> Array[Byte] raise ParseError {
let a = value
.to_array()
.filter(c => c != ' ' && c != '\t' && c != '\r' && c != '\n')
if a.length() % 4 != 0 {
raise InvalidRequiredField(path, "invalid Base64 length")
}
let mut padding = 0
for i, c in a {
if c == '=' {
padding += 1
if i < a.length() - 2 || padding > 2 {
raise InvalidRequiredField(path, "invalid Base64 padding")
}
} else if padding > 0 ||
(!ascii_alpha(c) && !ascii_digit(c) && c != '+' && c != '/') {
raise InvalidRequiredField(path, "invalid Base64 character")
}
}
let result : Array[Byte] = []
let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".to_array()
let values = a.map(c => {
let mut value = 0
for i, symbol in alphabet {
if c == symbol {
value = i
break
}
}
value
})
for i = 0; i < a.length(); i = i + 4 {
result.push((values[i] * 4 + values[i + 1] / 16).to_byte())
if a[i + 2] != '=' {
result.push((values[i + 1] % 16 * 16 + values[i + 2] / 4).to_byte())
}
if a[i + 3] != '=' {
result.push((values[i + 2] % 4 * 64 + values[i + 3]).to_byte())
}
}
result
}
///|
fn strict_content(node : Node, path : String) -> AtomContent raise ParseError {
let src = match raw_attr(node, "src") {
Some(v) => Some(reference(node, v, path + ".src"))
None => None
}
let kind = raw_attr(node, "type").unwrap_or(
if src is Some(_) {
"application/octet-stream"
} else {
"text"
},
)
let essence = if kind == "text" || kind == "html" || kind == "xhtml" {
kind
} else {
media_type(kind, path + ".type")
}
if essence.has_prefix("multipart/") || essence.has_prefix("message/") {
raise InvalidRequiredField(
path + ".type",
"composite content media types are not allowed",
)
}
let value = if src is Some(_) {
if !node.content.is_empty() {
raise InvalidRequiredField(path, "content with src must be empty")
}
if kind == "text" || kind == "html" || kind == "xhtml" {
raise InvalidRequiredField(
path + ".type",
"src requires a MIME media type",
)
}
""
} else if essence == "xhtml" {
descendant_text(xhtml_div(node, path))
} else if essence == "text" ||
essence == "html" ||
essence.has_prefix("text/") {
scalar(node, path)
} else if binary_media(essence) {
let v = scalar(node, path)
v
} else {
descendant_text(node)
}
{
media_type: if src is Some(_) {
raw_attr(node, "type")
} else {
Some(kind)
},
value,
decoded_bytes: if src is None && binary_media(essence) {
Some(decode_base64(value, path))
} else {
None
},
src,
markup: if src is None && essence == "xhtml" {
markup(xhtml_div(node, path)).content
} else {
markup(node).content
},
raw: markup(node),
language: node.language,
base_uri: node.base_uri,
}
}
///|
fn known_atom_name(name : String) -> Bool {
match name {
"feed"
| "entry"
| "id"
| "title"
| "updated"
| "published"
| "author"
| "contributor"
| "category"
| "link"
| "rights"
| "subtitle"
| "summary"
| "content"
| "source"
| "generator"
| "icon"
| "logo"
| "name"
| "uri"
| "email" => true
_ => false
}
}
///|
fn container_structure(
node : Node,
kind : String,
path : String,
) -> Unit raise ParseError {
if !node.text.to_string().trim(chars=" \t\r\n").is_empty() {
raise InvalidRequiredField(
path, "container must not contain non-whitespace text",
)
}
let mut saw_entry = false
for child in node.children {
if child.name.namespace_uri != Some("http://www.w3.org/2005/Atom") {
continue
}
let name = child.name.local_name
if !known_atom_name(name) {
continue
} // forward-compatible Atom vocabulary
let allowed = if kind == "person" {
name == "name" || name == "uri" || name == "email"
} else {
match name {
"id"
| "title"
| "updated"
| "author"
| "contributor"
| "category"
| "link"
| "rights" => true
"entry" => kind == "feed"
"published" | "source" | "summary" | "content" => kind == "entry"
"subtitle" | "generator" | "icon" | "logo" => kind != "entry"
_ => false
}
}
if !allowed {
raise InvalidRequiredField(
path + "." + name,
"element is not allowed in this container",
)
}
if kind == "feed" {
if name == "entry" {
saw_entry = true
} else if saw_entry {
raise InvalidRequiredField(
path + "." + name,
"feed metadata must precede entries",
)
}
}
}
}
///|
fn strict_entry(
node : Node,
path : String,
inherited_authors : Array[AtomPerson],
inherited_rights : AtomText?,
) -> AtomEntry raise ParseError {
container_structure(node, "entry", path)
let meta = metadata(node, path, true)
let source = match one(node, "source", path) {
Some(n) => {
container_structure(n, "source", path + ".source")
Some(metadata(n, path + ".source", false))
}
None => None
}
let authors = if !meta.authors.is_empty() {
meta.authors
} else if source is Some(s) && !s.authors.is_empty() {
s.authors
} else {
inherited_authors
}
if authors.is_empty() {
raise InvalidRequiredField(
path + ".author",
"author required on entry, source or containing feed",
)
}
let published = match one(node, "published", path) {
Some(n) => Some(date_value(n, path + ".published"))
None => None
}
let summary = optional_construct(node, "summary", path)
let content = match one(node, "content", path) {
Some(n) => Some(strict_content(n, path + ".content"))
None => None
}
if content is None && !meta.links.any(l => is_alternate(l.rel)) {
raise InvalidRequiredField(
path + ".link",
"entry without content needs an alternate link",
)
}
if content is Some(c) {
let raw_kind = c.media_type.unwrap_or("text")
let kind = if raw_kind == "text" ||
raw_kind == "html" ||
raw_kind == "xhtml" {
raw_kind
} else {
media_type(raw_kind, path + ".content.type")
}
if (c.src is Some(_) || binary_media(kind)) && summary is None {
raise InvalidRequiredField(
path + ".summary",
"summary required for external or Base64 content",
)
}
}
{
metadata: meta,
published,
summary,
content,
source,
effective_authors: authors,
effective_rights: match meta.rights {
Some(r) => Some(r)
None => inherited_rights
},
}
}
///|
fn strict_document(
input : String,
base : String?,
) -> AtomDocument raise ParseError {
if base is Some(b) {
let _ = absolute_iri(b, "document_uri")
}
let root = document_tree(input, base, strict_declaration=true)
validate_document_languages(root)
if named(root, "entry") {
return EntryDocument(strict_entry(root, "entry", [], None))
}
if !named(root, "feed") {
raise InvalidAtomRoot(root.name.local_name)
}
container_structure(root, "feed", "feed")
let meta = metadata(root, "feed", true)
let entries = []
for i, n in atom_children(root, "entry") {
let entry = strict_entry(n, "feed.entry[\{i}]", meta.authors, meta.rights)
if meta.authors.is_empty() && entry.metadata.authors.is_empty() {
raise InvalidRequiredField(
"feed.author", "feed author required unless every entry has its own author",
)
}
entries.push(entry)
}
FeedDocument({ metadata: meta, entries, })
}
///|
/// Strict Atom document API. Feed and standalone Entry retain distinct types.
pub fn parse_document(input : String) -> AtomDocument raise ParseError {
strict_document(input, None)
}
///|
/// Use the retrieval URI when XML Base does not supply an absolute base.
pub fn parse_document_with_base(
input : String,
document_uri : String,
) -> AtomDocument raise ParseError {
strict_document(input, Some(document_uri))
}