///|
fn rss_image_dimension(raw : String, maximum : Int32) -> Int32? {
let raw = raw.trim()
if raw.is_empty() {
return None
}
let mut value = 0
for character in raw {
let digit = character.to_int() - 48
if digit < 0 || digit > 9 || value > (maximum - digit) / 10 {
return None
}
value = value * 10 + digit
if value > maximum {
return None
}
}
if value == 0 {
None
} else {
Some(value)
}
}
///|
fn feed_image_problem(
diagnostics : Array[Diagnostic],
record_errors : Bool,
message : String,
line : Int32,
column : Int32,
) -> Unit {
if record_errors {
diagnostics.push({ kind: InvalidFeedImage(message), line, column, })
}
}
///|
fn atom_image_iri(
raw : String,
field : String,
line : Int32,
column : Int32,
diagnostics : Array[Diagnostic],
record_errors : Bool,
) -> Bool {
if !raw.is_empty() && raw.trim().is_empty() {
feed_image_problem(
diagnostics,
record_errors,
"atom " + field + " missing URL",
line,
column,
)
return false
}
if !atom_href_syntax(raw) {
feed_image_problem(
diagnostics,
record_errors,
"atom " + field + " invalid IRI",
line,
column,
)
}
true
}
///|
fn with_feed_images(feed : Feed, images : Array[FeedImage]) -> Feed {
{
format: feed.format,
identifier: feed.identifier,
title: feed.title,
title_kind: feed.title_kind,
title_xhtml: feed.title_xhtml,
link: feed.link,
language: feed.language,
description: feed.description,
description_kind: feed.description_kind,
description_xhtml: feed.description_xhtml,
updated_at: feed.updated_at,
published_at: feed.published_at,
ttl_minutes: feed.ttl_minutes,
docs_url: feed.docs_url,
rating: feed.rating,
cloud: feed.cloud,
text_input: feed.text_input,
skip_hours: feed.skip_hours,
skip_days: feed.skip_days,
rights: feed.rights,
rights_kind: feed.rights_kind,
rights_xhtml: feed.rights_xhtml,
generator: feed.generator,
generator_uri: feed.generator_uri,
generator_version: feed.generator_version,
managing_editor: feed.managing_editor,
webmaster: feed.webmaster,
next_url: feed.next_url,
expired: feed.expired,
hubs: feed.hubs,
links: feed.links,
authors: feed.authors,
contributors: feed.contributors,
categories: feed.categories,
images,
entries: feed.entries,
diagnostics: feed.diagnostics,
}
}
///|
/// Scan only feed-level visuals after the core XML parse. The scan is also
/// reused with a source URL so Atom icon/logo URI text gets its own xml:base
/// scope. Core fields and entries are never reinterpreted by this pass.
fn add_xml_feed_images(
input : String,
feed : Feed,
source_url : String?,
record_errors : Bool,
) -> Result[Feed, ParseError] {
let reader = @xml.NamespaceReader::from_string(input)
let images : Array[FeedImage] = []
let diagnostics = feed.diagnostics
let bases : Array[String?] = []
let base_scratch : Array[Diagnostic] = []
let mut depth = 0
let mut channel_depth = 0
let mut rss_image_seen = false
let mut atom_icon_seen = false
let mut atom_logo_seen = false
let mut rss_image_open = false
let mut image_depth = 0
let mut image_line = 1
let mut image_column = 1
let mut image_url : String? = None
let mut image_url_location : (Int32, Int32)? = None
let mut image_title : String? = None
let mut image_link : String? = None
let mut image_link_location : (Int32, Int32)? = None
let mut image_description : String? = None
let mut image_width : Int32? = None
let mut image_height : Int32? = None
let mut capture : String? = None
let mut capture_depth = 0
let mut capture_text = ""
let mut capture_nested = false
let mut capture_base : String? = None
let mut capture_line = 1
let mut capture_column = 1
try {
for ;; {
let event = reader.read_event()
match event.kind {
@xml.Start(element) => {
if capture is Some(_) && depth >= capture_depth {
capture_nested = true
}
let parent_base = if bases.is_empty() {
source_url
} else {
bases[bases.length() - 1]
}
let base = if source_url is Some(_) && feed.format == Atom1 {
xml_effective_base(
element,
parent_base,
Atom1,
event.span.start.line,
event.span.start.column,
base_scratch,
)
} else {
parent_base
}
if feed.format == Rss2 {
if depth == 1 && is_rss_name(element.name, "channel") {
channel_depth = depth + 1
} else if depth == channel_depth &&
channel_depth > 0 &&
is_rss_name(element.name, "image") {
if rss_image_seen {
feed_image_problem(
diagnostics,
record_errors,
"multiple rss channel image elements",
event.span.start.line,
event.span.start.column,
)
} else {
rss_image_seen = true
rss_image_open = true
image_depth = depth + 1
image_line = event.span.start.line
image_column = event.span.start.column
image_url = None
image_url_location = None
image_title = None
image_link = None
image_link_location = None
image_description = None
image_width = None
image_height = None
}
} else if rss_image_open &&
depth == image_depth &&
(
is_rss_name(element.name, "url") ||
is_rss_name(element.name, "title") ||
is_rss_name(element.name, "link") ||
is_rss_name(element.name, "description") ||
is_rss_name(element.name, "width") ||
is_rss_name(element.name, "height")
) {
capture = Some(element.name.local_name)
capture_depth = depth + 1
capture_text = ""
capture_nested = false
capture_base = base
capture_line = event.span.start.line
capture_column = event.span.start.column
if element.name.local_name == "url" {
image_url_location = Some((capture_line, capture_column))
} else if element.name.local_name == "link" {
image_link_location = Some((capture_line, capture_column))
}
}
} else if depth == 1 &&
(
is_atom_name(element.name, "icon") ||
is_atom_name(element.name, "logo")
) {
let is_icon = element.name.local_name == "icon"
if (is_icon && atom_icon_seen) || (!is_icon && atom_logo_seen) {
feed_image_problem(
diagnostics,
record_errors,
"duplicate atom " + element.name.local_name,
event.span.start.line,
event.span.start.column,
)
} else {
if is_icon {
atom_icon_seen = true
} else {
atom_logo_seen = true
}
capture = Some(element.name.local_name)
capture_depth = depth + 1
capture_text = ""
capture_nested = false
capture_base = base
capture_line = event.span.start.line
capture_column = event.span.start.column
}
}
bases.push(base)
depth = depth + 1
}
@xml.Empty(element) =>
if capture is Some(_) && depth >= capture_depth {
capture_nested = true
} else if feed.format == Rss2 &&
depth == channel_depth &&
channel_depth > 0 &&
is_rss_name(element.name, "image") {
if !rss_image_seen {
rss_image_seen = true
feed_image_problem(
diagnostics,
record_errors,
"rss image missing url/title/link",
event.span.start.line,
event.span.start.column,
)
} else {
feed_image_problem(
diagnostics,
record_errors,
"multiple rss channel image elements",
event.span.start.line,
event.span.start.column,
)
}
} else if feed.format == Rss2 &&
rss_image_open &&
depth == image_depth &&
(
is_rss_name(element.name, "width") ||
is_rss_name(element.name, "height")
) {
feed_image_problem(
diagnostics,
record_errors,
"rss image invalid " + element.name.local_name,
event.span.start.line,
event.span.start.column,
)
} else if feed.format == Atom1 &&
depth == 1 &&
(
is_atom_name(element.name, "icon") ||
is_atom_name(element.name, "logo")
) {
let is_icon = element.name.local_name == "icon"
if (is_icon && atom_icon_seen) || (!is_icon && atom_logo_seen) {
feed_image_problem(
diagnostics,
record_errors,
"duplicate atom " + element.name.local_name,
event.span.start.line,
event.span.start.column,
)
} else {
if is_icon {
atom_icon_seen = true
} else {
atom_logo_seen = true
}
let parent_base = if bases.is_empty() {
source_url
} else {
bases[bases.length() - 1]
}
let base = if source_url is Some(_) {
xml_effective_base(
element,
parent_base,
Atom1,
event.span.start.line,
event.span.start.column,
base_scratch,
)
} else {
parent_base
}
let raw = ""
images.push({
source: Atom1,
role: if is_icon {
AtomIcon
} else {
AtomLogo
},
url: if source_url is Some(_) {
uri_at(
base,
raw,
"atom " + element.name.local_name,
event.span.start.line,
event.span.start.column,
diagnostics,
)
} else {
raw
},
title: None,
link: None,
description: None,
width: None,
height: None,
})
}
}
@xml.Text(text) | @xml.CData(text) =>
if capture is Some(_) {
capture_text = capture_text + text
}
@xml.End(name) => {
if capture is Some(field) &&
capture_depth == depth &&
(
(feed.format == Rss2 && is_rss_name(name, field)) ||
(feed.format == Atom1 && is_atom_name(name, field))
) {
if capture_nested {
if record_errors {
diagnostics.push({
kind: UnsupportedNestedMarkup("feed image " + field),
line: capture_line,
column: capture_column,
})
}
} else if feed.format == Rss2 {
match field {
"url" => image_url = non_blank_text(capture_text)
"title" => image_title = non_blank_text(capture_text)
"link" => image_link = non_blank_text(capture_text)
"description" =>
image_description = non_blank_text(capture_text)
"width" | "height" => {
let maximum = if field == "width" { 144 } else { 400 }
let dimension = rss_image_dimension(capture_text, maximum)
if dimension is None {
feed_image_problem(
diagnostics,
record_errors,
"rss image invalid " + field,
capture_line,
capture_column,
)
}
if field == "width" {
image_width = dimension
} else {
image_height = dimension
}
}
_ => ()
}
} else if atom_image_iri(
capture_text, field, capture_line, capture_column, diagnostics, record_errors,
) {
let url = match source_url {
Some(_) =>
uri_at(
capture_base,
capture_text,
"atom " + field,
capture_line,
capture_column,
diagnostics,
)
None => capture_text
}
images.push({
source: Atom1,
role: if field == "icon" {
AtomIcon
} else {
AtomLogo
},
url,
title: None,
link: None,
description: None,
width: None,
height: None,
})
}
capture = None
capture_text = ""
capture_nested = false
}
if feed.format == Rss2 &&
rss_image_open &&
depth == image_depth &&
is_rss_name(name, "image") {
match (image_url, image_title, image_link) {
(Some(url), Some(title), Some(link)) => {
let (url_line, url_column) = image_url_location.unwrap_or(
(image_line, image_column),
)
let (link_line, link_column) = image_link_location.unwrap_or(
(image_line, image_column),
)
let (url, link) = match source_url {
Some(base) =>
(
uri_at(
Some(base),
url,
"rss image url",
url_line,
url_column,
diagnostics,
),
uri_at(
Some(base),
link,
"rss image link",
link_line,
link_column,
diagnostics,
),
)
None => (url, link)
}
images.push({
source: Rss2,
role: RssImage,
url,
title: Some(title),
link: Some(link),
description: image_description,
width: image_width,
height: image_height,
})
}
_ =>
feed_image_problem(
diagnostics, record_errors, "rss image missing url/title/link",
image_line, image_column,
)
}
rss_image_open = false
}
depth = depth - 1
ignore(bases.pop())
}
@xml.Eof => return Ok(with_feed_images(feed, images))
_ => ()
}
}
abort("unreachable")
} catch {
@xml.At(error~, span~) =>
Err(InvalidXml(error.to_string(), span.start.line, span.start.column))
}
}