use std::collections::{BTreeMap, BTreeSet, HashMap};
use yrs::{Any, Out, ReadTxn, Xml};
#[derive(Debug)]
pub enum Segment {
Html(String),
Deferred {
node_type: String,
attrs_json: String,
child_types: Vec<String>,
content: Vec<Segment>,
},
}
pub struct Emitter {
frames: Vec<Vec<Segment>>,
}
impl Default for Emitter {
fn default() -> Self {
Self::new()
}
}
impl Emitter {
pub fn new() -> Self {
Emitter {
frames: vec![Vec::new()],
}
}
pub fn push_str(&mut self, s: &str) {
if s.is_empty() {
return;
}
let frame = self.frames.last_mut().expect("emitter frame");
if let Some(Segment::Html(last)) = frame.last_mut() {
last.push_str(s);
} else {
frame.push(Segment::Html(s.to_string()));
}
}
pub fn push(&mut self, c: char) {
let mut buf = [0u8; 4];
self.push_str(c.encode_utf8(&mut buf));
}
pub fn begin_frame(&mut self) {
self.frames.push(Vec::new());
}
pub fn end_frame(&mut self) -> Vec<Segment> {
debug_assert!(self.frames.len() > 1, "unbalanced emitter frame");
self.frames.pop().unwrap_or_default()
}
pub fn append(&mut self, segments: Vec<Segment>) {
for seg in segments {
match seg {
Segment::Html(s) => self.push_str(&s),
deferred => self
.frames
.last_mut()
.expect("emitter frame")
.push(deferred),
}
}
}
pub fn emit_deferred(
&mut self,
node_type: String,
attrs_json: String,
child_types: Vec<String>,
content: Vec<Segment>,
) {
self.frames
.last_mut()
.expect("emitter frame")
.push(Segment::Deferred {
node_type,
attrs_json,
child_types,
content,
});
}
pub fn into_segments(mut self) -> Vec<Segment> {
debug_assert_eq!(self.frames.len(), 1, "unbalanced emitter frame");
self.frames.pop().unwrap_or_default()
}
}
pub enum Flattened {
Html(String),
Deferred(Vec<Segment>),
}
impl Flattened {
#[cfg_attr(not(test), allow(dead_code))]
pub fn into_html(self) -> Option<String> {
match self {
Flattened::Html(html) => Some(html),
Flattened::Deferred(_) => None,
}
}
}
pub fn flatten(segments: Vec<Segment>) -> Flattened {
if segments
.iter()
.any(|s| matches!(s, Segment::Deferred { .. }))
{
return Flattened::Deferred(segments);
}
let mut out = String::new();
for seg in segments {
if let Segment::Html(s) = seg {
if out.is_empty() {
out = s;
} else {
out.push_str(&s);
}
}
}
Flattened::Html(out)
}
pub enum AttrPart {
Lit(String),
Ref(String),
}
pub fn resolve_parts<F: Fn(&str) -> Option<String>>(
parts: &[AttrPart],
lookup: F,
) -> Option<String> {
let mut out = String::new();
for part in parts {
match part {
AttrPart::Lit(s) => out.push_str(s),
AttrPart::Ref(name) => {
if let Some(v) = lookup(name) {
out.push_str(&v);
}
}
}
}
if out.is_empty() { None } else { Some(out) }
}
pub fn xml_ref_attr<T: ReadTxn, N: Xml>(txn: &T, node: &N, name: &str) -> Option<String> {
let value = |out: Option<Out>| match out {
Some(Out::Any(any)) => any_attr_string(&any),
_ => None,
};
value(node.get_attribute(txn, name))
.or_else(|| value(node.get_attribute(txn, &format!("__{name}"))))
}
pub fn any_attr_string(any: &Any) -> Option<String> {
match any {
Any::String(s) => Some(s.to_string()),
Any::Number(n) => Some(if n.fract() == 0.0 {
format!("{}", *n as i64)
} else {
format!("{n}")
}),
Any::BigInt(n) => Some(format!("{n}")),
Any::Bool(b) => Some(if *b { "true" } else { "false" }.to_string()),
_ => None,
}
}
pub fn xml_attrs_json<T: ReadTxn, N: Xml>(txn: &T, node: &N) -> String {
let mut out = String::from("{");
let mut first = true;
for (key, value) in node.attributes(txn) {
let Out::Any(any) = value else { continue };
if !first {
out.push(',');
}
first = false;
out.push_str(&serde_json::to_string(key).unwrap_or_else(|_| "\"\"".into()));
out.push(':');
let mut v = String::new();
any.to_json(&mut v);
out.push_str(&v);
}
out.push('}');
out
}
#[derive(Clone, Copy, PartialEq)]
pub enum Content {
Blocks,
Inline,
None,
}
pub enum NodeRule {
Declarative {
tag: String,
void: bool,
attrs: Vec<(String, Vec<AttrPart>)>,
text: Option<Vec<AttrPart>>,
content: Content,
},
Callback { content: Content },
}
pub struct MarkRule {
pub tag: String,
pub attrs: Vec<(String, Vec<AttrPart>)>,
}
pub struct Rules {
pub nodes: HashMap<String, NodeRule>,
pub marks: HashMap<String, MarkRule>,
}
#[derive(Default)]
pub struct TypeInfo {
pub count: usize,
pub attrs: BTreeSet<String>,
pub children: BTreeSet<String>,
pub text: bool,
}
pub type TypeMap = BTreeMap<String, TypeInfo>;
pub fn type_map_json(map: &TypeMap, handled: impl Fn(&str) -> Option<&'static str>) -> String {
let mut root = serde_json::Map::new();
for (ty, info) in map {
let mut entry = serde_json::Map::new();
entry.insert("count".into(), info.count.into());
entry.insert(
"attrs".into(),
info.attrs.iter().cloned().collect::<Vec<_>>().into(),
);
entry.insert(
"children".into(),
info.children.iter().cloned().collect::<Vec<_>>().into(),
);
entry.insert("text".into(), info.text.into());
entry.insert(
"handled".into(),
match handled(ty) {
Some(by) => by.into(),
None => serde_json::Value::Null,
},
);
root.insert(ty.clone(), entry.into());
}
serde_json::Value::Object(root).to_string()
}
impl Rules {
pub fn empty() -> Self {
Rules {
nodes: HashMap::new(),
marks: HashMap::new(),
}
}
pub fn parse(json: &str) -> Result<Rules, String> {
let root: serde_json::Value =
serde_json::from_str(json).map_err(|e| format!("invalid rules JSON: {e}"))?;
let mut rules = Rules::empty();
if let Some(nodes) = root.get("nodes").and_then(|v| v.as_object()) {
for (name, spec) in nodes {
rules
.nodes
.insert(name.clone(), parse_node_rule(name, spec)?);
}
}
if let Some(marks) = root.get("marks").and_then(|v| v.as_object()) {
for (name, spec) in marks {
rules
.marks
.insert(name.clone(), parse_mark_rule(name, spec)?);
}
}
Ok(rules)
}
}
fn parse_node_rule(name: &str, spec: &serde_json::Value) -> Result<NodeRule, String> {
let content = match spec.get("content").and_then(|v| v.as_str()) {
Some("blocks") => Content::Blocks,
Some("inline") | None => Content::Inline,
Some("none") => Content::None,
Some(other) => {
return Err(format!(
"rule for {name:?}: unknown content kind {other:?} (blocks|inline|none)"
));
}
};
if spec
.get("callback")
.and_then(|v| v.as_bool())
.unwrap_or(false)
{
return Ok(NodeRule::Callback { content });
}
let Some(tag) = spec.get("tag").and_then(|v| v.as_str()) else {
return Err(format!("rule for {name:?} needs a tag (or a callback)"));
};
Ok(NodeRule::Declarative {
tag: tag.to_string(),
void: spec.get("void").and_then(|v| v.as_bool()).unwrap_or(false),
attrs: parse_attrs(name, spec.get("attrs"))?,
text: match spec.get("text") {
Some(serde_json::Value::Array(parts)) => Some(parse_parts(name, parts)?),
Some(serde_json::Value::Null) | None => None,
Some(_) => return Err(format!("rule for {name:?}: text must be a template array")),
},
content,
})
}
fn parse_mark_rule(name: &str, spec: &serde_json::Value) -> Result<MarkRule, String> {
let Some(tag) = spec.get("tag").and_then(|v| v.as_str()) else {
return Err(format!("mark rule for {name:?} needs a tag"));
};
Ok(MarkRule {
tag: tag.to_string(),
attrs: parse_attrs(name, spec.get("attrs"))?,
})
}
fn parse_attrs(
name: &str,
attrs: Option<&serde_json::Value>,
) -> Result<Vec<(String, Vec<AttrPart>)>, String> {
let mut out = Vec::new();
let entries = match attrs {
None | Some(serde_json::Value::Null) => return Ok(out),
Some(serde_json::Value::Array(entries)) => entries,
Some(_) => {
return Err(format!(
"rule for {name:?}: attrs must be an array of [name, template] pairs"
));
}
};
for entry in entries {
let (Some(attr_name), Some(serde_json::Value::Array(parts))) =
(entry.get(0).and_then(|v| v.as_str()), entry.get(1))
else {
return Err(format!("rule for {name:?}: malformed attrs entry"));
};
out.push((attr_name.to_string(), parse_parts(name, parts)?));
}
Ok(out)
}
fn parse_parts(name: &str, parts: &[serde_json::Value]) -> Result<Vec<AttrPart>, String> {
parts
.iter()
.map(|part| {
if let Some(lit) = part.get("lit").and_then(|v| v.as_str()) {
Ok(AttrPart::Lit(lit.to_string()))
} else if let Some(r) = part.get("ref").and_then(|v| v.as_str()) {
Ok(AttrPart::Ref(r.to_string()))
} else {
Err(format!(
"rule for {name:?}: template part must be lit or ref"
))
}
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_the_compiled_rule_shape() {
let rules = Rules::parse(
r#"{ "nodes": { "callout": { "tag": "aside",
"attrs": [["class", [{"lit": "callout"}]],
["data-kind", [{"ref": "kind"}]]],
"content": "blocks" },
"video": { "callback": true } },
"marks": { "comment": { "tag": "span",
"attrs": [["data-id", [{"ref": "id"}]]] } } }"#,
)
.unwrap();
assert_eq!(rules.nodes.len(), 2);
let NodeRule::Declarative {
tag,
attrs,
content,
..
} = &rules.nodes["callout"]
else {
panic!("callout should be declarative");
};
assert_eq!(tag, "aside");
assert!(matches!(content, Content::Blocks));
assert_eq!(attrs.len(), 2);
assert!(matches!(rules.nodes["video"], NodeRule::Callback { .. }));
assert_eq!(rules.marks["comment"].tag, "span");
}
#[test]
fn rejects_malformed_rules_loudly() {
assert!(Rules::parse("not json").is_err());
assert!(Rules::parse(r#"{ "nodes": { "x": {} } }"#).is_err()); assert!(Rules::parse(r#"{ "nodes": { "x": { "tag": "a", "content": "wat" } } }"#).is_err());
assert!(Rules::parse(r#"{ "marks": { "x": {} } }"#).is_err());
assert!(
Rules::parse(r#"{ "nodes": { "x": { "tag": "a", "attrs": {"class": "y"} } } }"#)
.is_err()
);
}
#[test]
fn emitter_frames_capture_and_merge() {
let mut em = Emitter::new();
em.push_str("<p>");
em.begin_frame();
em.push_str("inner");
let captured = em.end_frame();
em.emit_deferred("video".into(), "{}".into(), Vec::new(), captured);
em.push_str("</p>");
let segs = em.into_segments();
assert_eq!(segs.len(), 3);
assert!(matches!(&segs[0], Segment::Html(s) if s == "<p>"));
assert!(matches!(&segs[1], Segment::Deferred { node_type, .. } if node_type == "video"));
assert!(matches!(&segs[2], Segment::Html(s) if s == "</p>"));
let mut em = Emitter::new();
em.push_str("a");
em.push_str("b");
let segs = em.into_segments();
assert_eq!(segs.len(), 1);
assert_eq!(flatten(segs).into_html().unwrap(), "ab");
}
}