use tiptap_rusty_parser::{ContentExpr, Document, NodeSpec, Schema, ViolationKind};
fn is_valid(expr: &str, children: &[&str]) -> bool {
let schema = Schema::new()
.node("doc", NodeSpec::new().content_match(expr))
.node("paragraph", NodeSpec::new().group("block"))
.node("heading", NodeSpec::new().group("block"))
.node("image", NodeSpec::new().group("inline"))
.node("text", NodeSpec::new().group("inline"));
let content: Vec<_> = children
.iter()
.map(|t| serde_json::json!({ "type": t }))
.collect();
let doc =
Document::from_value(serde_json::json!({ "type": "doc", "content": content })).unwrap();
doc.is_valid(&schema)
}
#[test]
fn plus_star_opt() {
assert!(!is_valid("paragraph+", &[]));
assert!(is_valid("paragraph+", &["paragraph"]));
assert!(is_valid("paragraph+", &["paragraph", "paragraph"]));
assert!(!is_valid("paragraph+", &["paragraph", "heading"]));
assert!(is_valid("paragraph*", &[]));
assert!(is_valid("paragraph*", &["paragraph", "paragraph"]));
assert!(is_valid("paragraph?", &[]));
assert!(is_valid("paragraph?", &["paragraph"]));
assert!(!is_valid("paragraph?", &["paragraph", "paragraph"]));
}
#[test]
fn ranges() {
assert!(!is_valid("heading{1,3}", &[]));
assert!(is_valid("heading{1,3}", &["heading"]));
assert!(is_valid("heading{1,3}", &["heading", "heading", "heading"]));
assert!(!is_valid(
"heading{1,3}",
&["heading", "heading", "heading", "heading"]
));
assert!(!is_valid("heading{2,}", &["heading"]));
assert!(is_valid("heading{2,}", &["heading", "heading"]));
assert!(is_valid("heading{2,}", &["heading", "heading", "heading"]));
}
#[test]
fn alternation_sequence_groups() {
assert!(is_valid(
"(paragraph | heading)*",
&["heading", "paragraph", "heading"]
));
assert!(!is_valid("(paragraph | heading)*", &["text"]));
assert!(is_valid("heading paragraph+", &["heading", "paragraph"]));
assert!(is_valid(
"heading paragraph+",
&["heading", "paragraph", "paragraph"]
));
assert!(!is_valid("heading paragraph+", &["heading"]));
assert!(!is_valid("heading paragraph+", &["paragraph", "heading"]));
assert!(is_valid("block+", &["paragraph", "heading"]));
assert!(!is_valid("block+", &["text"]));
assert!(!is_valid("block+", &[]));
assert!(!is_valid("widget+", &["paragraph"]));
}
#[test]
fn invalid_content_violation_shape_and_path() {
let schema = Schema::new()
.node("doc", NodeSpec::new().content_match("paragraph+"))
.node("paragraph", NodeSpec::new().content_match("text+"))
.node("text", NodeSpec::new());
let doc =
Document::from_json_str(r#"{"type":"doc","content":[{"type":"paragraph"}]}"#).unwrap();
let v = doc.validate(&schema);
assert_eq!(v.len(), 1);
assert_eq!(v[0].path, vec![0]);
assert!(matches!(
&v[0].kind,
ViolationKind::InvalidContent { parent, expr } if parent == "paragraph" && expr == "text+"
));
let empty = Document::from_json_str(r#"{"type":"doc"}"#).unwrap();
let v = empty.validate(&schema);
assert_eq!(v.len(), 1);
assert_eq!(v[0].path, Vec::<usize>::new());
}
#[test]
fn parse_errors_and_ok() {
for ok in [
"paragraph+",
"heading{1,3}",
"(a | b) c*",
"",
" ",
"block+",
] {
assert!(ContentExpr::parse(ok).is_ok(), "should parse: {ok:?}");
}
for bad in ["(a", "a)", "{2,1}", "{}", "a**", "heading{2000}"] {
assert!(ContentExpr::parse(bad).is_err(), "should fail: {bad:?}");
}
}
#[test]
fn serde_roundtrip_and_invalid_expr() {
let json = r#"{"nodes":{
"doc":{"content":"block+"},
"paragraph":{"group":"block","content":["text"]},
"heading":{"group":"block"},
"text":{}
}}"#;
let schema = Schema::from_json_str(json).unwrap();
let good =
Document::from_json_str(r#"{"type":"doc","content":[{"type":"paragraph"}]}"#).unwrap();
let bad = Document::from_json_str(r#"{"type":"doc","content":[{"type":"text"}]}"#).unwrap();
assert!(good.is_valid(&schema));
assert!(!bad.is_valid(&schema));
let reser = serde_json::to_string(&schema).unwrap();
let schema2 = Schema::from_json_str(&reser).unwrap();
assert_eq!(good.validate(&schema2), good.validate(&schema));
assert_eq!(bad.validate(&schema2), bad.validate(&schema));
assert!(Schema::from_json_str(r#"{"nodes":{"doc":{"content":"(a"}}}"#).is_err());
}
#[test]
fn builder_matches_json() {
let built = Schema::new()
.node("doc", NodeSpec::new().content_match("paragraph+"))
.node("paragraph", NodeSpec::new());
let loaded =
Schema::from_json_str(r#"{"nodes":{"doc":{"content":"paragraph+"},"paragraph":{}}}"#)
.unwrap();
let doc = Document::from_json_str(r#"{"type":"doc"}"#).unwrap();
assert_eq!(doc.validate(&built), doc.validate(&loaded));
}
#[test]
fn array_form_still_disallowed_child() {
let schema = Schema::from_json_str(
r#"{"nodes":{"doc":{"content":["paragraph"]},"paragraph":{},"heading":{}}}"#,
)
.unwrap();
let doc = Document::from_json_str(r#"{"type":"doc","content":[{"type":"heading"}]}"#).unwrap();
let v = doc.validate(&schema);
assert_eq!(v.len(), 1);
assert!(matches!(&v[0].kind, ViolationKind::DisallowedChild { .. }));
}