use super::glyph;
use crate::parse::{
Parser,
lexer::{Kind, TokenSet},
};
use crate::token_tree::Kind as AstKind;
pub(crate) fn gsub_rule(parser: &mut Parser, recovery: TokenSet) {
fn gsub_body(parser: &mut Parser, recovery: TokenSet) -> AstKind {
const RECOVERY: TokenSet = TokenSet::new(&[
Kind::ByKw,
Kind::FromKw,
Kind::SingleQuote,
Kind::Comma,
Kind::LookupKw,
Kind::Semi,
]);
if parser.matches(0, Kind::IgnoreKw) {
return parse_ignore(parser, recovery);
}
if parser.matches(0, Kind::RsubKw) {
return parse_rsub(parser, recovery);
}
assert!(parser.eat(Kind::SubKw));
let target_is_class = matches!(parser.nth(0).kind, Kind::LSquare | Kind::NamedGlyphClass);
if !glyph::eat_glyph_or_glyph_class(parser, recovery.union(RECOVERY)) {
parser.err_and_bump("Expected glyph or glyph class");
parser.eat_until(recovery.union(Kind::Semi.into()));
return AstKind::GsubNode;
}
if parser.eat(Kind::Semi) {
return AstKind::GsubType1;
} else if parser.eat(Kind::ByKw) {
if parser.eat(Kind::NullKw) {
parser.expect_semi();
return AstKind::GsubType1;
}
if !glyph::expect_glyph_or_glyph_class(parser, recovery) {
parser.eat_until(recovery.union(Kind::Semi.into()));
parser.expect_semi();
return AstKind::GsubNode;
}
let multiple_targets =
super::greedy(glyph::eat_glyph_or_glyph_class)(parser, recovery.union(RECOVERY));
parser.expect_semi();
if multiple_targets {
return AstKind::GsubType2;
} else {
return AstKind::GsubType1;
}
} else if !target_is_class && parser.eat(Kind::FromKw) {
glyph::eat_named_or_unnamed_glyph_class(parser, recovery.union(RECOVERY));
parser.expect_semi();
return AstKind::GsubType3;
} else if parser.matches(0, Kind::FromKw) {
parser.err_and_bump("'from' can only follow glyph, not glyph class");
parser.eat_until(recovery.union(Kind::Semi.into()));
return AstKind::GsubNode;
}
let is_seq = glyph::eat_glyph_or_glyph_class(parser, recovery.union(RECOVERY));
while glyph::eat_glyph_or_glyph_class(parser, recovery.union(RECOVERY)) {
continue;
}
if is_seq && parser.eat(Kind::ByKw) {
glyph::expect_glyph_name_like(parser, recovery.union(RECOVERY));
parser.expect_semi();
AstKind::GsubType4
} else if parser.matches(0, Kind::SingleQuote) {
finish_chain_rule(parser, recovery)
} else {
if parser.matches(0, Kind::ByKw) {
parser.err("ligature substitution must replace two or more glyphs");
} else {
parser.err("expected ligature substitution or marked glyph");
}
parser.eat_until(recovery.union(Kind::Semi.into()));
AstKind::GsubNode
}
}
parser.eat_trivia();
parser.start_node(AstKind::GsubNode);
let kind = gsub_body(parser, recovery);
parser.finish_and_remap_node(kind);
}
fn finish_chain_rule(parser: &mut Parser, recovery: TokenSet) -> AstKind {
debug_assert!(parser.matches(0, Kind::SingleQuote));
let recovery = recovery.union(Kind::Semi.into());
while parser.eat(Kind::SingleQuote) {
while parser.eat(Kind::LookupKw) {
if !parser.eat(Kind::Ident) {
parser.err("expected named lookup");
parser.eat_until(recovery);
return AstKind::GsubNode;
}
}
glyph::eat_glyph_or_glyph_class(parser, recovery);
}
super::greedy(glyph::eat_glyph_or_glyph_class)(parser, recovery);
if parser.eat(Kind::ByKw) {
if parser.eat(Kind::NullKw) {
} else if glyph::eat_glyph_name_like(parser) {
while glyph::eat_glyph_name_like(parser) {
continue;
}
} else if !glyph::expect_named_or_unnamed_glyph_class(parser, recovery) {
parser.eat_until(recovery);
parser.eat(Kind::Semi);
return AstKind::GsubNode;
}
} else if parser.eat(Kind::FromKw)
&& !glyph::expect_named_or_unnamed_glyph_class(parser, recovery)
{
parser.eat_until(recovery);
parser.eat(Kind::Semi);
return AstKind::GsubNode;
}
if parser.expect_semi() {
AstKind::GsubNodeNeedsRewrite
} else {
AstKind::GsubNode
}
}
fn parse_ignore(parser: &mut Parser, recovery: TokenSet) -> AstKind {
assert!(parser.eat(Kind::IgnoreKw));
assert!(parser.eat(Kind::SubKw));
if super::expect_ignore_pattern_body(parser, recovery) {
AstKind::GsubNodeNeedsRewrite
} else {
AstKind::GsubNode
}
}
fn parse_rsub(parser: &mut Parser, recovery: TokenSet) -> AstKind {
assert!(parser.eat(Kind::RsubKw));
let recovery = recovery.add(Kind::Semi);
super::greedy(glyph::eat_glyph_or_glyph_class)(parser, recovery);
if !parser.expect(Kind::SingleQuote) {
parser.eat_until(recovery);
parser.expect_semi();
return AstKind::GsubNode;
}
super::greedy(glyph::eat_glyph_or_glyph_class)(parser, recovery);
if parser.matches(0, Kind::SingleQuote) {
parser.err("reversesub rule can have only one marked glyph");
parser.eat_until(recovery);
parser.expect_semi();
return AstKind::GsubNode;
}
if parser.eat(Kind::ByKw) {
if parser.matches(0, Kind::NullKw) {
parser.err("Although explicitly part of the FEA spec, 'by NULL' in rsub rules is meaningless.\nSee https://github.com/fonttools/fonttools/issues/2952 for more information");
parser.eat_until(recovery);
parser.expect_semi();
return AstKind::GsubNode;
}
glyph::expect_glyph_or_glyph_class(parser, recovery);
}
parser.expect_semi();
AstKind::GsubNodeNeedsRewrite
}
#[cfg(test)]
mod tests {
use super::super::debug_parse_output;
use super::*;
#[test]
fn gsub_smoke_test() {
let not_allowed = [
"sub a [b c]' by [b c] d;", "rsub a b' c' d;", "sub a b' c d' by g;", ];
for bad in not_allowed {
let (_out, errors, _errstr) =
debug_parse_output(bad, |parser| gsub_rule(parser, TokenSet::from(Kind::Eof)));
assert!(!errors.is_empty(), "{}", bad);
}
}
}