oficina / docs Public

Plan 4: correct the yrs API surface against 0.23.5's own source

XmlOut, not XmlNode; get_attribute already returns Option<String>.
Verified in the registry source rather than recalled.
Paul Campbell pushed by paul@keymail.dev 79d6a753900f00b07b6ce1e73647df48f7368bb7 parent 5be20b3
1 file changed, +21 −11
  • docs/superpowers/plans/2026-08-31-editing.md +21 −11
diff --git a/docs/superpowers/plans/2026-08-31-editing.md b/docs/superpowers/plans/2026-08-31-editing.md
index 495df33..fc52858 100644
--- a/docs/superpowers/plans/2026-08-31-editing.md
+++ b/docs/superpowers/plans/2026-08-31-editing.md
@@ -259,7 +259,7 @@ Walk the fragment recursively. Two rules matter and both are pinned by tests abo
//! loss, not a degradation.
use std::fmt::Write as _;
-use yrs::types::xml::{XmlFragment, XmlNode, XmlTextRef};
+use yrs::types::xml::{XmlFragment, XmlOut, XmlTextRef};
use yrs::{Doc, GetString, ReadTxn, Transact, Xml, XmlFragmentRef};
/// The CRDT root name. y-prosemirror's default, and part of the wire
@@ -311,10 +311,10 @@ fn text_html<T: ReadTxn>(txn: &T, t: &XmlTextRef, out: &mut String) {
}
}
-fn node_html<T: ReadTxn>(txn: &T, node: &XmlNode, out: &mut String) {
+fn node_html<T: ReadTxn>(txn: &T, node: &XmlOut, out: &mut String) {
match node {
- XmlNode::Text(t) => text_html(txn, t, out),
- XmlNode::Element(e) => {
+ XmlOut::Text(t) => text_html(txn, t, out),
+ XmlOut::Element(e) => {
let name = e.tag().to_string();
let (open, close) = match name.as_str() {
"paragraph" => ("<p>".to_string(), "</p>".to_string()),
@@ -323,7 +323,7 @@ fn node_html<T: ReadTxn>(txn: &T, node: &XmlNode, out: &mut String) {
// <h0> or <h99>.
let level = e
.get_attribute(txn, "level")
- .and_then(|v| v.to_string().parse::<u8>().ok())
+ .and_then(|v| v.parse::<u8>().ok())
.unwrap_or(1)
.clamp(1, 6);
(format!("<h{level}>"), format!("</h{level}>"))
@@ -342,7 +342,7 @@ fn node_html<T: ReadTxn>(txn: &T, node: &XmlNode, out: &mut String) {
}
out.push_str(&close);
}
- XmlNode::Fragment(f) => {
+ XmlOut::Fragment(f) => {
for child in f.children(txn) {
node_html(txn, &child, out);
}
@@ -362,10 +362,10 @@ pub fn fragment_html(doc: &Doc) -> String {
out
}
-fn node_text<T: ReadTxn>(txn: &T, node: &XmlNode, blocks: &mut Vec<String>, cur: &mut String) {
+fn node_text<T: ReadTxn>(txn: &T, node: &XmlOut, blocks: &mut Vec<String>, cur: &mut String) {
match node {
- XmlNode::Text(t) => cur.push_str(&t.get_string(txn)),
- XmlNode::Element(e) => {
+ XmlOut::Text(t) => cur.push_str(&t.get_string(txn)),
+ XmlOut::Element(e) => {
let name = e.tag().to_string();
let is_block = matches!(
name.as_str(),
@@ -378,7 +378,7 @@ fn node_text<T: ReadTxn>(txn: &T, node: &XmlNode, blocks: &mut Vec<String>, cur:
blocks.push(std::mem::take(cur));
}
}
- XmlNode::Fragment(f) => {
+ XmlOut::Fragment(f) => {
for child in f.children(txn) {
node_text(txn, &child, blocks, cur);
}
@@ -1134,4 +1134,14 @@ git commit -m "app: edit a document — ProseMirror bound to the shared fragment
**Type consistency.** `fragment_text`/`fragment_html` (Rust) → `docs_core_html_len`/`docs_core_html_read` (ABI) → `(*Handle).HTML` (Go host) → `(*Store).HTML` returning `template.HTML` (app) → `{{.HTML}}` (template). The CRDT root is `"prosemirror"` in `fragment.rs`, `generate-fragment.mjs`, `generate-seed.mjs` and `editor.js`. `maxUpdateBytes` and `maxLogBytes` are defined in Task 5 and referenced only there.
-**Risk to watch.** Task 1's `yrs` 0.23.5 API surface for `diff`, `children` and `get_attribute` may differ from what is written above. The assertions are the contract; adapt the call sites, not the tests.
+**API surface, verified against `yrs` 0.23.5's own source before this plan was committed** (`~/.cargo/registry/src/*/yrs-0.23.5`), because writing Rust against a half-remembered API is how a plan wastes an implementer's afternoon:
+
+- `Doc::get_or_insert_xml_fragment(name) -> XmlFragmentRef` — exists, `doc.rs:253`.
+- The child enum is **`XmlOut`**, not `XmlNode` — `types/xml.rs:89`, variants `Element(XmlElementRef)`, `Fragment(XmlFragmentRef)`, `Text(XmlTextRef)`. The code above uses `XmlOut`.
+- `children(txn)` yields `XmlOut` — `types/xml.rs:1223`.
+- `XmlElementRef::tag() -> &Arc<str>` — `types/xml.rs:280`.
+- `get_attribute(txn, name) -> Option<String>` — `types/xml.rs:1026`. It returns the `String` directly, so the heading level parses with `.and_then(|v| v.parse::<u8>().ok())` and no intermediate `to_string()`.
+
+**Still unverified, and the one thing to check first.** `XmlTextRef::diff`'s exact signature and the shape of its returned chunks — this is where marks come from, and `MARKS` handling in `text_html` depends on it. Read `types/xml.rs` and `types/text.rs` before writing that function. If `diff` proves awkward, the fallback is `get_string` plus a separate attribute walk, which loses mark fidelity — take that only after reading, and say so in your report, because the marks test is not optional.
+
+The assertions in Step 2 are the contract; adapt call sites, never the tests.