1use std::collections::{BTreeMap, BTreeSet, HashMap};
21use yrs::{Any, Out, ReadTxn, Xml};
22
23#[derive(Debug)]
31pub enum Segment {
32 Html(String),
33 Deferred {
34 node_type: String,
35 attrs_json: String,
36 child_types: Vec<String>,
37 content: Vec<Segment>,
38 },
39}
40
41pub struct Emitter {
45 frames: Vec<Vec<Segment>>,
46}
47
48impl Default for Emitter {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl Emitter {
55 pub fn new() -> Self {
56 Emitter {
57 frames: vec![Vec::new()],
58 }
59 }
60
61 pub fn push_str(&mut self, s: &str) {
62 if s.is_empty() {
63 return;
64 }
65 let frame = self.frames.last_mut().expect("emitter frame");
66 if let Some(Segment::Html(last)) = frame.last_mut() {
67 last.push_str(s);
68 } else {
69 frame.push(Segment::Html(s.to_string()));
70 }
71 }
72
73 pub fn push(&mut self, c: char) {
74 let mut buf = [0u8; 4];
75 self.push_str(c.encode_utf8(&mut buf));
76 }
77
78 pub fn begin_frame(&mut self) {
80 self.frames.push(Vec::new());
81 }
82
83 pub fn end_frame(&mut self) -> Vec<Segment> {
85 debug_assert!(self.frames.len() > 1, "unbalanced emitter frame");
86 self.frames.pop().unwrap_or_default()
87 }
88
89 pub fn append(&mut self, segments: Vec<Segment>) {
91 for seg in segments {
92 match seg {
93 Segment::Html(s) => self.push_str(&s),
94 deferred => self
95 .frames
96 .last_mut()
97 .expect("emitter frame")
98 .push(deferred),
99 }
100 }
101 }
102
103 pub fn emit_deferred(
104 &mut self,
105 node_type: String,
106 attrs_json: String,
107 child_types: Vec<String>,
108 content: Vec<Segment>,
109 ) {
110 self.frames
111 .last_mut()
112 .expect("emitter frame")
113 .push(Segment::Deferred {
114 node_type,
115 attrs_json,
116 child_types,
117 content,
118 });
119 }
120
121 pub fn into_segments(mut self) -> Vec<Segment> {
122 debug_assert_eq!(self.frames.len(), 1, "unbalanced emitter frame");
123 self.frames.pop().unwrap_or_default()
124 }
125}
126
127pub enum Flattened {
131 Html(String),
132 Deferred(Vec<Segment>),
133}
134
135impl Flattened {
136 #[cfg_attr(not(test), allow(dead_code))]
141 pub fn into_html(self) -> Option<String> {
142 match self {
143 Flattened::Html(html) => Some(html),
144 Flattened::Deferred(_) => None,
145 }
146 }
147}
148
149pub fn flatten(segments: Vec<Segment>) -> Flattened {
153 if segments
154 .iter()
155 .any(|s| matches!(s, Segment::Deferred { .. }))
156 {
157 return Flattened::Deferred(segments);
158 }
159 let mut out = String::new();
162 for seg in segments {
163 if let Segment::Html(s) = seg {
164 if out.is_empty() {
165 out = s;
166 } else {
167 out.push_str(&s);
168 }
169 }
170 }
171 Flattened::Html(out)
172}
173
174pub enum AttrPart {
177 Lit(String),
178 Ref(String),
179}
180
181pub fn resolve_parts<F: Fn(&str) -> Option<String>>(
185 parts: &[AttrPart],
186 lookup: F,
187) -> Option<String> {
188 let mut out = String::new();
189 for part in parts {
190 match part {
191 AttrPart::Lit(s) => out.push_str(s),
192 AttrPart::Ref(name) => {
193 if let Some(v) = lookup(name) {
194 out.push_str(&v);
195 }
196 }
197 }
198 }
199 if out.is_empty() { None } else { Some(out) }
200}
201
202pub fn xml_ref_attr<T: ReadTxn, N: Xml>(txn: &T, node: &N, name: &str) -> Option<String> {
206 let value = |out: Option<Out>| match out {
207 Some(Out::Any(any)) => any_attr_string(&any),
208 _ => None,
209 };
210 value(node.get_attribute(txn, name))
211 .or_else(|| value(node.get_attribute(txn, &format!("__{name}"))))
212}
213
214pub fn any_attr_string(any: &Any) -> Option<String> {
217 match any {
218 Any::String(s) => Some(s.to_string()),
219 Any::Number(n) => Some(if n.fract() == 0.0 {
220 format!("{}", *n as i64)
221 } else {
222 format!("{n}")
223 }),
224 Any::BigInt(n) => Some(format!("{n}")),
225 Any::Bool(b) => Some(if *b { "true" } else { "false" }.to_string()),
226 _ => None,
227 }
228}
229
230pub fn xml_attrs_json<T: ReadTxn, N: Xml>(txn: &T, node: &N) -> String {
234 let mut out = String::from("{");
235 let mut first = true;
236 for (key, value) in node.attributes(txn) {
237 let Out::Any(any) = value else { continue };
238 if !first {
239 out.push(',');
240 }
241 first = false;
242 out.push_str(&serde_json::to_string(key).unwrap_or_else(|_| "\"\"".into()));
243 out.push(':');
244 let mut v = String::new();
245 any.to_json(&mut v);
246 out.push_str(&v);
247 }
248 out.push('}');
249 out
250}
251
252#[derive(Clone, Copy, PartialEq)]
254pub enum Content {
255 Blocks,
256 Inline,
257 None,
258}
259
260pub enum NodeRule {
262 Declarative {
265 tag: String,
266 void: bool,
267 attrs: Vec<(String, Vec<AttrPart>)>,
268 text: Option<Vec<AttrPart>>,
269 content: Content,
270 },
271 Callback { content: Content },
274}
275
276pub struct MarkRule {
279 pub tag: String,
280 pub attrs: Vec<(String, Vec<AttrPart>)>,
281}
282
283pub struct Rules {
284 pub nodes: HashMap<String, NodeRule>,
285 pub marks: HashMap<String, MarkRule>,
286}
287
288#[derive(Default)]
292pub struct TypeInfo {
293 pub count: usize,
294 pub attrs: BTreeSet<String>,
295 pub children: BTreeSet<String>,
296 pub text: bool,
297}
298
299pub type TypeMap = BTreeMap<String, TypeInfo>;
301
302pub fn type_map_json(map: &TypeMap, handled: impl Fn(&str) -> Option<&'static str>) -> String {
306 let mut root = serde_json::Map::new();
307 for (ty, info) in map {
308 let mut entry = serde_json::Map::new();
309 entry.insert("count".into(), info.count.into());
310 entry.insert(
311 "attrs".into(),
312 info.attrs.iter().cloned().collect::<Vec<_>>().into(),
313 );
314 entry.insert(
315 "children".into(),
316 info.children.iter().cloned().collect::<Vec<_>>().into(),
317 );
318 entry.insert("text".into(), info.text.into());
319 entry.insert(
320 "handled".into(),
321 match handled(ty) {
322 Some(by) => by.into(),
323 None => serde_json::Value::Null,
324 },
325 );
326 root.insert(ty.clone(), entry.into());
327 }
328 serde_json::Value::Object(root).to_string()
329}
330
331impl Rules {
332 pub fn empty() -> Self {
333 Rules {
334 nodes: HashMap::new(),
335 marks: HashMap::new(),
336 }
337 }
338
339 pub fn parse(json: &str) -> Result<Rules, String> {
353 let root: serde_json::Value =
354 serde_json::from_str(json).map_err(|e| format!("invalid rules JSON: {e}"))?;
355 let mut rules = Rules::empty();
356
357 if let Some(nodes) = root.get("nodes").and_then(|v| v.as_object()) {
358 for (name, spec) in nodes {
359 rules
360 .nodes
361 .insert(name.clone(), parse_node_rule(name, spec)?);
362 }
363 }
364 if let Some(marks) = root.get("marks").and_then(|v| v.as_object()) {
365 for (name, spec) in marks {
366 rules
367 .marks
368 .insert(name.clone(), parse_mark_rule(name, spec)?);
369 }
370 }
371 Ok(rules)
372 }
373}
374
375fn parse_node_rule(name: &str, spec: &serde_json::Value) -> Result<NodeRule, String> {
376 let content = match spec.get("content").and_then(|v| v.as_str()) {
377 Some("blocks") => Content::Blocks,
378 Some("inline") | None => Content::Inline,
379 Some("none") => Content::None,
380 Some(other) => {
381 return Err(format!(
382 "rule for {name:?}: unknown content kind {other:?} (blocks|inline|none)"
383 ));
384 }
385 };
386 if spec
387 .get("callback")
388 .and_then(|v| v.as_bool())
389 .unwrap_or(false)
390 {
391 return Ok(NodeRule::Callback { content });
392 }
393 let Some(tag) = spec.get("tag").and_then(|v| v.as_str()) else {
394 return Err(format!("rule for {name:?} needs a tag (or a callback)"));
395 };
396 Ok(NodeRule::Declarative {
397 tag: tag.to_string(),
398 void: spec.get("void").and_then(|v| v.as_bool()).unwrap_or(false),
399 attrs: parse_attrs(name, spec.get("attrs"))?,
400 text: match spec.get("text") {
401 Some(serde_json::Value::Array(parts)) => Some(parse_parts(name, parts)?),
402 Some(serde_json::Value::Null) | None => None,
403 Some(_) => return Err(format!("rule for {name:?}: text must be a template array")),
404 },
405 content,
406 })
407}
408
409fn parse_mark_rule(name: &str, spec: &serde_json::Value) -> Result<MarkRule, String> {
410 let Some(tag) = spec.get("tag").and_then(|v| v.as_str()) else {
411 return Err(format!("mark rule for {name:?} needs a tag"));
412 };
413 Ok(MarkRule {
414 tag: tag.to_string(),
415 attrs: parse_attrs(name, spec.get("attrs"))?,
416 })
417}
418
419fn parse_attrs(
420 name: &str,
421 attrs: Option<&serde_json::Value>,
422) -> Result<Vec<(String, Vec<AttrPart>)>, String> {
423 let mut out = Vec::new();
424 let entries = match attrs {
425 None | Some(serde_json::Value::Null) => return Ok(out),
426 Some(serde_json::Value::Array(entries)) => entries,
427 Some(_) => {
428 return Err(format!(
429 "rule for {name:?}: attrs must be an array of [name, template] pairs"
430 ));
431 }
432 };
433 for entry in entries {
434 let (Some(attr_name), Some(serde_json::Value::Array(parts))) =
435 (entry.get(0).and_then(|v| v.as_str()), entry.get(1))
436 else {
437 return Err(format!("rule for {name:?}: malformed attrs entry"));
438 };
439 out.push((attr_name.to_string(), parse_parts(name, parts)?));
440 }
441 Ok(out)
442}
443
444fn parse_parts(name: &str, parts: &[serde_json::Value]) -> Result<Vec<AttrPart>, String> {
445 parts
446 .iter()
447 .map(|part| {
448 if let Some(lit) = part.get("lit").and_then(|v| v.as_str()) {
449 Ok(AttrPart::Lit(lit.to_string()))
450 } else if let Some(r) = part.get("ref").and_then(|v| v.as_str()) {
451 Ok(AttrPart::Ref(r.to_string()))
452 } else {
453 Err(format!(
454 "rule for {name:?}: template part must be lit or ref"
455 ))
456 }
457 })
458 .collect()
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn parses_the_compiled_rule_shape() {
467 let rules = Rules::parse(
468 r#"{ "nodes": { "callout": { "tag": "aside",
469 "attrs": [["class", [{"lit": "callout"}]],
470 ["data-kind", [{"ref": "kind"}]]],
471 "content": "blocks" },
472 "video": { "callback": true } },
473 "marks": { "comment": { "tag": "span",
474 "attrs": [["data-id", [{"ref": "id"}]]] } } }"#,
475 )
476 .unwrap();
477 assert_eq!(rules.nodes.len(), 2);
478 let NodeRule::Declarative {
479 tag,
480 attrs,
481 content,
482 ..
483 } = &rules.nodes["callout"]
484 else {
485 panic!("callout should be declarative");
486 };
487 assert_eq!(tag, "aside");
488 assert!(matches!(content, Content::Blocks));
489 assert_eq!(attrs.len(), 2);
490 assert!(matches!(rules.nodes["video"], NodeRule::Callback { .. }));
491 assert_eq!(rules.marks["comment"].tag, "span");
492 }
493
494 #[test]
495 fn rejects_malformed_rules_loudly() {
496 assert!(Rules::parse("not json").is_err());
497 assert!(Rules::parse(r#"{ "nodes": { "x": {} } }"#).is_err()); assert!(Rules::parse(r#"{ "nodes": { "x": { "tag": "a", "content": "wat" } } }"#).is_err());
499 assert!(Rules::parse(r#"{ "marks": { "x": {} } }"#).is_err());
500 assert!(
503 Rules::parse(r#"{ "nodes": { "x": { "tag": "a", "attrs": {"class": "y"} } } }"#)
504 .is_err()
505 );
506 }
507
508 #[test]
509 fn emitter_frames_capture_and_merge() {
510 let mut em = Emitter::new();
511 em.push_str("<p>");
512 em.begin_frame();
513 em.push_str("inner");
514 let captured = em.end_frame();
515 em.emit_deferred("video".into(), "{}".into(), Vec::new(), captured);
516 em.push_str("</p>");
517 let segs = em.into_segments();
518 assert_eq!(segs.len(), 3);
519 assert!(matches!(&segs[0], Segment::Html(s) if s == "<p>"));
520 assert!(matches!(&segs[1], Segment::Deferred { node_type, .. } if node_type == "video"));
521 assert!(matches!(&segs[2], Segment::Html(s) if s == "</p>"));
522
523 let mut em = Emitter::new();
525 em.push_str("a");
526 em.push_str("b");
527 let segs = em.into_segments();
528 assert_eq!(segs.len(), 1);
529 assert_eq!(flatten(segs).into_html().unwrap(), "ab");
530 }
531}