use tiptap_rusty_parser::{Document, MarkSpec, NodeSpec, Schema, ViolationKind};
fn schema() -> Schema {
Schema::new()
.node("doc", NodeSpec::new().content(["paragraph", "heading"]))
.node("paragraph", NodeSpec::new().content(["text"]))
.node(
"heading",
NodeSpec::new()
.content(["text"])
.attrs(["level"])
.required_attrs(["level"]),
)
.node("text", NodeSpec::new().marks(["bold", "italic"]))
.mark("bold", MarkSpec::new())
.mark("italic", MarkSpec::new())
.mark(
"link",
MarkSpec::new().attrs(["href"]).required_attrs(["href"]),
)
}
fn doc(json: &str) -> Document {
Document::from_json_str(json).unwrap()
}
#[test]
fn valid_document() {
let d = doc(r#"{
"type":"doc","content":[
{"type":"heading","attrs":{"level":1},"content":[{"type":"text","text":"T"}]},
{"type":"paragraph","content":[{"type":"text","text":"a","marks":[{"type":"bold"}]}]}
]}"#);
assert!(d.is_valid(&schema()));
assert_eq!(d.validate(&schema()), vec![]);
}
#[test]
fn unknown_node_type() {
let d = doc(r#"{"type":"doc","content":[{"type":"widget"}]}"#);
let v = d.validate(&schema());
assert!(v
.iter()
.any(|x| x.kind == ViolationKind::UnknownNodeType("widget".into()) && x.path == vec![0]));
}
#[test]
fn missing_node_type() {
let d = doc(r#"{"type":"doc","content":[{"attrs":{}}]}"#);
let v = d.validate(&schema());
assert!(v
.iter()
.any(|x| x.kind == ViolationKind::MissingNodeType && x.path == vec![0]));
}
#[test]
fn disallowed_child() {
let d = doc(r#"{
"type":"doc","content":[
{"type":"paragraph","content":[{"type":"heading","attrs":{"level":1}}]}
]}"#);
let v = d.validate(&schema());
assert!(v.iter().any(|x| x.path == vec![0]
&& x.kind
== ViolationKind::DisallowedChild {
parent: "paragraph".into(),
child: "heading".into()
}));
}
#[test]
fn unknown_and_disallowed_marks() {
let d = doc(r#"{
"type":"doc","content":[
{"type":"paragraph","content":[
{"type":"text","text":"x","marks":[{"type":"strike"}]}
]}
]}"#);
let v = d.validate(&schema());
assert!(v
.iter()
.any(|x| x.kind == ViolationKind::UnknownMark("strike".into())));
}
#[test]
fn disallowed_mark_on_node() {
let d = doc(r#"{
"type":"doc","content":[
{"type":"paragraph","content":[
{"type":"text","text":"x","marks":[{"type":"link","attrs":{"href":"u"}}]}
]}
]}"#);
let v = d.validate(&schema());
assert!(v.iter().any(|x| x.kind
== ViolationKind::DisallowedMark {
node: "text".into(),
mark: "link".into()
}));
}
#[test]
fn missing_required_attr() {
let d = doc(r#"{"type":"doc","content":[{"type":"heading","content":[]}]}"#);
let v = d.validate(&schema());
assert!(v.iter().any(|x| x.path == vec![0]
&& x.kind
== ViolationKind::MissingAttr {
key: "level".into()
}));
}
#[test]
fn unknown_attr() {
let d =
doc(r#"{"type":"doc","content":[{"type":"heading","attrs":{"level":1,"bogus":true}}]}"#);
let v = d.validate(&schema());
assert!(v.iter().any(|x| x.kind
== ViolationKind::UnknownAttr {
key: "bogus".into()
}));
}
#[test]
fn missing_mark_attr() {
let schema = Schema::new()
.node("doc", NodeSpec::new().content(["paragraph"]))
.node("paragraph", NodeSpec::new().content(["text"]))
.node("text", NodeSpec::new().marks(["link"]))
.mark(
"link",
MarkSpec::new().attrs(["href"]).required_attrs(["href"]),
);
let d = doc(r#"{
"type":"doc","content":[
{"type":"paragraph","content":[
{"type":"text","text":"x","marks":[{"type":"link"}]}
]}
]}"#);
let v = d.validate(&schema);
assert!(v
.iter()
.any(|x| x.kind == ViolationKind::MissingAttr { key: "href".into() }));
}
#[test]
fn json_schema_matches_builder() {
let json = r#"{
"nodes": {
"doc": { "content": ["paragraph","heading"] },
"paragraph": { "content": ["text"] },
"heading": { "content": ["text"], "attrs": ["level"], "required_attrs": ["level"] },
"text": { "marks": ["bold","italic"] }
},
"marks": {
"bold": {}, "italic": {},
"link": { "attrs": ["href"], "required_attrs": ["href"] }
}
}"#;
let from_json = Schema::from_json_str(json).unwrap();
let bad = doc(r#"{"type":"doc","content":[{"type":"heading"}]}"#);
assert_eq!(bad.validate(&from_json), bad.validate(&schema()));
assert!(!bad.is_valid(&from_json)); }
#[test]
fn violation_locates_child_and_mark() {
let schema = Schema::new()
.node("doc", NodeSpec::new().content_match("block+"))
.node(
"paragraph",
NodeSpec::new().group("block").content_match("inline*"),
)
.node(
"heading",
NodeSpec::new()
.group("block")
.content_match("inline*")
.attrs(["level"]),
)
.node("text", NodeSpec::new().group("inline"))
.mark("bold", MarkSpec::new())
.mark("link", MarkSpec::new().attrs(["href"]));
let d = doc(r#"{"type":"doc","content":[
{"type":"paragraph","content":[
{"type":"text","text":"a"},
{"type":"heading","attrs":{"level":1}}
]},
{"type":"heading","attrs":{"level":1,"color":"red"},"content":[
{"type":"text","text":"b","marks":[{"type":"bold"},{"type":"glow"},{"type":"link","attrs":{"href":"u","x":1}}]}
]}
]}"#);
let v = d.validate(&schema);
let ic = v
.iter()
.find(|x| matches!(x.kind, ViolationKind::InvalidContent { .. }))
.unwrap();
assert_eq!((ic.path.as_slice(), ic.child), (&[0][..], Some(1)));
let ua = v
.iter()
.find(|x| {
x.kind
== ViolationKind::UnknownAttr {
key: "color".into(),
}
})
.unwrap();
assert_eq!((ua.path.as_slice(), ua.mark), (&[1][..], None));
let um = v
.iter()
.find(|x| x.kind == ViolationKind::UnknownMark("glow".into()))
.unwrap();
assert_eq!((um.path.as_slice(), um.mark), (&[1, 0][..], Some(1)));
let ma = v
.iter()
.find(|x| x.kind == ViolationKind::UnknownAttr { key: "x".into() })
.unwrap();
assert_eq!(ma.mark, Some(2));
assert_eq!(v.len(), 4, "{v:?}");
}
#[test]
fn invalid_content_at_end_has_no_child() {
let schema = Schema::new()
.node("doc", NodeSpec::new().content_match("paragraph+"))
.node("paragraph", NodeSpec::new());
let v = doc(r#"{"type":"doc"}"#).validate(&schema);
assert_eq!(v.len(), 1);
assert_eq!(v[0].child, None);
}
#[test]
fn unknown_child_reported_once() {
let expr_schema = Schema::new()
.node("doc", NodeSpec::new().content_match("block"))
.node("paragraph", NodeSpec::new().group("block"));
let v = doc(r#"{"type":"doc","content":[{"type":"widget"}]}"#).validate(&expr_schema);
assert_eq!(v.len(), 1, "{v:?}");
assert_eq!(v[0].kind, ViolationKind::UnknownNodeType("widget".into()));
let v = doc(r#"{"type":"doc","content":[{"type":"widget"},{"type":"widget"}]}"#)
.validate(&expr_schema);
assert_eq!(v.len(), 3, "{v:?}");
assert_eq!(v[0].child, Some(1));
let v = doc(r#"{"type":"doc","content":[{"type":"widget"}]}"#).validate(&schema());
assert_eq!(v.len(), 1, "{v:?}");
}
#[test]
fn violation_json_omits_empty_locators() {
let v = doc(r#"{"type":"doc","content":[{"type":"widget"}]}"#).validate(&schema());
let json = serde_json::to_string(&v[0]).unwrap();
assert!(!json.contains("child") && !json.contains("mark"), "{json}");
}