#[cfg(not(feature = "std"))]
use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use crate::{bzz_new::bzz_decode, error::BzzError};
#[derive(Debug, thiserror::Error)]
pub enum AnnotationError {
#[error("bzz decode failed: {0}")]
Bzz(#[from] BzzError),
#[error("invalid color value: {0}")]
InvalidColor(String),
#[error("invalid number: {0}")]
InvalidNumber(String),
#[error("malformed s-expression: {0}")]
Parse(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Color {
pub r: u8,
pub g: u8,
pub b: u8,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Shape {
Rect(Rect),
Oval(Rect),
Poly(Vec<(u32, u32)>),
Line(u32, u32, u32, u32),
Text(Rect),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Border {
pub style: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Highlight {
pub color: Color,
}
#[derive(Debug, Clone)]
pub struct MapArea {
pub url: String,
pub description: String,
pub shape: Shape,
pub border: Option<Border>,
pub highlight: Option<Highlight>,
}
#[derive(Debug, Clone, Default)]
pub struct Annotation {
pub background: Option<Color>,
pub zoom: Option<u32>,
pub mode: Option<String>,
}
pub fn parse_annotations(data: &[u8]) -> Result<(Annotation, Vec<MapArea>), AnnotationError> {
let text = core::str::from_utf8(data).unwrap_or("");
parse_annotation_text(text)
}
pub fn parse_annotations_bzz(data: &[u8]) -> Result<(Annotation, Vec<MapArea>), AnnotationError> {
let decoded = bzz_decode(data)?;
let text = core::str::from_utf8(&decoded).unwrap_or("");
parse_annotation_text(text)
}
#[derive(Debug, PartialEq)]
enum Token<'a> {
LParen,
RParen,
Atom(&'a str),
Quoted(String),
}
fn tokenize(input: &str) -> Vec<Token<'_>> {
let mut tokens = Vec::new();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
match bytes.get(i) {
Some(b'(') => {
tokens.push(Token::LParen);
i += 1;
}
Some(b')') => {
tokens.push(Token::RParen);
i += 1;
}
Some(b'"') => {
i += 1;
let start = i;
let mut s = String::new();
while i < bytes.len() {
match bytes.get(i) {
Some(b'\\') if i + 1 < bytes.len() => {
i += 1;
if let Some(&c) = bytes.get(i) {
s.push(c as char);
}
i += 1;
}
Some(b'"') => {
i += 1;
break;
}
Some(&c) => {
s.push(c as char);
i += 1;
}
None => break,
}
}
let _ = start; tokens.push(Token::Quoted(s));
}
Some(b' ') | Some(b'\t') | Some(b'\n') | Some(b'\r') => {
i += 1;
}
Some(b';') => {
while i < bytes.len() && bytes.get(i) != Some(&b'\n') {
i += 1;
}
}
_ => {
let start = i;
while i < bytes.len() {
match bytes.get(i) {
Some(b'(') | Some(b')') | Some(b'"') | Some(b' ') | Some(b'\t')
| Some(b'\n') | Some(b'\r') => break,
_ => i += 1,
}
}
if let Some(slice) = input.get(start..i)
&& !slice.is_empty()
{
tokens.push(Token::Atom(slice));
}
}
}
}
tokens
}
#[derive(Debug)]
enum SExpr {
Atom(String),
List(Vec<SExpr>),
}
fn parse_sexprs(tokens: &[Token<'_>]) -> Vec<SExpr> {
let mut result = Vec::new();
let mut pos = 0usize;
while pos < tokens.len() {
if let Some(expr) = parse_one(tokens, &mut pos) {
result.push(expr);
}
}
result
}
fn parse_one(tokens: &[Token<'_>], pos: &mut usize) -> Option<SExpr> {
match tokens.get(*pos) {
Some(Token::LParen) => {
*pos += 1;
let mut items = Vec::new();
loop {
match tokens.get(*pos) {
Some(Token::RParen) => {
*pos += 1;
break;
}
None => break,
_ => {
if let Some(child) = parse_one(tokens, pos) {
items.push(child);
} else {
break;
}
}
}
}
Some(SExpr::List(items))
}
Some(Token::RParen) => {
*pos += 1;
None
}
Some(Token::Atom(s)) => {
let s = s.to_string();
*pos += 1;
Some(SExpr::Atom(s))
}
Some(Token::Quoted(s)) => {
let s = s.clone();
*pos += 1;
Some(SExpr::Atom(s))
}
None => None,
}
}
fn parse_annotation_text(text: &str) -> Result<(Annotation, Vec<MapArea>), AnnotationError> {
if text.trim().is_empty() {
return Ok((Annotation::default(), Vec::new()));
}
let tokens = tokenize(text);
let exprs = parse_sexprs(&tokens);
let mut annotation = Annotation::default();
let mut mapareas = Vec::new();
for expr in &exprs {
if let SExpr::List(items) = expr {
let head = match items.first() {
Some(SExpr::Atom(s)) => s.as_str(),
_ => continue,
};
match head {
"background" => {
if let Some(SExpr::Atom(color_str)) = items.get(1) {
annotation.background = Some(parse_color(color_str)?);
}
}
"zoom" => {
if let Some(SExpr::Atom(n)) = items.get(1) {
annotation.zoom = Some(parse_uint(n)?);
}
}
"mode" => {
if let Some(SExpr::Atom(m)) = items.get(1) {
annotation.mode = Some(m.clone());
}
}
"maparea" => {
if let Some(ma) = parse_maparea(items)? {
mapareas.push(ma);
}
}
_ => {} }
}
}
Ok((annotation, mapareas))
}
fn parse_maparea(items: &[SExpr]) -> Result<Option<MapArea>, AnnotationError> {
let url = match items.get(1) {
Some(SExpr::Atom(s)) => s.clone(),
_ => String::new(),
};
let description = match items.get(2) {
Some(SExpr::Atom(s)) => s.clone(),
_ => String::new(),
};
let shape_expr = match items.get(3) {
Some(SExpr::List(l)) => l,
_ => return Ok(None),
};
let shape = parse_shape(shape_expr)?;
let mut border = None;
let mut highlight = None;
for item in items.get(4..).unwrap_or(&[]) {
if let SExpr::List(opts) = item {
match opts.first() {
Some(SExpr::Atom(s)) if s == "border" => {
if let Some(SExpr::Atom(style)) = opts.get(1) {
border = Some(Border {
style: style.clone(),
});
}
}
Some(SExpr::Atom(s)) if s == "hilite" => {
if let Some(SExpr::Atom(color)) = opts.get(1) {
highlight = Some(Highlight {
color: parse_color(color)?,
});
}
}
_ => {}
}
}
}
Ok(Some(MapArea {
url,
description,
shape,
border,
highlight,
}))
}
fn parse_shape(items: &[SExpr]) -> Result<Shape, AnnotationError> {
let kind = match items.first() {
Some(SExpr::Atom(s)) => s.as_str(),
_ => return Err(AnnotationError::Parse("shape has no kind".to_string())),
};
match kind {
"rect" => {
let x = get_uint(items, 1)?;
let y = get_uint(items, 2)?;
let w = get_uint(items, 3)?;
let h = get_uint(items, 4)?;
Ok(Shape::Rect(Rect {
x,
y,
width: w,
height: h,
}))
}
"oval" => {
let x = get_uint(items, 1)?;
let y = get_uint(items, 2)?;
let w = get_uint(items, 3)?;
let h = get_uint(items, 4)?;
Ok(Shape::Oval(Rect {
x,
y,
width: w,
height: h,
}))
}
"text" => {
let x = get_uint(items, 1)?;
let y = get_uint(items, 2)?;
let w = get_uint(items, 3)?;
let h = get_uint(items, 4)?;
Ok(Shape::Text(Rect {
x,
y,
width: w,
height: h,
}))
}
"line" => {
let x1 = get_uint(items, 1)?;
let y1 = get_uint(items, 2)?;
let x2 = get_uint(items, 3)?;
let y2 = get_uint(items, 4)?;
Ok(Shape::Line(x1, y1, x2, y2))
}
"poly" => {
let mut pts = Vec::new();
let mut i = 1usize;
while i + 1 < items.len() {
let x = get_uint(items, i)?;
let y = get_uint(items, i + 1)?;
pts.push((x, y));
i += 2;
}
Ok(Shape::Poly(pts))
}
other => Err(AnnotationError::Parse(format!(
"unknown shape kind: {other}"
))),
}
}
fn get_uint(items: &[SExpr], idx: usize) -> Result<u32, AnnotationError> {
match items.get(idx) {
Some(SExpr::Atom(s)) => parse_uint(s),
_ => Err(AnnotationError::Parse(format!(
"expected uint at position {idx}"
))),
}
}
fn parse_uint(s: &str) -> Result<u32, AnnotationError> {
s.parse::<u32>()
.map_err(|_| AnnotationError::InvalidNumber(s.to_string()))
}
fn parse_color(s: &str) -> Result<Color, AnnotationError> {
let hex = s.strip_prefix('#').unwrap_or(s);
if hex.len() != 6 {
return Err(AnnotationError::InvalidColor(s.to_string()));
}
let r = u8::from_str_radix(&hex[0..2], 16)
.map_err(|_| AnnotationError::InvalidColor(s.to_string()))?;
let g = u8::from_str_radix(&hex[2..4], 16)
.map_err(|_| AnnotationError::InvalidColor(s.to_string()))?;
let b = u8::from_str_radix(&hex[4..6], 16)
.map_err(|_| AnnotationError::InvalidColor(s.to_string()))?;
Ok(Color { r, g, b })
}