···144145 /// Pending snap direction for cursor restoration after edits.
146 /// Set by input handlers, consumed by cursor restoration.
147- pub pending_snap: Signal<Option<super::offset_map::SnapDirection>>,
148149 /// Collected refs (wikilinks, AT embeds) from the most recent render.
150 /// Updated by the render pipeline, read by publish for populating records.
···144145 /// Pending snap direction for cursor restoration after edits.
146 /// Set by input handlers, consumed by cursor restoration.
147+ pub pending_snap: Signal<Option<weaver_editor_core::SnapDirection>>,
148149 /// Collected refs (wikilinks, AT embeds) from the most recent render.
150 /// Updated by the render pipeline, read by publish for populating records.
···67use super::document::EditorDocument;
8use super::formatting::{self, FormatAction};
9-use super::offset_map::SnapDirection;
1011/// Check if we need to intercept this key event.
12/// Returns true for content-modifying operations, false for navigation.
···67use super::document::EditorDocument;
8use super::formatting::{self, FormatAction};
9+use weaver_editor_core::SnapDirection;
1011/// Check if we need to intercept this key event.
12/// Returns true for content-modifying operations, false for navigation.
+7-4
crates/weaver-app/src/components/editor/mod.rs
···15mod image_upload;
16mod input;
17mod log_buffer;
18-mod offset_map;
19mod paragraph;
20mod platform;
21mod publish;
···54#[allow(unused_imports)]
55pub use formatting::{FormatAction, apply_formatting, find_word_boundaries};
5657-// Rendering
58#[allow(unused_imports)]
59-pub use offset_map::{OffsetMapping, RenderResult, find_mapping_for_byte};
00060#[allow(unused_imports)]
61pub use paragraph::ParagraphRender;
62#[allow(unused_imports)]
63pub use render::{RenderCache, render_paragraphs_incremental};
064#[allow(unused_imports)]
65-pub use writer::{EditorImageResolver, ImageResolver, SyntaxSpanInfo, SyntaxType, WriterResult};
6667// Storage
68#[allow(unused_imports)]
···3//! Paragraphs are discovered during markdown rendering by tracking
4//! Tag::Paragraph events. This allows updating only changed paragraphs in the DOM.
56-use super::offset_map::OffsetMapping;
7-use super::writer::SyntaxSpanInfo;
8use loro::LoroText;
9use std::ops::Range;
01011/// A rendered paragraph with its source range and offset mappings.
12#[derive(Debug, Clone, PartialEq)]
···3//! Paragraphs are discovered during markdown rendering by tracking
4//! Tag::Paragraph events. This allows updating only changed paragraphs in the DOM.
5006use loro::LoroText;
7use std::ops::Range;
8+use weaver_editor_core::{OffsetMapping, SyntaxSpanInfo};
910/// A rendered paragraph with its source range and offset mappings.
11#[derive(Debug, Clone, PartialEq)]
+18-21
crates/weaver-app/src/components/editor/render.rs
···5//! Uses EditorWriter which tracks gaps in offset_iter to preserve formatting characters.
67use super::document::EditInfo;
8-#[allow(unused_imports)]
9-use super::offset_map::{OffsetMapping, RenderResult};
10use super::paragraph::{ParagraphRender, hash_source, make_paragraph_id, text_slice_to_string};
11-#[allow(unused_imports)]
12-use super::writer::{EditorImageResolver, EditorWriter, ImageResolver, SyntaxSpanInfo};
13use loro::LoroText;
14use markdown_weaver::Parser;
15use std::ops::Range;
16use weaver_common::{EntryIndex, ResolvedContent};
0001718/// Cache for incremental paragraph rendering.
19/// Stores previously rendered paragraphs to avoid re-rendering unchanged content.
···413 let parser = Parser::new_ext(¶_source, weaver_renderer::default_md_options())
414 .into_offset_iter();
415416- let para_doc = loro::LoroDoc::new();
417- let para_text = para_doc.get_text("content");
418- let _ = para_text.insert(0, ¶_source);
419420- let mut writer = EditorWriter::<_, &ResolvedContent, &EditorImageResolver>::new(
421- ¶_source,
422- ¶_text,
423- parser,
424- )
425- .with_node_id_prefix(&cached_para.id)
426- .with_image_resolver(&resolver)
427- .with_embed_provider(resolved_content);
0428429 if let Some(idx) = entry_index {
430 writer = writer.with_entry_index(idx);
···613 // Use provided resolver or empty default
614 let resolver = image_resolver.cloned().unwrap_or_default();
615616- // Create a temporary LoroText for the slice (needed by writer)
617- let slice_doc = loro::LoroDoc::new();
618- let slice_text = slice_doc.get_text("content");
619- let _ = slice_text.insert(0, parse_slice);
620621 // Determine starting paragraph ID for freshly parsed paragraphs
622 // This MUST match the IDs we assign later - the writer bakes node ID prefixes into HTML
···665 });
666667 // Build writer with all resolvers and auto-incrementing paragraph prefixes
668- let mut writer = EditorWriter::<_, &ResolvedContent, &EditorImageResolver>::new(
669 parse_slice,
670- &slice_text,
671 parser,
672 )
673 .with_auto_incrementing_prefix(parsed_para_id_start)
···5//! Uses EditorWriter which tracks gaps in offset_iter to preserve formatting characters.
67use super::document::EditInfo;
008use super::paragraph::{ParagraphRender, hash_source, make_paragraph_id, text_slice_to_string};
9+use super::writer::embed::EditorImageResolver;
010use loro::LoroText;
11use markdown_weaver::Parser;
12use std::ops::Range;
13use weaver_common::{EntryIndex, ResolvedContent};
14+use weaver_editor_core::{
15+ EditorRope, EditorWriter, EmbedContentProvider, ImageResolver, OffsetMapping, SyntaxSpanInfo,
16+};
1718/// Cache for incremental paragraph rendering.
19/// Stores previously rendered paragraphs to avoid re-rendering unchanged content.
···413 let parser = Parser::new_ext(¶_source, weaver_renderer::default_md_options())
414 .into_offset_iter();
415416+ let para_rope = EditorRope::from(para_source.as_str());
00417418+ let mut writer =
419+ EditorWriter::<_, _, &ResolvedContent, &EditorImageResolver, ()>::new(
420+ ¶_source,
421+ ¶_rope,
422+ parser,
423+ )
424+ .with_node_id_prefix(&cached_para.id)
425+ .with_image_resolver(&resolver)
426+ .with_embed_provider(resolved_content);
427428 if let Some(idx) = entry_index {
429 writer = writer.with_entry_index(idx);
···612 // Use provided resolver or empty default
613 let resolver = image_resolver.cloned().unwrap_or_default();
614615+ // Create EditorRope for efficient offset conversions
616+ let slice_rope = EditorRope::from(parse_slice);
00617618 // Determine starting paragraph ID for freshly parsed paragraphs
619 // This MUST match the IDs we assign later - the writer bakes node ID prefixes into HTML
···662 });
663664 // Build writer with all resolvers and auto-incrementing paragraph prefixes
665+ let mut writer = EditorWriter::<_, _, &ResolvedContent, &EditorImageResolver, ()>::new(
666 parse_slice,
667+ &slice_rope,
668 parser,
669 )
670 .with_auto_incrementing_prefix(parsed_para_id_start)
···7//!
8//! Implementations are provided by the consuming application (e.g., weaver-app).
90010/// Provides HTML content for embedded resources.
11///
12/// When rendering markdown with embeds (e.g., `![[at://...]]`), this trait
13/// is consulted to get the pre-rendered HTML for the embed.
00014pub trait EmbedContentProvider {
15- /// Get HTML content for an embed URL.
16 ///
17 /// Returns `Some(html)` if the embed content is available,
18 /// `None` to render a placeholder.
19- fn get_embed_html(&self, url: &str) -> Option<&str>;
20}
2122/// Unit type implementation - no embeds available.
23impl EmbedContentProvider for () {
24- fn get_embed_html(&self, _url: &str) -> Option<&str> {
25 None
26 }
27}
···63/// Reference implementations for common patterns.
6465impl<T: EmbedContentProvider> EmbedContentProvider for &T {
66- fn get_embed_html(&self, url: &str) -> Option<&str> {
67- (*self).get_embed_html(url)
68 }
69}
70···81}
8283impl<T: EmbedContentProvider> EmbedContentProvider for Option<T> {
84- fn get_embed_html(&self, url: &str) -> Option<&str> {
85- self.as_ref().and_then(|p| p.get_embed_html(url))
86 }
87}
88···95impl<T: WikilinkValidator> WikilinkValidator for Option<T> {
96 fn is_valid_link(&self, target: &str) -> bool {
97 self.as_ref().map(|v| v.is_valid_link(target)).unwrap_or(true)
00000000000000000098 }
99}
100101#[cfg(test)]
102mod tests {
103 use super::*;
0104105 struct TestEmbedProvider;
106107 impl EmbedContentProvider for TestEmbedProvider {
108- fn get_embed_html(&self, url: &str) -> Option<&str> {
109- if url == "at://test/embed" {
110- Some("<div>Test Embed</div>")
111- } else {
112- None
113 }
0114 }
115 }
116···136 }
137 }
1380000000000139 #[test]
140 fn test_embed_provider() {
141 let provider = TestEmbedProvider;
142 assert_eq!(
143- provider.get_embed_html("at://test/embed"),
144- Some("<div>Test Embed</div>")
145 );
146- assert_eq!(provider.get_embed_html("at://other"), None);
147 }
148149 #[test]
···169 #[test]
170 fn test_unit_impls() {
171 let embed: () = ();
172- assert_eq!(embed.get_embed_html("anything"), None);
173174 let image: () = ();
175 assert_eq!(image.resolve_image_url("anything"), None);
···182 fn test_option_impls() {
183 let some_provider: Option<TestEmbedProvider> = Some(TestEmbedProvider);
184 assert_eq!(
185- some_provider.get_embed_html("at://test/embed"),
186- Some("<div>Test Embed</div>")
187 );
188189 let none_provider: Option<TestEmbedProvider> = None;
190- assert_eq!(none_provider.get_embed_html("at://test/embed"), None);
191 }
192}
···7//!
8//! Implementations are provided by the consuming application (e.g., weaver-app).
910+use markdown_weaver::Tag;
11+12/// Provides HTML content for embedded resources.
13///
14/// When rendering markdown with embeds (e.g., `![[at://...]]`), this trait
15/// is consulted to get the pre-rendered HTML for the embed.
16+///
17+/// The full `Tag::Embed` is provided so implementations can access all context:
18+/// embed_type, dest_url, title, id, and attrs.
19pub trait EmbedContentProvider {
20+ /// Get HTML content for an embed tag.
21 ///
22 /// Returns `Some(html)` if the embed content is available,
23 /// `None` to render a placeholder.
24+ fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String>;
25}
2627/// Unit type implementation - no embeds available.
28impl EmbedContentProvider for () {
29+ fn get_embed_content(&self, _tag: &Tag<'_>) -> Option<String> {
30 None
31 }
32}
···68/// Reference implementations for common patterns.
6970impl<T: EmbedContentProvider> EmbedContentProvider for &T {
71+ fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> {
72+ (*self).get_embed_content(tag)
73 }
74}
75···86}
8788impl<T: EmbedContentProvider> EmbedContentProvider for Option<T> {
89+ fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> {
90+ self.as_ref().and_then(|p| p.get_embed_content(tag))
91 }
92}
93···100impl<T: WikilinkValidator> WikilinkValidator for Option<T> {
101 fn is_valid_link(&self, target: &str) -> bool {
102 self.as_ref().map(|v| v.is_valid_link(target)).unwrap_or(true)
103+ }
104+}
105+106+/// Implementation for ResolvedContent from weaver-common.
107+///
108+/// Resolves AT Protocol embeds by looking up the content in the ResolvedContent map.
109+impl EmbedContentProvider for weaver_common::ResolvedContent {
110+ fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> {
111+ if let Tag::Embed { dest_url, .. } = tag {
112+ let url = dest_url.as_ref();
113+ if url.starts_with("at://") {
114+ if let Ok(at_uri) = jacquard::types::string::AtUri::new(url) {
115+ return weaver_common::ResolvedContent::get_embed_content(self, &at_uri)
116+ .map(|s| s.to_string());
117+ }
118+ }
119+ }
120+ None
121 }
122}
123124#[cfg(test)]
125mod tests {
126 use super::*;
127+ use markdown_weaver::EmbedType;
128129 struct TestEmbedProvider;
130131 impl EmbedContentProvider for TestEmbedProvider {
132+ fn get_embed_content(&self, tag: &Tag<'_>) -> Option<String> {
133+ if let Tag::Embed { dest_url, .. } = tag {
134+ if dest_url.as_ref() == "at://test/embed" {
135+ return Some("<div>Test Embed</div>".to_string());
136+ }
137 }
138+ None
139 }
140 }
141···161 }
162 }
163164+ fn make_embed_tag(url: &str) -> Tag<'_> {
165+ Tag::Embed {
166+ embed_type: EmbedType::Other,
167+ dest_url: url.into(),
168+ title: "".into(),
169+ id: "".into(),
170+ attrs: None,
171+ }
172+ }
173+174 #[test]
175 fn test_embed_provider() {
176 let provider = TestEmbedProvider;
177 assert_eq!(
178+ provider.get_embed_content(&make_embed_tag("at://test/embed")),
179+ Some("<div>Test Embed</div>".to_string())
180 );
181+ assert_eq!(provider.get_embed_content(&make_embed_tag("at://other")), None);
182 }
183184 #[test]
···204 #[test]
205 fn test_unit_impls() {
206 let embed: () = ();
207+ assert_eq!(embed.get_embed_content(&make_embed_tag("anything")), None);
208209 let image: () = ();
210 assert_eq!(image.resolve_image_url("anything"), None);
···217 fn test_option_impls() {
218 let some_provider: Option<TestEmbedProvider> = Some(TestEmbedProvider);
219 assert_eq!(
220+ some_provider.get_embed_content(&make_embed_tag("at://test/embed")),
221+ Some("<div>Test Embed</div>".to_string())
222 );
223224 let none_provider: Option<TestEmbedProvider> = None;
225+ assert_eq!(none_provider.get_embed_content(&make_embed_tag("at://test/embed")), None);
226 }
227}
+1-1
crates/weaver-editor-core/src/text.rs
···10/// A text buffer that supports efficient editing and offset conversion.
11///
12/// All offsets are in Unicode scalar values (chars), not bytes or UTF-16.
13-pub trait TextBuffer: Default + Clone {
14 /// Total length in bytes (UTF-8).
15 fn len_bytes(&self) -> usize;
16
···10/// A text buffer that supports efficient editing and offset conversion.
11///
12/// All offsets are in Unicode scalar values (chars), not bytes or UTF-16.
13+pub trait TextBuffer {
14 /// Total length in bytes (UTF-8).
15 fn len_bytes(&self) -> usize;
16
+13-3
crates/weaver-editor-core/src/undo.rs
···47///
48/// This is the standard way to get undo support for local editing.
49/// All mutations go through this wrapper, which records them for undo.
50-#[derive(Clone)]
51-pub struct UndoableBuffer<T: TextBuffer> {
52 buffer: T,
53 undo_stack: Vec<EditOperation>,
54 redo_stack: Vec<EditOperation>,
55 max_steps: usize,
56}
5758-impl<T: TextBuffer> Default for UndoableBuffer<T> {
0000000000059 fn default() -> Self {
60 Self::new(T::default(), 100)
61 }
···47///
48/// This is the standard way to get undo support for local editing.
49/// All mutations go through this wrapper, which records them for undo.
50+pub struct UndoableBuffer<T> {
051 buffer: T,
52 undo_stack: Vec<EditOperation>,
53 redo_stack: Vec<EditOperation>,
54 max_steps: usize,
55}
5657+impl<T: Clone> Clone for UndoableBuffer<T> {
58+ fn clone(&self) -> Self {
59+ Self {
60+ buffer: self.buffer.clone(),
61+ undo_stack: self.undo_stack.clone(),
62+ redo_stack: self.redo_stack.clone(),
63+ max_steps: self.max_steps,
64+ }
65+ }
66+}
67+68+impl<T: TextBuffer + Default> Default for UndoableBuffer<T> {
69 fn default() -> Self {
70 Self::new(T::default(), 100)
71 }
+20-13
crates/weaver-editor-core/src/writer/embed.rs
···67use jacquard::IntoStatic;
8use jacquard::types::{ident::AtIdentifier, string::Rkey};
9-use markdown_weaver::{CowStr, EmbedType, Event};
10use markdown_weaver_escape::{StrWrite, escape_html};
11use smol_str::SmolStr;
12···65 blob_rkey: Rkey<'static>,
66 ident: AtIdentifier<'static>,
67 ) {
68- self.images
69- .insert(SmolStr::new(name), ResolvedImage::Draft { blob_rkey, ident });
0070 }
7172 /// Add an already-uploaded draft image.
···160}
161162// write_embed implementation
163-impl<'a, I, E, R, W> EditorWriter<'a, I, E, R, W>
164where
0165 I: Iterator<Item = (Event<'a>, Range<usize>)>,
166 E: EmbedContentProvider,
167 R: ImageResolver,
···170 pub(crate) fn write_embed(
171 &mut self,
172 range: Range<usize>,
173- _embed_type: EmbedType,
174- dest_url: CowStr<'_>,
175- title: CowStr<'_>,
176- _id: CowStr<'_>,
177- attrs: Option<markdown_weaver::WeaverAttributes<'_>>,
178 ) -> Result<(), fmt::Error> {
0000000000179 // Embed rendering: all syntax elements share one syn_id for visibility toggling
180 // Structure: ![[ url-as-link ]] <embed-content>
181 let raw_text = &self.source[range.clone()];
···279280 // 4. Emit the actual embed content
281 // Try to get content from attributes first
282- let content_from_attrs = if let Some(ref attrs) = attrs {
283 attrs
284 .attrs
285 .iter()
···290 };
291292 // If no content in attrs, try provider
293- // Convert to owned to avoid borrow checker issues with self.write()
294- // TODO: figure out a way to do this that doesn't involve cloning
295 let content: Option<String> = if content_from_attrs.is_some() {
296 content_from_attrs
297 } else if let Some(ref provider) = self.embed_provider {
298- provider.get_embed_html(url).map(|s| s.to_string())
299 } else {
300 None
301 };
···67use jacquard::IntoStatic;
8use jacquard::types::{ident::AtIdentifier, string::Rkey};
9+use markdown_weaver::{CowStr, Event, Tag};
10use markdown_weaver_escape::{StrWrite, escape_html};
11use smol_str::SmolStr;
12···65 blob_rkey: Rkey<'static>,
66 ident: AtIdentifier<'static>,
67 ) {
68+ self.images.insert(
69+ SmolStr::new(name),
70+ ResolvedImage::Draft { blob_rkey, ident },
71+ );
72 }
7374 /// Add an already-uploaded draft image.
···162}
163164// write_embed implementation
165+impl<'a, T, I, E, R, W> EditorWriter<'a, T, I, E, R, W>
166where
167+ T: crate::TextBuffer,
168 I: Iterator<Item = (Event<'a>, Range<usize>)>,
169 E: EmbedContentProvider,
170 R: ImageResolver,
···173 pub(crate) fn write_embed(
174 &mut self,
175 range: Range<usize>,
176+ tag: Tag<'_>,
0000177 ) -> Result<(), fmt::Error> {
178+ let Tag::Embed {
179+ dest_url,
180+ title,
181+ attrs,
182+ ..
183+ } = &tag
184+ else {
185+ return Ok(());
186+ };
187+188 // Embed rendering: all syntax elements share one syn_id for visibility toggling
189 // Structure: ![[ url-as-link ]] <embed-content>
190 let raw_text = &self.source[range.clone()];
···288289 // 4. Emit the actual embed content
290 // Try to get content from attributes first
291+ let content_from_attrs = if let Some(attrs) = attrs {
292 attrs
293 .attrs
294 .iter()
···299 };
300301 // If no content in attrs, try provider
00302 let content: Option<String> = if content_from_attrs.is_some() {
303 content_from_attrs
304 } else if let Some(ref provider) = self.embed_provider {
305+ provider.get_embed_content(&tag)
306 } else {
307 None
308 };
+3-2
crates/weaver-editor-core/src/writer/events.rs
···14use super::{EditorWriter, WriterResult};
1516// Main run loop
17-impl<'a, I, E, R, W> EditorWriter<'a, I, E, R, W>
18where
019 I: Iterator<Item = (Event<'a>, Range<usize>)>,
20 E: EmbedContentProvider,
21 R: ImageResolver,
···84 // Handle unmapped trailing content (stripped by parser)
85 // This includes trailing spaces that markdown ignores
86 let doc_byte_len = self.source.len();
87- let doc_char_len = self.source_len_chars;
8889 if self.last_byte_offset < doc_byte_len || self.last_char_offset < doc_char_len {
90 // Emit the trailing content as visible syntax
···14use super::{EditorWriter, WriterResult};
1516// Main run loop
17+impl<'a, T, I, E, R, W> EditorWriter<'a, T, I, E, R, W>
18where
19+ T: crate::TextBuffer,
20 I: Iterator<Item = (Event<'a>, Range<usize>)>,
21 E: EmbedContentProvider,
22 R: ImageResolver,
···85 // Handle unmapped trailing content (stripped by parser)
86 // This includes trailing spaces that markdown ignores
87 let doc_byte_len = self.source.len();
88+ let doc_char_len = self.text_buffer.len_chars();
8990 if self.last_byte_offset < doc_byte_len || self.last_char_offset < doc_char_len {
91 // Emit the trailing content as visible syntax
+18-14
crates/weaver-editor-core/src/writer/mod.rs
···94/// HTML writer that preserves markdown formatting characters.
95///
96/// Generic over:
097/// - `I`: Iterator of markdown events with byte ranges
98/// - `E`: Embed content provider (optional)
99/// - `R`: Image resolver (optional)
100/// - `W`: Wikilink validator (optional)
101-pub struct EditorWriter<'a, I, E = (), R = (), W = ()>
102where
0103 I: Iterator<Item = (Event<'a>, Range<usize>)>,
104{
105 // === Input ===
106 source: &'a str,
107- source_len_chars: usize,
108 events: I,
109110 // === Output ===
···146 ref_collector: weaver_common::RefCollector,
147}
148149-impl<'a, I, E, R, W> EditorWriter<'a, I, E, R, W>
150where
0151 I: Iterator<Item = (Event<'a>, Range<usize>)>,
152{
153 /// Create a new EditorWriter.
154 ///
155- /// `source` is the markdown source text.
156- /// `source_len_chars` is the length in Unicode chars (for bounds checking).
157 /// `events` is the markdown parser event iterator.
158- pub fn new(source: &'a str, source_len_chars: usize, events: I) -> Self {
159 Self {
160 source,
161- source_len_chars,
162 events,
163 writer: SegmentedWriter::new(),
164 last_byte_offset: 0,
···232 pub fn with_embed_provider<E2: EmbedContentProvider>(
233 self,
234 provider: E2,
235- ) -> EditorWriter<'a, I, E2, R, W> {
236 EditorWriter {
237 source: self.source,
238- source_len_chars: self.source_len_chars,
239 events: self.events,
240 writer: self.writer,
241 last_byte_offset: self.last_byte_offset,
···268 pub fn with_image_resolver<R2: ImageResolver>(
269 self,
270 resolver: R2,
271- ) -> EditorWriter<'a, I, E, R2, W> {
272 EditorWriter {
273 source: self.source,
274- source_len_chars: self.source_len_chars,
275 events: self.events,
276 writer: self.writer,
277 last_byte_offset: self.last_byte_offset,
···304 pub fn with_wikilink_validator<W2: WikilinkValidator>(
305 self,
306 validator: W2,
307- ) -> EditorWriter<'a, I, E, R, W2> {
308 EditorWriter {
309 source: self.source,
310- source_len_chars: self.source_len_chars,
311 events: self.events,
312 writer: self.writer,
313 last_byte_offset: self.last_byte_offset,
···344}
345346// Core helper methods
347-impl<'a, I, E, R, W> EditorWriter<'a, I, E, R, W>
348where
0349 I: Iterator<Item = (Event<'a>, Range<usize>)>,
350{
351 /// Write a string to the output.
···94/// HTML writer that preserves markdown formatting characters.
95///
96/// Generic over:
97+/// - `T`: Text buffer for efficient offset conversions
98/// - `I`: Iterator of markdown events with byte ranges
99/// - `E`: Embed content provider (optional)
100/// - `R`: Image resolver (optional)
101/// - `W`: Wikilink validator (optional)
102+pub struct EditorWriter<'a, T, I, E = (), R = (), W = ()>
103where
104+ T: crate::TextBuffer,
105 I: Iterator<Item = (Event<'a>, Range<usize>)>,
106{
107 // === Input ===
108 source: &'a str,
109+ text_buffer: &'a T,
110 events: I,
111112 // === Output ===
···148 ref_collector: weaver_common::RefCollector,
149}
150151+impl<'a, T, I, E, R, W> EditorWriter<'a, T, I, E, R, W>
152where
153+ T: crate::TextBuffer,
154 I: Iterator<Item = (Event<'a>, Range<usize>)>,
155{
156 /// Create a new EditorWriter.
157 ///
158+ /// `source` is the markdown source text (should match text_buffer content).
159+ /// `text_buffer` provides efficient offset conversions.
160 /// `events` is the markdown parser event iterator.
161+ pub fn new(source: &'a str, text_buffer: &'a T, events: I) -> Self {
162 Self {
163 source,
164+ text_buffer,
165 events,
166 writer: SegmentedWriter::new(),
167 last_byte_offset: 0,
···235 pub fn with_embed_provider<E2: EmbedContentProvider>(
236 self,
237 provider: E2,
238+ ) -> EditorWriter<'a, T, I, E2, R, W> {
239 EditorWriter {
240 source: self.source,
241+ text_buffer: self.text_buffer,
242 events: self.events,
243 writer: self.writer,
244 last_byte_offset: self.last_byte_offset,
···271 pub fn with_image_resolver<R2: ImageResolver>(
272 self,
273 resolver: R2,
274+ ) -> EditorWriter<'a, T, I, E, R2, W> {
275 EditorWriter {
276 source: self.source,
277+ text_buffer: self.text_buffer,
278 events: self.events,
279 writer: self.writer,
280 last_byte_offset: self.last_byte_offset,
···307 pub fn with_wikilink_validator<W2: WikilinkValidator>(
308 self,
309 validator: W2,
310+ ) -> EditorWriter<'a, T, I, E, R, W2> {
311 EditorWriter {
312 source: self.source,
313+ text_buffer: self.text_buffer,
314 events: self.events,
315 writer: self.writer,
316 last_byte_offset: self.last_byte_offset,
···347}
348349// Core helper methods
350+impl<'a, T, I, E, R, W> EditorWriter<'a, T, I, E, R, W>
351where
352+ T: crate::TextBuffer,
353 I: Iterator<Item = (Event<'a>, Range<usize>)>,
354{
355 /// Write a string to the output.
+2-1
crates/weaver-editor-core/src/writer/syntax.rs
···1112use super::EditorWriter;
1314-impl<'a, I, E, R, W> EditorWriter<'a, I, E, R, W>
15where
016 I: Iterator<Item = (Event<'a>, Range<usize>)>,
17 E: EmbedContentProvider,
18 R: ImageResolver,
···1112use super::EditorWriter;
1314+impl<'a, T, I, E, R, W> EditorWriter<'a, T, I, E, R, W>
15where
16+ T: crate::TextBuffer,
17 I: Iterator<Item = (Event<'a>, Range<usize>)>,
18 E: EmbedContentProvider,
19 R: ImageResolver,
+8-12
crates/weaver-editor-core/src/writer/tags.rs
···1314use super::{EditorWriter, TableState};
1516-impl<'a, I, E, R, W> EditorWriter<'a, I, E, R, W>
17where
018 I: Iterator<Item = (Event<'a>, Range<usize>)>,
19 E: EmbedContentProvider,
20 R: ImageResolver,
···747 if matches!(link_type, LinkType::WikiLink { .. })
748 && (url.starts_with("at://") || url.starts_with("did:"))
749 {
750- return self.write_embed(
751- range,
752- EmbedType::Other, // AT embeds - disambiguated via NSID later
753 dest_url,
754 title,
755 id,
756 attrs,
757- );
0758 }
759760 // Image rendering: all syntax elements share one syn_id for visibility toggling
···906907 Ok(())
908 }
909- Tag::Embed {
910- embed_type,
911- dest_url,
912- title,
913- id,
914- attrs,
915- } => self.write_embed(range, embed_type, dest_url, title, id, attrs),
916 Tag::WeaverBlock(_, attrs) => {
917 self.in_non_writing_block = true;
918 self.weaver_block.buffer.clear();
···1314use super::{EditorWriter, TableState};
1516+impl<'a, T, I, E, R, W> EditorWriter<'a, T, I, E, R, W>
17where
18+ T: crate::TextBuffer,
19 I: Iterator<Item = (Event<'a>, Range<usize>)>,
20 E: EmbedContentProvider,
21 R: ImageResolver,
···748 if matches!(link_type, LinkType::WikiLink { .. })
749 && (url.starts_with("at://") || url.starts_with("did:"))
750 {
751+ // Construct an Embed tag from the Image fields
752+ let embed_tag = Tag::Embed {
753+ embed_type: EmbedType::Other,
754 dest_url,
755 title,
756 id,
757 attrs,
758+ };
759+ return self.write_embed(range, embed_tag);
760 }
761762 // Image rendering: all syntax elements share one syn_id for visibility toggling
···908909 Ok(())
910 }
911+ tag @ Tag::Embed { .. } => self.write_embed(range, tag),
000000912 Tag::WeaverBlock(_, attrs) => {
913 self.in_non_writing_block = true;
914 self.weaver_block.buffer.clear();