use crate::js::chars::{has_prefix_fold, is_space, is_word, space_run, unit, unit_at};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[repr(u8)]
pub(super) enum T {
End,
Error,
OpenDirective,
AccTitle,
AccTitleValue,
AccDescr,
AccDescrValue,
AccDescrMultilineValue,
Nl,
Href,
Callbackname,
Callbackargs,
Click,
Gantt,
DateFormat,
InclusiveEndDates,
TopAxis,
AxisFormat,
TickInterval,
Includes,
Excludes,
TodayMarker,
WeekdayMonday,
WeekdayTuesday,
WeekdayWednesday,
WeekdayThursday,
WeekdayFriday,
WeekdaySaturday,
WeekdaySunday,
WeekendFriday,
WeekendSaturday,
Date,
Title,
AccDescription,
Section,
TaskTxt,
TaskData,
Colon,
Eof,
}
pub(super) const WEEKDAY_TOKENS: [T; 7] = [
T::WeekdayMonday,
T::WeekdayTuesday,
T::WeekdayWednesday,
T::WeekdayThursday,
T::WeekdayFriday,
T::WeekdaySaturday,
T::WeekdaySunday,
];
pub(super) const TOKEN_NAMES: [&str; 39] = [
"$end",
"INVALID",
"open_directive",
"acc_title",
"acc_title_value",
"acc_descr",
"acc_descr_value",
"acc_descr_multiline_value",
"NL",
"href",
"callbackname",
"callbackargs",
"click",
"gantt",
"dateFormat",
"inclusiveEndDates",
"topAxis",
"axisFormat",
"tickInterval",
"includes",
"excludes",
"todayMarker",
"weekday_monday",
"weekday_tuesday",
"weekday_wednesday",
"weekday_thursday",
"weekday_friday",
"weekday_saturday",
"weekday_sunday",
"weekend_friday",
"weekend_saturday",
"date",
"title",
"accDescription",
"section",
"taskTxt",
"taskData",
":",
"EOF",
];
#[derive(Clone, Copy, Debug)]
pub(super) struct Tok<'a> {
pub(super) kind: T,
pub(super) text: &'a str,
pub(super) start: usize,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum S {
Initial,
AccTitle,
AccDescr,
AccDescrMultiline,
Href,
Callbackname,
Callbackargs,
Click,
}
const DAYS: [&str; 7] = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday",
];
fn word_at(s: &str, i: usize) -> bool {
s.as_bytes().get(i).is_some_and(|&c| is_word(i32::from(c)))
}
fn fold_at(s: &str, p: usize, word: &str) -> bool {
has_prefix_fold(&s[p..], word)
}
fn match_acc(s: &str, p: usize, word: &str, sep: u8) -> Option<usize> {
if !fold_at(s, p, word) {
return None;
}
let e = space_run(s, p + word.len());
(s.as_bytes().get(e) == Some(&sep)).then(|| space_run(s, e + 1))
}
fn match_spaced(s: &str, p: usize, word: &str, quote: bool) -> Option<usize> {
if !fold_at(s, p, word) {
return None;
}
let mut e = space_run(s, p + word.len());
if e == p + word.len() {
return None;
}
if quote {
if s.as_bytes().get(e) != Some(&b'"') {
return None;
}
e += 1;
}
Some(e)
}
fn match_word(s: &str, p: usize, word: &str) -> Option<usize> {
(fold_at(s, p, word) && !word_at(s, p + word.len())).then_some(p + word.len())
}
fn match_rest(s: &str, p: usize, word: &str, stop: &[u8]) -> Option<usize> {
if !fold_at(s, p, word) {
return None;
}
let mut q = p + word.len();
let (c, size) = unit_at(s, q);
if !is_space(c) {
return None;
}
q += size;
let b = s.as_bytes();
let mut e = q;
while e < b.len() && !stop.contains(&b[e]) {
e += 1;
}
(e > q).then_some(e)
}
fn match_bare_title(s: &str, p: usize) -> Option<usize> {
if !fold_at(s, p, "title") {
return None;
}
let mut e = p + "title".len();
loop {
let (c, size) = unit_at(s, e);
if c == 10 || c == 13 || !is_space(c) {
break;
}
e += size;
}
match s.as_bytes().get(e) {
None | Some(b'\n' | b'\r') => Some(e),
_ => None,
}
}
fn match_title(s: &str, p: usize) -> Option<usize> {
let c = unit(s, p + "title".len());
if c == 10 || c == 13 {
return None;
}
match_rest(s, p, "title", b"\n")
}
fn match_days(s: &str, p: usize, prefix: &str, names: &[&str]) -> Option<(usize, usize)> {
let e = match_spaced(s, p, prefix, false)?;
names
.iter()
.position(|name| fold_at(s, e, name) && !word_at(s, e + name.len()))
.map(|k| (e + names[k].len(), k))
}
fn match_date(s: &str, p: usize) -> bool {
let Some(b) = s.as_bytes().get(p..p + 10) else {
return false;
};
b.iter().enumerate().all(|(i, &c)| {
if i == 4 || i == 7 {
c == b'-'
} else {
c.is_ascii_digit()
}
}) && !word_at(s, p + 10)
}
type Rule = (fn(&str, usize) -> Option<usize>, T);
fn keyword_rules(c: u8) -> &'static [Rule] {
match c {
b'a' => &[
(
|s, p| match_rest(s, p, "axisFormat", b"#\n;"),
T::AxisFormat,
),
(
|s, p| match_rest(s, p, "accDescription", b"#\n;"),
T::AccDescription,
),
],
b'g' => &[(|s, p| match_word(s, p, "gantt"), T::Gantt)],
b'd' => &[(
|s, p| match_rest(s, p, "dateFormat", b"#\n;"),
T::DateFormat,
)],
b'i' => &[
(
|s, p| match_word(s, p, "inclusiveEndDates"),
T::InclusiveEndDates,
),
(|s, p| match_rest(s, p, "includes", b"#\n;"), T::Includes),
],
b't' => &[
(|s, p| match_word(s, p, "topAxis"), T::TopAxis),
(
|s, p| match_rest(s, p, "tickInterval", b"#\n;"),
T::TickInterval,
),
(
|s, p| match_rest(s, p, "todayMarker", b"\n;"),
T::TodayMarker,
),
(match_title, T::Title),
],
b'e' => &[(|s, p| match_rest(s, p, "excludes", b"#\n;"), T::Excludes)],
b's' => &[(|s, p| match_rest(s, p, "section", b"\n"), T::Section)],
_ => &[],
}
}
pub(super) fn tokenize<'a>(src: &'a str) -> Vec<Tok<'a>> {
let n = src.len();
let b = src.as_bytes();
let mut toks = Vec::new();
let mut state = S::Initial;
let mut p = 0;
let emit = |toks: &mut Vec<Tok<'a>>, kind: T, s: usize, e: usize| -> usize {
toks.push(Tok {
kind,
text: &src[s..e],
start: s,
});
e
};
let until = |ch: u8, from: usize| -> usize {
if from >= n {
return n;
}
b[from..]
.iter()
.position(|&c| c == ch)
.map_or(n, |e| from + e)
};
let keyword = |toks: &mut Vec<Tok<'a>>, p: &mut usize, state: &mut S, c: i32| -> bool {
let lower = (c | 32) as u8;
match lower {
b'a' => {
if let Some(e) = match_acc(src, *p, "accTitle", b':') {
*state = S::AccTitle;
*p = emit(toks, T::AccTitle, *p, e);
return true;
}
if let Some(e) = match_acc(src, *p, "accDescr", b':') {
*state = S::AccDescr;
*p = emit(toks, T::AccDescr, *p, e);
return true;
}
if let Some(e) = match_acc(src, *p, "accDescr", b'{') {
*state = S::AccDescrMultiline;
*p = e;
return true;
}
}
b'h' => {
if let Some(e) = match_spaced(src, *p, "href", true) {
*state = S::Href;
*p = e;
return true;
}
}
b'c' => {
if let Some(e) = match_spaced(src, *p, "call", false) {
*state = S::Callbackname;
*p = e;
return true;
}
if let Some(e) = match_spaced(src, *p, "click", false) {
*state = S::Click;
*p = e;
return true;
}
}
b't' => {
if let Some(e) = match_bare_title(src, *p) {
*p = e;
return true;
}
}
b'w' => {
if let Some((e, k)) = match_days(src, *p, "weekday", &DAYS) {
*p = emit(toks, WEEKDAY_TOKENS[k], *p, e);
return true;
}
if let Some((e, k)) = match_days(src, *p, "weekend", &["friday", "saturday"]) {
let kind = if k == 0 {
T::WeekendFriday
} else {
T::WeekendSaturday
};
*p = emit(toks, kind, *p, e);
return true;
}
}
_ => {}
}
for &(pattern, kind) in keyword_rules(lower) {
if let Some(e) = pattern(src, *p) {
*p = emit(toks, kind, *p, e);
return true;
}
}
false
};
loop {
if p >= n {
let kind = match state {
S::Initial => T::Eof,
S::AccTitle => T::AccTitleValue,
S::AccDescr => T::AccDescrValue,
S::AccDescrMultiline => T::AccDescrMultilineValue,
S::Href => T::Href,
S::Callbackname => T::Callbackname,
S::Callbackargs => T::Callbackargs,
S::Click => T::Click,
};
emit(&mut toks, kind, n, n);
break;
}
let (c, size) = unit_at(src, p);
match state {
S::Initial => {
let d = if size < 4 { unit(src, p + size) } else { -1 };
if c == 37 {
if d != 37 {
let mut e = until(b'\n', p + 1);
while e < n && b[e] == b'\n' {
e += 1;
}
p = e;
} else if unit(src, p + 2) == 123 {
p = emit(&mut toks, T::OpenDirective, p, p + 3);
emit(&mut toks, T::Error, p, p);
toks.push(Tok {
kind: T::End,
text: "",
start: n,
});
return toks;
} else {
p = until(b'\n', p + 2);
}
} else if c == 10 {
let mut e = p + 1;
while e < n && b[e] == b'\n' {
e += 1;
}
p = emit(&mut toks, T::Nl, p, e);
} else if is_space(c) {
p = space_run(src, p);
} else if keyword(&mut toks, &mut p, &mut state, c) {
} else if (48..=57).contains(&c) && match_date(src, p) {
p = emit(&mut toks, T::Date, p, p + 10);
} else if c != 58 {
let mut e = p + 1;
while e < n && b[e] != b':' && b[e] != b'\n' {
e += 1;
}
p = emit(&mut toks, T::TaskTxt, p, e);
} else {
let mut e = p + 1;
while e < n && b[e] != b'#' && b[e] != b'\n' && b[e] != b';' {
e += 1;
}
let kind = if e > p + 1 { T::TaskData } else { T::Colon };
p = emit(&mut toks, kind, p, e);
}
}
S::AccTitle | S::AccDescr => {
let kind = if state == S::AccTitle {
T::AccTitleValue
} else {
T::AccDescrValue
};
state = S::Initial;
p = emit(&mut toks, kind, p, until(b'\n', p));
}
S::AccDescrMultiline => {
if c == 125 {
state = S::Initial;
p += 1;
} else {
p = emit(&mut toks, T::AccDescrMultilineValue, p, until(b'}', p));
}
}
S::Href => {
if c == 34 {
state = S::Initial;
p += 1;
} else {
p = emit(&mut toks, T::Href, p, until(b'"', p));
}
}
S::Callbackname => {
if c == 40 {
let e = space_run(src, p + 1);
if b.get(e) == Some(&b')') {
state = S::Initial;
p = e + 1;
} else {
state = S::Callbackargs;
p += 1;
}
} else {
p = emit(&mut toks, T::Callbackname, p, until(b'(', p));
}
}
S::Callbackargs => {
if c == 41 {
state = S::Initial;
p += 1;
} else {
p = emit(&mut toks, T::Callbackargs, p, until(b')', p));
}
}
S::Click => {
if is_space(c) {
state = S::Initial;
p += size;
} else {
let mut e = p + size;
loop {
let (c, size) = unit_at(src, e);
if size == 0 || is_space(c) {
break;
}
e += size;
}
p = emit(&mut toks, T::Click, p, e);
}
}
}
}
toks.push(Tok {
kind: T::End,
text: "",
start: n,
});
toks
}