Make the naming scheme fully user-editable, with the user's preferred format as default
- ExpressionFormat::compile_scriptlet now falls back to treating a {token} as a
literal multi-word binding key when it's neither a valid Rhai identifier nor
valid Rhai script, but still looks like a plain human-typed field name (only
letters/digits/underscores/spaces). This is what lets `{show name}` work
without anyone needing to know Rhai, while real Rhai script (even containing
spaces) still takes priority and genuine syntax errors still surface.
- New default naming scheme app-wide: `{show name} - {season number}x
{episode number} - {episode name}` (previously the terser {n} - {s00e00} -
{t} form). Match/Fetch Data auto-upgrade targets updated to match, and
sample items carry both the short FileBot-style keys and the new
human-readable ones side by side.
- Naming scheme editor UI rewritten: explains the field is fully editable,
lists available fields grouped by when they become available (always /
after Match / after Fetch Data), and clarifies Rhai script is also
supported for anyone who wants it.
- 3 new unit tests for the plain-field-name fallback (multi-word lookup
works, real Rhai still takes priority, genuine syntax errors still error).
This commit is contained in:
@@ -109,13 +109,42 @@ fn compile_scriptlet(token: &str, expression: &str) -> Result<Segment, Expressio
|
||||
return Ok(Segment::Variable(token.to_string()));
|
||||
}
|
||||
|
||||
engine::shared()
|
||||
.compile(token)
|
||||
.map(Segment::Script)
|
||||
.map_err(|e| ExpressionError::Syntax {
|
||||
expression: expression.to_string(),
|
||||
message: e.to_string(),
|
||||
})
|
||||
match engine::shared().compile(token) {
|
||||
Ok(ast) => Ok(Segment::Script(ast)),
|
||||
Err(e) => {
|
||||
// Not valid as a single bare identifier, and not valid as a Rhai script
|
||||
// either - but if it otherwise looks like a plain, human-typed field name
|
||||
// (e.g. "show name": just letters/digits/spaces/underscores), treat the
|
||||
// whole thing as a literal binding key instead of surfacing a syntax
|
||||
// error. This is what lets a naming scheme like
|
||||
// `{show name} - {season number}x{episode number} - {episode name}` work
|
||||
// without every such phrase needing to be a reserved single-word binding.
|
||||
// Genuine script syntax errors (unmatched parens/quotes, bad operators,
|
||||
// etc.) still get reported exactly as before.
|
||||
let trimmed = token.trim();
|
||||
if is_plain_field_name(trimmed) {
|
||||
Ok(Segment::Variable(trimmed.to_string()))
|
||||
} else {
|
||||
Err(ExpressionError::Syntax {
|
||||
expression: expression.to_string(),
|
||||
message: e.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// True for a token that reads like a plain (possibly multi-word) field name rather
|
||||
/// than code: starts with a letter or underscore, and contains only letters, digits,
|
||||
/// underscores, and internal spaces. Only consulted as a fallback after Rhai has
|
||||
/// already rejected the token as a script.
|
||||
fn is_plain_field_name(token: &str) -> bool {
|
||||
let mut chars = token.chars();
|
||||
match chars.next() {
|
||||
Some(c) if c.is_alphabetic() || c == '_' => {}
|
||||
_ => return false,
|
||||
}
|
||||
chars.all(|c| c.is_alphanumeric() || c == '_' || c == ' ')
|
||||
}
|
||||
|
||||
/// Splits an expression into literal text and `{ }` scriptlets, honoring nested braces
|
||||
|
||||
@@ -64,3 +64,36 @@ fn syntax_error_is_reported_with_the_offending_expression() {
|
||||
let message = err.to_string();
|
||||
assert!(message.contains("syntax error"), "message was: {message}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multi_word_plain_field_names_are_treated_as_literal_binding_keys() {
|
||||
// "show name" etc. aren't valid Rhai (two bare identifiers with no operator between
|
||||
// them), so this only works because compile_scriptlet falls back to a literal key
|
||||
// lookup when a token looks like a plain human-typed field name rather than code.
|
||||
let fmt = ExpressionFormat::compile("{show name} - {season number}x{episode number} - {episode name}").unwrap();
|
||||
let bindings = string_bindings([
|
||||
("show name", "Reacher"),
|
||||
("season number", "1"),
|
||||
("episode number", "1"),
|
||||
("episode name", "Welcome to Margrave"),
|
||||
]);
|
||||
assert_eq!(fmt.format(&bindings).unwrap(), "Reacher - 1x1 - Welcome to Margrave");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rhai_expressions_still_take_priority_over_plain_field_name_fallback() {
|
||||
// Contains spaces too, but it's valid Rhai (string concatenation) - should compile
|
||||
// and run as a script, never falling through to the plain-field-name path.
|
||||
let fmt = ExpressionFormat::compile(r#"{n + " " + t}"#).unwrap();
|
||||
let bindings = string_bindings([("n", "Breaking Bad"), ("t", "Ozymandias")]);
|
||||
assert_eq!(fmt.format(&bindings).unwrap(), "Breaking Bad Ozymandias");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn genuinely_invalid_syntax_with_spaces_still_errors() {
|
||||
// Has spaces like a plain field name might, but also punctuation that no field name
|
||||
// would ever contain - should still be reported as a real syntax error, not
|
||||
// silently treated as a (wrong) literal key.
|
||||
let err = ExpressionFormat::compile("{show name (}").unwrap_err();
|
||||
assert!(err.to_string().contains("syntax error"), "message was: {err}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user