oficina / docs Public

Clone
git clone https://amadan.net/oficina/docs

Plain git — no account needed to clone.

Download

Download this file

1//! Native editor CRDT operations. In production this crate runs ONLY inside
2//! a fuel/memory-limited WebAssembly interpreter, never as a native decoder.
3//! The server retains its separate ABI; both modules share the reviewed yrs deletion backports.
4mod projection;
5mod operations;
6mod structure;
7mod display;
8mod paste;
9mod lists;
10mod input_rules;
11use serde_json::{json, Value};
12use std::{cell::RefCell, collections::HashSet, sync::Arc};
13use yrs::{Doc, OffsetKind, Options, ReadTxn, StateVector, Transact, UndoManager, Update};
14use yrs::updates::decoder::{Decode, Decoder, DecoderV1};
15use yrs::updates::encoder::Encode;
16
17pub struct Editor {
18 doc: Doc,
19 undo: UndoManager,
20}
21impl Editor {
22 pub fn new(guid: &str, client_id: u64) -> Self {
23 let mut options = Options::with_guid_and_client_id(guid.into(), client_id);
24 options.offset_kind = OffsetKind::Utf16;
25 let doc = Doc::with_options(options);
26 let root = doc.get_or_insert_xml_fragment("prosemirror");
27 let undo = UndoManager::with_scope_and_options(&doc, &root, yrs::undo::Options {
28 capture_timeout_millis: 0,
29 tracked_origins: HashSet::from(["native-edit".into()]),
30 capture_transaction: None,
31 timestamp: Arc::new(|| 1),
32 });
33 Self {doc, undo}
34 }
35 pub fn apply(&self, bytes: &[u8]) -> Result<(), &'static str> { self.apply_origin(bytes, false) }
36 pub fn apply_local(&self, bytes: &[u8]) -> Result<(), &'static str> { self.apply_origin(bytes, true) }
37 fn apply_origin(&self, bytes: &[u8], local: bool) -> Result<(), &'static str> {
38 let updates = decode_updates(bytes)?;
39 let mut txn = self.doc.transact_mut_with(if local { "native-edit" } else { "remote" });
40 for update in updates { txn.apply_update(update).map_err(|_| "invalid_update")?; }
41 Ok(())
42 }
43 pub fn snapshot(&self) -> Vec<u8> {
44 self.doc.transact().encode_state_as_update_v1(&StateVector::default())
45 }
46 pub fn projection(&self) -> Value {
47 let root = self.doc.get_or_insert_xml_fragment("prosemirror");
48 let txn = self.doc.transact();
49 json!({"children": projection::children(&root, &txn), "canUndo": !self.undo.undo_stack().is_empty(), "canRedo": !self.undo.redo_stack().is_empty()})
50 }
51 pub fn anchor(&self, at: u32) -> Result<Value, &'static str> { display::anchor(&self.doc, at) }
52 pub fn resolve(&self, anchor: &Value) -> Result<u32, &'static str> { display::resolve(&self.doc, anchor) }
53 pub fn command(&mut self, request: &Value) -> Result<Vec<u8>, &'static str> {
54 let before = self.doc.transact().state_vector();
55 match request.get("op").and_then(Value::as_str).ok_or("invalid_command")? {
56 "batch" => {
57 let commands = request["commands"].as_array().filter(|v| v.len() <= 1000).ok_or("invalid_command")?;
58 if commands.iter().any(|c| ["batch","undo","redo"].contains(&c["op"].as_str().unwrap_or(""))) { return Err("invalid_command"); }
59 let mut affected = HashSet::new();
60 for command in commands {
61 let target = lists::normalization_target(&self.doc, command);
62 self.command(command)?;
63 if let Some(id) = target.and_then(|t| lists::normalized_identity(&self.doc, t)) { affected.insert(id); }
64 }
65 lists::normalize(&self.doc, &mut affected);
66 },
67 "undo" => { self.undo.undo_blocking(); },
68 "redo" => { self.undo.redo_blocking(); },
69 "display_autoformat" | "display_revert_rule" | "display_exit_list" => input_rules::execute(&self.doc, request)?,
70 "list_indent" => lists::execute(&self.doc, request)?,
71 "display_paste" => paste::execute(&self.doc, request)?,
72 "display_edit" => display::execute(&self.doc, request)?,
73 "block" if ["bullet_list","ordered_list"].contains(&request["type"].as_str().unwrap_or("")) && lists::contains(&self.doc, request["node"].as_str().unwrap_or("")) => lists::toggle(&self.doc, request)?,
74 "block" | "split" | "hard_break" | "join_range" => structure::execute(&self.doc, request)?,
75 _ => operations::execute(&self.doc, request)?,
76 }
77 Ok(self.doc.transact().encode_state_as_update_v1(&before))
78 }
79}
80
81thread_local! {
82 static EDITOR: RefCell<Option<Editor>> = const { RefCell::new(None) };
83 static OUTPUT: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
84}
85
86// All pointers are private WASM linear-memory offsets passed by the interpreter.
87// No API in this module may be called with native pointers by an Apple process.
88#[cfg(target_arch = "wasm32")]
89#[no_mangle]
90pub extern "C" fn docs_editor_alloc(len: u32) -> u32 {
91 if len > 8 * 1024 * 1024 + 1 { return 0; }
92 Box::into_raw(vec![0u8; len as usize].into_boxed_slice()) as *mut u8 as u32
93}
94#[cfg(target_arch = "wasm32")]
95#[no_mangle]
96pub unsafe extern "C" fn docs_editor_free(ptr: u32, len: u32) {
97 if ptr != 0 { drop(Box::from_raw(std::ptr::slice_from_raw_parts_mut(ptr as *mut u8, len as usize))); }
98}
99#[cfg(target_arch = "wasm32")]
100#[no_mangle]
101pub unsafe extern "C" fn docs_editor_call(ptr: u32, len: u32) -> i32 {
102 let end = (ptr as u64) + (len as u64);
103 if len > 8 * 1024 * 1024 + 1 || end > (core::arch::wasm32::memory_size(0) * 65536) as u64 { return -1; }
104 let bytes = std::slice::from_raw_parts(ptr as *const u8, len as usize);
105 let result = dispatch(bytes);
106 match result {
107 Ok(bytes) => { OUTPUT.with(|out| *out.borrow_mut() = bytes); 0 },
108 Err(code) => { OUTPUT.with(|out| *out.borrow_mut() = code.as_bytes().to_vec()); -1 },
109 }
110}
111#[cfg(target_arch = "wasm32")]
112#[no_mangle]
113pub extern "C" fn docs_editor_output_ptr() -> u32 { OUTPUT.with(|out| out.borrow().as_ptr() as u32) }
114#[cfg(target_arch = "wasm32")]
115#[no_mangle]
116pub extern "C" fn docs_editor_output_len() -> u32 { OUTPUT.with(|out| out.borrow().len() as u32) }
117
118fn decode_updates(mut remaining: &[u8]) -> Result<Vec<Update>, &'static str> {
119 let mut updates = Vec::new();
120 while !remaining.is_empty() {
121 let mut decoder = DecoderV1::from(remaining);
122 updates.push(Update::decode(&mut decoder).map_err(|_| "invalid_update")?);
123 let trailing = decoder.read_to_end().map_err(|_| "invalid_update")?.len();
124 if trailing >= remaining.len() { return Err("invalid_update"); }
125 remaining = &remaining[remaining.len() - trailing..];
126 }
127 Ok(updates)
128}
129
130pub fn dispatch(bytes: &[u8]) -> Result<Vec<u8>, &'static str> {
131 // Merge pending writes without constructing a document: pending structs may
132 // depend on state already held by the server. Snapshotting an empty document
133 // would silently lose those structs and deletes.
134 if bytes.first() == Some(&253) {
135 let updates = decode_updates(&bytes[1..])?;
136 if updates.is_empty() { return Err("invalid_update"); }
137 return Ok(Update::merge_updates(updates).encode_v1());
138 }
139 // Binary envelope avoids expanding an 8MiB log into JSON byte numbers.
140 if matches!(bytes.first(), Some(254 | 255)) {
141 return EDITOR.with(|cell| {
142 cell.borrow().as_ref().ok_or("not_open")?.apply_origin(&bytes[1..], bytes[0] == 254)?;
143 Ok(Vec::new())
144 });
145 }
146 let request: Value = serde_json::from_slice(bytes).map_err(|_| "invalid_command")?;
147 EDITOR.with(|cell| {
148 let mut state = cell.borrow_mut();
149 if request["op"] == "open" {
150 let guid = request["guid"].as_str().ok_or("invalid_identity")?;
151 let client = request["client"].as_u64().filter(|v| *v > 0 && *v <= u32::MAX as u64).ok_or("invalid_identity")?;
152 *state = Some(Editor::new(guid, client));
153 return Ok(Vec::new());
154 }
155 let editor = state.as_mut().ok_or("not_open")?;
156 match request["op"].as_str() {
157 Some("apply") => {
158 let bytes = request["bytes"].as_array().ok_or("invalid_update")?.iter()
159 .map(|v| v.as_u64().filter(|n| *n <= 255).map(|n| n as u8).ok_or("invalid_update"))
160 .collect::<Result<Vec<_>, _>>()?;
161 editor.apply(&bytes)?; Ok(Vec::new())
162 },
163 Some("snapshot") => Ok(editor.snapshot()),
164 Some("anchor") => serde_json::to_vec(&editor.anchor(request["at"].as_u64().and_then(|v| u32::try_from(v).ok()).ok_or("invalid_range")?)?).map_err(|_| "invalid_anchor"),
165 Some("resolve") => serde_json::to_vec(&editor.resolve(&request["anchor"])?).map_err(|_| "invalid_anchor"),
166 Some("clients") => serde_json::to_vec(&editor.doc.transact().state_vector().iter().map(|(id, _)| *id).collect::<Vec<_>>()).map_err(|_| "invalid_projection"),
167 Some("project") => serde_json::to_vec(&editor.projection()).map_err(|_| "invalid_projection"),
168 _ => editor.command(&request),
169 }
170 })
171}
172
173#[cfg(target_arch = "wasm32")]
174#[no_mangle]
175extern "Rust" fn __getrandom_v03_custom(_dest: *mut u8, _len: usize) -> Result<(), getrandom::Error> {
176 Err(getrandom::Error::UNSUPPORTED)
177}
178