use std::sync::OnceLock;
use crate::common::tokens::{
ACC_DESCR, ACC_TITLE, DIRECTIVE, END, NEWLINE, Pattern, Reader, SINGLE_LINE_COMMENT, STRING,
Table, TokenType, WHITESPACE, YAML, acc_descr_value, acc_title_value, int, keyword,
match_title, string_value, title_value, token, tokenize,
};
use crate::detect::DiagramType;
use crate::errors::Error;
use crate::js::chars::{is_word, space_run};
use crate::js::number::parse_int;
pub(crate) const T_CHERRY_PICK: usize = 0;
pub(crate) const T_GIT_GRAPH_COLON: usize = 1;
pub(crate) const T_HIGHLIGHT: usize = 2;
pub(crate) const T_GIT_GRAPH: usize = 3;
pub(crate) const T_CHECKOUT: usize = 4;
pub(crate) const T_REVERSE: usize = 5;
pub(crate) const T_PARENT: usize = 6;
pub(crate) const T_COMMIT: usize = 7;
pub(crate) const T_NORMAL: usize = 8;
pub(crate) const T_BRANCH: usize = 9;
pub(crate) const T_ORDER: usize = 10;
pub(crate) const T_SWITCH: usize = 11;
pub(crate) const T_TYPE: usize = 12;
pub(crate) const T_MERGE: usize = 13;
pub(crate) const T_MSG: usize = 14;
pub(crate) const T_TAG: usize = 15;
pub(crate) const T_ID: usize = 16;
pub(crate) const T_LR: usize = 17;
pub(crate) const T_TB: usize = 18;
pub(crate) const T_BT: usize = 19;
pub(crate) const T_COLON: usize = 20;
pub(crate) const T_ACC_DESCR: usize = 21;
pub(crate) const T_ACC_TITLE: usize = 22;
pub(crate) const T_TITLE: usize = 23;
pub(crate) const T_INT: usize = 24;
pub(crate) const T_STRING: usize = 25;
pub(crate) const T_NEWLINE: usize = 26;
pub(crate) const T_REFERENCE: usize = 31;
const WORD: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz";
fn is_word_byte(b: u8) -> bool {
is_word(i32::from(b))
}
fn reference(s: &str, at: usize) -> Option<usize> {
let b = s.as_bytes();
if !is_word_byte(b[at]) {
return None;
}
let mut end = at + 1;
let mut i = at + 1;
while i < b.len() && (matches!(b[i], b'-' | b'.' | b'/') || is_word_byte(b[i])) {
if b[i] != b'.' && b[i] != b'/' {
end = i + 1;
}
i += 1;
}
Some(end)
}
fn reference_rest(s: &str, at: usize) -> bool {
let b = s.as_bytes();
let mut i = at;
while i < b.len() && (matches!(b[i], b'-' | b'.' | b'/') || is_word_byte(b[i])) {
if b[i] != b'.' && b[i] != b'/' {
return true;
}
i += 1;
}
false
}
fn title(s: &str, at: usize) -> Option<usize> {
match_title(s, at, Some(reference_rest))
}
fn acc_descr_opener(s: &str, at: usize) -> bool {
let b = s.as_bytes();
let mut i = at;
while i < b.len() && (b[i] == b' ' || b[i] == b'\t') {
i += 1;
}
if !s[i..].starts_with("accDescr") {
return false;
}
let i = space_run(s, i + "accDescr".len());
b.get(i) == Some(&b'{')
}
const fn word(text: &'static str) -> TokenType {
TokenType {
longer: &[T_REFERENCE],
..token(text, Pattern::Literal(text))
}
}
pub(crate) const GIT_TOKENS: &[TokenType] = &[
word("cherry-pick"),
token("gitGraph:", Pattern::Literal("gitGraph:")),
word("HIGHLIGHT"),
TokenType {
longer: &[T_REFERENCE],
..keyword("gitGraph")
},
word("checkout"),
word("REVERSE"),
token("parent:", Pattern::Literal("parent:")),
word("commit"),
word("NORMAL"),
word("branch"),
token("order:", Pattern::Literal("order:")),
word("switch"),
token("type:", Pattern::Literal("type:")),
word("merge"),
token("msg:", Pattern::Literal("msg:")),
token("tag:", Pattern::Literal("tag:")),
token("id:", Pattern::Literal("id:")),
word("LR"),
word("TB"),
word("BT"),
token(":", Pattern::Literal(":")),
TokenType {
opener: Some(acc_descr_opener),
..ACC_DESCR
},
ACC_TITLE,
TokenType {
first: Some("\t t"),
..token("TITLE", Pattern::Match(title))
},
TokenType {
first: Some("0123456789"),
..token("INT", Pattern::Match(int))
},
STRING,
NEWLINE,
WHITESPACE,
YAML,
DIRECTIVE,
SINGLE_LINE_COMMENT,
TokenType {
first: Some(WORD),
..token("REFERENCE", Pattern::Match(reference))
},
];
pub(crate) fn table() -> &'static Table {
static TABLE: OnceLock<Table> = OnceLock::new();
TABLE.get_or_init(|| Table::new(GIT_TOKENS))
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum StatementAst {
Commit {
id: Option<String>,
message: Option<String>,
tags: Vec<String>,
kind: Option<&'static str>,
},
Branch { name: String, order: Option<f64> },
Merge {
branch: String,
id: Option<String>,
tags: Vec<String>,
kind: Option<&'static str>,
},
Checkout { branch: String },
CherryPicking {
id: Option<String>,
tags: Vec<String>,
parent: Option<String>,
},
}
#[derive(Clone, Debug, Default, PartialEq)]
pub(crate) struct GitGraphAst {
pub(crate) is_direction: bool,
pub(crate) dir: Option<&'static str>,
pub(crate) title: Option<String>,
pub(crate) acc_title: Option<String>,
pub(crate) acc_descr: Option<String>,
pub(crate) statements: Vec<StatementAst>,
}
fn commit_type(r: &mut Reader) -> Result<&'static str, Error> {
let name = match r.kind() {
T_NORMAL => "NORMAL",
T_REVERSE => "REVERSE",
T_HIGHLIGHT => "HIGHLIGHT",
_ => return Err(r.fail("NORMAL, REVERSE, or HIGHLIGHT")),
};
r.i += 1;
Ok(name)
}
fn name(r: &mut Reader) -> Result<String, Error> {
match r.kind() {
T_REFERENCE => Ok(r.take().to_string()),
T_STRING => Ok(string_value(r.take())),
_ => Err(r.fail("a branch name")),
}
}
fn string(r: &mut Reader) -> Result<String, Error> {
Ok(string_value(r.expect(T_STRING)?))
}
fn end_of_line(r: &mut Reader) -> Result<(), Error> {
if r.kind() == END {
return Ok(());
}
if r.kind() != T_NEWLINE {
return Err(r.fail("a line break or the end of input"));
}
while r.kind() == T_NEWLINE {
r.i += 1;
}
Ok(())
}
pub(crate) fn parse_git(src: &str) -> Result<GitGraphAst, Error> {
let tokens = tokenize(src, table(), DiagramType::GitGraph)?;
let mut r = Reader::new(tokens, table(), DiagramType::GitGraph);
let mut ast = GitGraphAst::default();
while r.kind() == T_NEWLINE {
r.i += 1;
}
if !r.accept(T_GIT_GRAPH_COLON) {
r.expect(T_GIT_GRAPH)?;
let dir = match r.kind() {
T_LR => Some("LR"),
T_TB => Some("TB"),
T_BT => Some("BT"),
_ => None,
};
if dir.is_some() && r.tokens.kinds[r.i + 1] == T_COLON {
ast.is_direction = true;
ast.dir = dir;
r.i += 2;
} else {
r.accept(T_COLON);
}
}
while r.kind() != END {
match r.kind() {
T_NEWLINE => r.i += 1,
T_ACC_DESCR => {
ast.acc_descr = Some(acc_descr_value(r.take()));
end_of_line(&mut r)?;
}
T_ACC_TITLE => {
ast.acc_title = Some(acc_title_value(r.take()));
end_of_line(&mut r)?;
}
T_TITLE => {
ast.title = Some(title_value(r.take()));
end_of_line(&mut r)?;
}
T_COMMIT => {
r.i += 1;
let (mut id, mut message, mut tags, mut kind) = (None, None, Vec::new(), None);
loop {
if r.accept(T_ID) {
id = Some(string(&mut r)?);
} else if r.accept(T_TAG) {
tags.push(string(&mut r)?);
} else if r.accept(T_TYPE) {
kind = Some(commit_type(&mut r)?);
} else if r.accept(T_MSG) || r.kind() == T_STRING {
message = Some(string(&mut r)?);
} else {
break;
}
}
end_of_line(&mut r)?;
ast.statements.push(StatementAst::Commit {
id,
message,
tags,
kind,
});
}
T_BRANCH => {
r.i += 1;
let name = name(&mut r)?;
let mut order = None;
if r.accept(T_ORDER) {
order = Some(parse_int(r.expect(T_INT)?, 10));
}
end_of_line(&mut r)?;
ast.statements.push(StatementAst::Branch { name, order });
}
T_MERGE => {
r.i += 1;
let branch = name(&mut r)?;
let (mut id, mut tags, mut kind) = (None, Vec::new(), None);
loop {
if r.accept(T_ID) {
id = Some(string(&mut r)?);
} else if r.accept(T_TAG) {
tags.push(string(&mut r)?);
} else if r.accept(T_TYPE) {
kind = Some(commit_type(&mut r)?);
} else {
break;
}
}
end_of_line(&mut r)?;
ast.statements.push(StatementAst::Merge {
branch,
id,
tags,
kind,
});
}
T_CHECKOUT | T_SWITCH => {
r.i += 1;
let branch = name(&mut r)?;
end_of_line(&mut r)?;
ast.statements.push(StatementAst::Checkout { branch });
}
T_CHERRY_PICK => {
r.i += 1;
let (mut id, mut tags, mut parent) = (None, Vec::new(), None);
loop {
if r.accept(T_ID) {
id = Some(string(&mut r)?);
} else if r.accept(T_TAG) {
tags.push(string(&mut r)?);
} else if r.accept(T_PARENT) {
parent = Some(string(&mut r)?);
} else {
break;
}
}
end_of_line(&mut r)?;
ast.statements
.push(StatementAst::CherryPicking { id, tags, parent });
}
_ => return Err(r.fail("a statement, a title, or a line break")),
}
}
Ok(ast)
}