#![no_std]
#![cfg_attr(feature = "nightly", feature(error_in_core))]
use core::ops::Range;
use core::fmt;
#[cfg(feature = "nightly")]
use core::error::Error;
#[cfg(feature = "std")]
extern crate std;
#[cfg(all(feature = "std", not(feature = "nightly")))]
use std::error::Error;
#[cfg(feature = "read")]
pub mod read;
const OSC8: &str = "\x1b]8";
const ST: &str = "\x1b\\";
#[derive(Default, Debug, PartialEq, Clone)]
pub struct Hyperlink<'a>{
url: &'a str,
id: Option<&'a str>,
}
impl fmt::Display for Hyperlink<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let url = self.url;
if f.alternate() {
write!(f, "{OSC8};;{ST}")
} else if let Some(id) = self.id {
write!(f, "{OSC8};id={id};{url}{ST}")
} else {
write!(f, "{OSC8};;{url}{ST}")
}
}
}
#[derive(Debug, PartialEq)]
pub enum ParseError{
Partial,
MissingSemi,
MissingEq,
}
#[cfg(any(feature = "std", feature = "nightly"))]
impl Error for ParseError {}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use ParseError::*;
f.write_str(match self {
Partial => "incomplete OSC 8 hyperlink",
MissingSemi => "malformed OSC 8 hyperlink: missing semicolon",
MissingEq =>
"malformed OSC 8 hyperlink: missing equals sign in param",
})
}
}
impl<'a> Hyperlink<'a> {
pub const END: Self = Self::new("");
pub const fn new(url: &'a str) -> Self {
Self{ url, id: None }
}
#[inline]
pub const fn with_id(&self, id: &'a str) -> Self {
Self{ url: self.url, id: Some(id) }
}
pub fn parse(src: &'a str) -> Result<Option<(Self, Range<usize>)>, ParseError> {
let Some(start) = src.find(OSC8) else { return Ok(None) };
let mut i: usize = start+OSC8.len();
if i >= src.len() { return Err(ParseError::Partial); }
if src.as_bytes()[i] != b';' { return Err(ParseError::MissingSemi); }
i += 1;
let mut id = None;
if i >= src.len() { return Err(ParseError::Partial); }
if src.as_bytes()[i] != b';' {
let Some(end_params) = src[i..].find(';') else { return Err(ParseError::Partial) };
for param in src[i..i+end_params].split(':') {
let Some((key, val)) = param.split_once('=') else { return Err(ParseError::MissingEq) };
if key == "id" {
id = Some(val);
}
}
i += end_params;
} else {
i += 1;
}
let start_url = i;
let Some(end_url) = src[start_url..].find(ST) else { return Err(ParseError::Partial) };
let end_url = end_url+start_url;
let end = end_url + ST.len();
Ok(Some((Hyperlink{ url: &src[start_url..end_url], id }, Range{ start, end })))
}
pub fn url(&self) -> &'a str {
self.url
}
pub fn id(&self) -> Option<&'a str> {
self.id
}
}
#[cfg(test)]
mod tests {
use super::*;
extern crate std;
use std::dbg;
#[test]
fn it_works() {
let txt = "\x1b]8;;http://example.com\x1b\\This is a link\x1b]8;;\x1b\\\n";
assert_eq!(ST, "\x1b\\");
assert!(txt.find(ST).is_some());
let (link1, r1) = Hyperlink::parse(txt).unwrap().unwrap();
assert_eq!(link1.url(), "http://example.com");
assert_eq!(r1, 0..25);
let (link2, r2) = Hyperlink::parse(&txt[r1.end..]).unwrap().unwrap();
assert_eq!(link2.url(), "");
assert_eq!(r2.start, 14);
assert_eq!(&txt[r1.end..r2.start+r1.end], "This is a link");
for i in 0..txt.len() {
for k in 0..i {
let _ = Hyperlink::parse(&txt[k..i]);
}
}
for r in &[r1.clone(), (r2.start+r1.end..r2.end+r1.end)] {
for i in r.start+OSC8.len()+1..r.end-1 {
let txt_slice = &txt[r.start..i];
dbg!(r, i, txt_slice);
assert!(txt_slice.contains(OSC8));
assert_eq!(Hyperlink::parse(txt_slice),
Err(ParseError::Partial),
"not partial {r:?} {i} {txt_slice:?}");
}
}
}
}