tangled
alpha
login
or
join now
nonbinary.computer
/
weaver
atproto blogging
24
fork
atom
overview
issues
2
pulls
pipelines
editor js bindings scaffold
Orual
1 month ago
cfdc9a2b
4c9f8b6f
+1164
9 changed files
expand all
collapse all
unified
split
Cargo.lock
crates
weaver-editor-js
.gitignore
Cargo.toml
build.sh
src
actions.rs
collab.rs
editor.rs
lib.rs
types.rs
+20
Cargo.lock
···
12319
]
12320
12321
[[package]]
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
12322
name = "weaver-embed-worker"
12323
version = "0.1.0"
12324
dependencies = [
···
12319
]
12320
12321
[[package]]
12322
+
name = "weaver-editor-js"
12323
+
version = "0.1.0"
12324
+
dependencies = [
12325
+
"console_error_panic_hook",
12326
+
"js-sys",
12327
+
"serde",
12328
+
"serde-wasm-bindgen 0.6.5",
12329
+
"serde_bytes",
12330
+
"tsify-next",
12331
+
"wasm-bindgen",
12332
+
"weaver-api",
12333
+
"weaver-common",
12334
+
"weaver-editor-browser",
12335
+
"weaver-editor-core",
12336
+
"weaver-editor-crdt",
12337
+
"weaver-embed-worker",
12338
+
"web-sys",
12339
+
]
12340
+
12341
+
[[package]]
12342
name = "weaver-embed-worker"
12343
version = "0.1.0"
12344
dependencies = [
+2
crates/weaver-editor-js/.gitignore
···
0
0
···
1
+
pkg/
2
+
target/
+57
crates/weaver-editor-js/Cargo.toml
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
[package]
2
+
name = "weaver-editor-js"
3
+
version.workspace = true
4
+
edition.workspace = true
5
+
license.workspace = true
6
+
authors.workspace = true
7
+
repository = "https://tangled.org/nonbinary.computer/weaver"
8
+
description = "WASM bindings for the weaver markdown editor"
9
+
10
+
[lib]
11
+
crate-type = ["cdylib"]
12
+
13
+
[features]
14
+
default = []
15
+
collab = ["weaver-editor-crdt"]
16
+
17
+
[dependencies]
18
+
weaver-editor-core = { path = "../weaver-editor-core" }
19
+
weaver-editor-browser = { path = "../weaver-editor-browser" }
20
+
weaver-editor-crdt = { path = "../weaver-editor-crdt", optional = true }
21
+
weaver-embed-worker = { path = "../weaver-embed-worker" }
22
+
weaver-api = { path = "../weaver-api" }
23
+
weaver-common = { path = "../weaver-common" }
24
+
25
+
wasm-bindgen = "0.2"
26
+
serde = { workspace = true }
27
+
serde_bytes = "0.11"
28
+
serde-wasm-bindgen = "0.6"
29
+
tsify-next = "0.5"
30
+
js-sys = "0.3"
31
+
console_error_panic_hook = "0.1"
32
+
33
+
[dependencies.web-sys]
34
+
version = "0.3"
35
+
features = [
36
+
"console",
37
+
"Document",
38
+
"Element",
39
+
"HtmlElement",
40
+
"HtmlDivElement",
41
+
"Node",
42
+
"Window",
43
+
"Selection",
44
+
"Range",
45
+
"InputEvent",
46
+
"KeyboardEvent",
47
+
"ClipboardEvent",
48
+
"CompositionEvent",
49
+
"DataTransfer",
50
+
"EventTarget",
51
+
]
52
+
53
+
[package.metadata.wasm-pack.profile.dev]
54
+
wasm-opt = false
55
+
56
+
[package.metadata.wasm-pack.profile.release]
57
+
wasm-opt = ['-Oz', '--enable-bulk-memory-opt', '--enable-nontrapping-float-to-int']
+290
crates/weaver-editor-js/build.sh
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
#!/usr/bin/env bash
2
+
set -euo pipefail
3
+
4
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
+
cd "$SCRIPT_DIR"
6
+
7
+
PKG_NAME="@weaver.sh/editor"
8
+
PKG_VERSION="0.1.0"
9
+
10
+
# Targets to build
11
+
TARGETS=(bundler web nodejs deno)
12
+
13
+
COMMAND="${1:-build}"
14
+
shift || true
15
+
16
+
# Feature variants
17
+
declare -A VARIANTS=(
18
+
["core"]=""
19
+
["collab"]="collab"
20
+
)
21
+
22
+
build() {
23
+
local target="$1"
24
+
local variant="$2"
25
+
local features="$3"
26
+
local out_dir="pkg/${variant}/${target}"
27
+
28
+
echo "Building ${variant}/${target}..."
29
+
30
+
local feature_args="--no-default-features"
31
+
if [[ -n "$features" ]]; then
32
+
feature_args="$feature_args --features $features"
33
+
fi
34
+
35
+
wasm-pack build \
36
+
--out-name weaver_editor \
37
+
--out-dir "$out_dir" \
38
+
--target "$target" \
39
+
$feature_args
40
+
41
+
# Report size
42
+
local wasm_file="${out_dir}/weaver_editor_bg.wasm"
43
+
if [[ -f "$wasm_file" ]]; then
44
+
local size=$(ls -lh "$wasm_file" | awk '{print $5}')
45
+
echo " → ${size}"
46
+
fi
47
+
}
48
+
49
+
generate_package_json() {
50
+
local variant="$1"
51
+
local out_dir="pkg/${variant}"
52
+
local pkg_suffix=""
53
+
local description=""
54
+
55
+
if [[ "$variant" == "collab" ]]; then
56
+
pkg_suffix="-collab"
57
+
description="Weaver markdown editor with collaborative editing (Loro CRDT + iroh P2P)"
58
+
else
59
+
pkg_suffix="-core"
60
+
description="Weaver markdown editor (local editing, lightweight)"
61
+
fi
62
+
63
+
cat > "${out_dir}/package.json" << EOF
64
+
{
65
+
"name": "${PKG_NAME}${pkg_suffix}",
66
+
"version": "${PKG_VERSION}",
67
+
"description": "${description}",
68
+
"license": "MPL-2.0",
69
+
"repository": {
70
+
"type": "git",
71
+
"url": "https://tangled.org/nonbinary.computer/weaver"
72
+
},
73
+
"keywords": ["atproto", "markdown", "editor", "wasm", "weaver"],
74
+
"main": "nodejs/weaver_editor.js",
75
+
"module": "bundler/weaver_editor.js",
76
+
"browser": "web/weaver_editor.js",
77
+
"types": "bundler/weaver_editor.d.ts",
78
+
"exports": {
79
+
".": {
80
+
"deno": "./deno/weaver_editor.js",
81
+
"node": {
82
+
"import": "./nodejs/weaver_editor.js",
83
+
"require": "./nodejs/weaver_editor.js"
84
+
},
85
+
"browser": {
86
+
"import": "./web/weaver_editor.js"
87
+
},
88
+
"default": "./bundler/weaver_editor.js"
89
+
},
90
+
"./bundler": {
91
+
"import": "./bundler/weaver_editor.js",
92
+
"types": "./bundler/weaver_editor.d.ts"
93
+
},
94
+
"./web": {
95
+
"import": "./web/weaver_editor.js",
96
+
"types": "./web/weaver_editor.d.ts"
97
+
},
98
+
"./nodejs": {
99
+
"import": "./nodejs/weaver_editor.js",
100
+
"require": "./nodejs/weaver_editor.js",
101
+
"types": "./nodejs/weaver_editor.d.ts"
102
+
},
103
+
"./deno": {
104
+
"import": "./deno/weaver_editor.js",
105
+
"types": "./deno/weaver_editor.d.ts"
106
+
}
107
+
},
108
+
"files": [
109
+
"bundler/",
110
+
"web/",
111
+
"nodejs/",
112
+
"deno/",
113
+
"README.md"
114
+
]
115
+
}
116
+
EOF
117
+
}
118
+
119
+
generate_readme() {
120
+
local variant="$1"
121
+
local out_dir="pkg/${variant}"
122
+
123
+
cat > "${out_dir}/README.md" << 'EOF'
124
+
# @weaver.sh/editor
125
+
126
+
WASM-based markdown editor for the weaver.sh ecosystem.
127
+
128
+
## Installation
129
+
130
+
```bash
131
+
npm install @weaver.sh/editor-core # Local editing only
132
+
npm install @weaver.sh/editor-collab # With collaborative editing
133
+
```
134
+
135
+
## Usage
136
+
137
+
### With a bundler (webpack, vite, etc.)
138
+
139
+
```javascript
140
+
import init, { JsEditor } from '@weaver.sh/editor-core';
141
+
142
+
await init();
143
+
144
+
const editor = JsEditor.fromMarkdown('# Hello\n\nWorld');
145
+
console.log(editor.getMarkdown());
146
+
```
147
+
148
+
### Direct browser usage (no bundler)
149
+
150
+
```html
151
+
<script type="module">
152
+
import init, { JsEditor } from '@weaver.sh/editor-core/web';
153
+
await init();
154
+
// ...
155
+
</script>
156
+
```
157
+
158
+
### Node.js
159
+
160
+
```javascript
161
+
const { JsEditor } = require('@weaver.sh/editor-core/nodejs');
162
+
```
163
+
164
+
## API
165
+
166
+
See the TypeScript definitions for full API documentation.
167
+
168
+
### Core
169
+
170
+
- `JsEditor.new()` - Create empty editor
171
+
- `JsEditor.fromMarkdown(content)` - Create from markdown
172
+
- `JsEditor.fromSnapshot(entry)` - Create from EntryJson snapshot
173
+
- `editor.getMarkdown()` - Get markdown content
174
+
- `editor.getSnapshot()` - Get EntryJson for drafts
175
+
- `editor.toEntry()` - Get validated EntryJson for publishing
176
+
- `editor.executeAction(action)` - Execute an EditorAction
177
+
- `editor.setTitle(title)` / `editor.setPath(path)` / `editor.setTags(tags)`
178
+
179
+
### Images
180
+
181
+
- `editor.addPendingImage(image)` - Track pending upload
182
+
- `editor.finalizeImage(localId, finalized)` - Mark upload complete
183
+
- `editor.getPendingImages()` - Get images awaiting upload
184
+
- `editor.getStagingUris()` - Get staging record URIs for cleanup
185
+
186
+
### Collab (editor-collab only)
187
+
188
+
- `JsCollabEditor` - Collaborative editor with Loro CRDT
189
+
- `editor.exportUpdates()` / `editor.importUpdates(bytes)`
190
+
- `editor.addPeer(nodeId)` / `editor.removePeer(nodeId)`
191
+
EOF
192
+
}
193
+
194
+
do_build() {
195
+
# Clean previous builds
196
+
rm -rf pkg
197
+
198
+
# Build all combinations
199
+
for variant in "${!VARIANTS[@]}"; do
200
+
features="${VARIANTS[$variant]}"
201
+
202
+
for target in "${TARGETS[@]}"; do
203
+
build "$target" "$variant" "$features"
204
+
done
205
+
206
+
generate_package_json "$variant"
207
+
generate_readme "$variant"
208
+
209
+
# Clean up wasm-pack artifacts we don't need
210
+
find "pkg/${variant}" -name ".gitignore" -delete
211
+
find "pkg/${variant}" -name "package.json" -path "*/bundler/*" -delete
212
+
find "pkg/${variant}" -name "package.json" -path "*/web/*" -delete
213
+
find "pkg/${variant}" -name "package.json" -path "*/nodejs/*" -delete
214
+
find "pkg/${variant}" -name "package.json" -path "*/deno/*" -delete
215
+
done
216
+
217
+
echo ""
218
+
echo "Build complete!"
219
+
echo ""
220
+
ls -lh pkg/core/web/*.wasm pkg/collab/web/*.wasm 2>/dev/null || true
221
+
echo ""
222
+
echo "Packages:"
223
+
echo " pkg/core/ - @weaver.sh/editor-core (local editing)"
224
+
echo " pkg/collab/ - @weaver.sh/editor-collab (with CRDT collab)"
225
+
}
226
+
227
+
do_pack() {
228
+
echo "Packing..."
229
+
for variant in "${!VARIANTS[@]}"; do
230
+
echo " ${variant}..."
231
+
(cd "pkg/${variant}" && npm pack)
232
+
done
233
+
echo ""
234
+
echo "Tarballs created:"
235
+
ls -lh pkg/*/*.tgz 2>/dev/null || true
236
+
}
237
+
238
+
do_publish() {
239
+
local tag="${1:-}"
240
+
local tag_arg=""
241
+
if [[ -n "$tag" ]]; then
242
+
tag_arg="--tag $tag"
243
+
fi
244
+
245
+
echo "Publishing..."
246
+
for variant in "${!VARIANTS[@]}"; do
247
+
echo " ${variant}..."
248
+
(cd "pkg/${variant}" && npm publish --access public $tag_arg)
249
+
done
250
+
echo ""
251
+
echo "Published!"
252
+
}
253
+
254
+
usage() {
255
+
echo "Usage: $0 [command]"
256
+
echo ""
257
+
echo "Commands:"
258
+
echo " build Build all variants and targets (default)"
259
+
echo " pack Create npm tarballs"
260
+
echo " publish Publish to npm registry"
261
+
echo " all Build, pack, and publish"
262
+
echo ""
263
+
echo "Options for publish:"
264
+
echo " --tag <tag> Publish with a specific tag (e.g., 'next', 'beta')"
265
+
}
266
+
267
+
case "$COMMAND" in
268
+
build)
269
+
do_build
270
+
;;
271
+
pack)
272
+
do_pack
273
+
;;
274
+
publish)
275
+
do_publish "$@"
276
+
;;
277
+
all)
278
+
do_build
279
+
do_pack
280
+
do_publish "$@"
281
+
;;
282
+
-h|--help|help)
283
+
usage
284
+
;;
285
+
*)
286
+
echo "Unknown command: $COMMAND"
287
+
usage
288
+
exit 1
289
+
;;
290
+
esac
+153
crates/weaver-editor-js/src/actions.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! EditorAction conversion for JavaScript.
2
+
3
+
use serde::{Deserialize, Serialize};
4
+
use tsify_next::Tsify;
5
+
use wasm_bindgen::prelude::*;
6
+
use weaver_editor_core::{EditorAction, FormatAction, Range};
7
+
8
+
/// JavaScript-friendly editor action.
9
+
///
10
+
/// Mirrors EditorAction from core but with JS-compatible types.
11
+
/// Also includes FormatAction variants for extended formatting.
12
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
13
+
#[tsify(into_wasm_abi, from_wasm_abi)]
14
+
#[serde(tag = "type", rename_all = "camelCase")]
15
+
pub enum JsEditorAction {
16
+
// Text insertion
17
+
Insert { text: String, start: usize, end: usize },
18
+
InsertLineBreak { start: usize, end: usize },
19
+
InsertParagraph { start: usize, end: usize },
20
+
21
+
// Deletion
22
+
DeleteBackward { start: usize, end: usize },
23
+
DeleteForward { start: usize, end: usize },
24
+
DeleteWordBackward { start: usize, end: usize },
25
+
DeleteWordForward { start: usize, end: usize },
26
+
DeleteToLineStart { start: usize, end: usize },
27
+
DeleteToLineEnd { start: usize, end: usize },
28
+
DeleteSoftLineBackward { start: usize, end: usize },
29
+
DeleteSoftLineForward { start: usize, end: usize },
30
+
31
+
// History
32
+
Undo,
33
+
Redo,
34
+
35
+
// Inline formatting (EditorAction variants)
36
+
ToggleBold,
37
+
ToggleItalic,
38
+
ToggleCode,
39
+
ToggleStrikethrough,
40
+
InsertLink,
41
+
42
+
// Extended formatting (FormatAction variants)
43
+
InsertImage,
44
+
InsertHeading { level: u8 },
45
+
ToggleBulletList,
46
+
ToggleNumberedList,
47
+
ToggleQuote,
48
+
49
+
// Clipboard
50
+
Cut,
51
+
Copy,
52
+
Paste { start: usize, end: usize },
53
+
CopyAsHtml,
54
+
55
+
// Selection
56
+
SelectAll,
57
+
58
+
// Cursor
59
+
MoveCursor { offset: usize },
60
+
ExtendSelection { offset: usize },
61
+
}
62
+
63
+
/// Result of converting JsEditorAction.
64
+
pub enum ActionKind {
65
+
/// Standard EditorAction.
66
+
Editor(EditorAction),
67
+
/// FormatAction (needs apply_formatting).
68
+
Format(FormatAction),
69
+
}
70
+
71
+
impl JsEditorAction {
72
+
/// Convert to ActionKind (either EditorAction or FormatAction).
73
+
pub fn to_action_kind(&self) -> ActionKind {
74
+
match self {
75
+
// Text insertion
76
+
Self::Insert { text, start, end } => ActionKind::Editor(EditorAction::Insert {
77
+
text: text.clone(),
78
+
range: Range::new(*start, *end),
79
+
}),
80
+
Self::InsertLineBreak { start, end } => ActionKind::Editor(EditorAction::InsertLineBreak {
81
+
range: Range::new(*start, *end),
82
+
}),
83
+
Self::InsertParagraph { start, end } => ActionKind::Editor(EditorAction::InsertParagraph {
84
+
range: Range::new(*start, *end),
85
+
}),
86
+
87
+
// Deletion
88
+
Self::DeleteBackward { start, end } => ActionKind::Editor(EditorAction::DeleteBackward {
89
+
range: Range::new(*start, *end),
90
+
}),
91
+
Self::DeleteForward { start, end } => ActionKind::Editor(EditorAction::DeleteForward {
92
+
range: Range::new(*start, *end),
93
+
}),
94
+
Self::DeleteWordBackward { start, end } => ActionKind::Editor(EditorAction::DeleteWordBackward {
95
+
range: Range::new(*start, *end),
96
+
}),
97
+
Self::DeleteWordForward { start, end } => ActionKind::Editor(EditorAction::DeleteWordForward {
98
+
range: Range::new(*start, *end),
99
+
}),
100
+
Self::DeleteToLineStart { start, end } => ActionKind::Editor(EditorAction::DeleteToLineStart {
101
+
range: Range::new(*start, *end),
102
+
}),
103
+
Self::DeleteToLineEnd { start, end } => ActionKind::Editor(EditorAction::DeleteToLineEnd {
104
+
range: Range::new(*start, *end),
105
+
}),
106
+
Self::DeleteSoftLineBackward { start, end } => ActionKind::Editor(EditorAction::DeleteSoftLineBackward {
107
+
range: Range::new(*start, *end),
108
+
}),
109
+
Self::DeleteSoftLineForward { start, end } => ActionKind::Editor(EditorAction::DeleteSoftLineForward {
110
+
range: Range::new(*start, *end),
111
+
}),
112
+
113
+
// History
114
+
Self::Undo => ActionKind::Editor(EditorAction::Undo),
115
+
Self::Redo => ActionKind::Editor(EditorAction::Redo),
116
+
117
+
// Inline formatting (EditorAction)
118
+
Self::ToggleBold => ActionKind::Editor(EditorAction::ToggleBold),
119
+
Self::ToggleItalic => ActionKind::Editor(EditorAction::ToggleItalic),
120
+
Self::ToggleCode => ActionKind::Editor(EditorAction::ToggleCode),
121
+
Self::ToggleStrikethrough => ActionKind::Editor(EditorAction::ToggleStrikethrough),
122
+
Self::InsertLink => ActionKind::Editor(EditorAction::InsertLink),
123
+
124
+
// Extended formatting (FormatAction)
125
+
Self::InsertImage => ActionKind::Format(FormatAction::Image),
126
+
Self::InsertHeading { level } => ActionKind::Format(FormatAction::Heading(*level)),
127
+
Self::ToggleBulletList => ActionKind::Format(FormatAction::BulletList),
128
+
Self::ToggleNumberedList => ActionKind::Format(FormatAction::NumberedList),
129
+
Self::ToggleQuote => ActionKind::Format(FormatAction::Quote),
130
+
131
+
// Clipboard
132
+
Self::Cut => ActionKind::Editor(EditorAction::Cut),
133
+
Self::Copy => ActionKind::Editor(EditorAction::Copy),
134
+
Self::Paste { start, end } => ActionKind::Editor(EditorAction::Paste {
135
+
range: Range::new(*start, *end),
136
+
}),
137
+
Self::CopyAsHtml => ActionKind::Editor(EditorAction::CopyAsHtml),
138
+
139
+
// Selection
140
+
Self::SelectAll => ActionKind::Editor(EditorAction::SelectAll),
141
+
142
+
// Cursor
143
+
Self::MoveCursor { offset } => ActionKind::Editor(EditorAction::MoveCursor { offset: *offset }),
144
+
Self::ExtendSelection { offset } => ActionKind::Editor(EditorAction::ExtendSelection { offset: *offset }),
145
+
}
146
+
}
147
+
}
148
+
149
+
/// Parse a JsValue into JsEditorAction.
150
+
pub fn parse_action(value: JsValue) -> Result<JsEditorAction, JsError> {
151
+
serde_wasm_bindgen::from_value(value)
152
+
.map_err(|e| JsError::new(&format!("Invalid action: {}", e)))
153
+
}
+43
crates/weaver-editor-js/src/collab.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! JsCollabEditor - collaborative editor with Loro CRDT.
2
+
//!
3
+
//! Only available with the `collab` feature.
4
+
5
+
use wasm_bindgen::prelude::*;
6
+
7
+
use weaver_editor_crdt::LoroTextBuffer;
8
+
9
+
/// Collaborative editor with CRDT sync.
10
+
///
11
+
/// Wraps LoroTextBuffer for collaborative editing with iroh P2P transport.
12
+
#[wasm_bindgen]
13
+
pub struct JsCollabEditor {
14
+
// TODO: Implement collab editor
15
+
// - LoroTextBuffer for CRDT-backed text
16
+
// - iroh node for P2P transport
17
+
// - Session management callbacks
18
+
_marker: std::marker::PhantomData<LoroTextBuffer>,
19
+
}
20
+
21
+
#[wasm_bindgen]
22
+
impl JsCollabEditor {
23
+
/// Create a new collaborative editor.
24
+
#[wasm_bindgen(constructor)]
25
+
pub fn new() -> Result<JsCollabEditor, JsError> {
26
+
Err(JsError::new("CollabEditor not yet implemented"))
27
+
}
28
+
}
29
+
30
+
impl Default for JsCollabEditor {
31
+
fn default() -> Self {
32
+
Self {
33
+
_marker: std::marker::PhantomData,
34
+
}
35
+
}
36
+
}
37
+
38
+
// TODO: Implement these when ready:
39
+
// - from_snapshot / from_loro_doc
40
+
// - export_updates / import_updates
41
+
// - get_version
42
+
// - add_peer / remove_peer / get_connected_peers
43
+
// - Session callbacks (onSessionNeeded, onSessionRefresh, onSessionEnd, onPeersNeeded)
+344
crates/weaver-editor-js/src/editor.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! JsEditor - the main editor wrapper for JavaScript.
2
+
3
+
use std::collections::HashMap;
4
+
5
+
use wasm_bindgen::prelude::*;
6
+
use web_sys::HtmlElement;
7
+
8
+
use weaver_editor_browser::BrowserClipboard;
9
+
use weaver_editor_core::{
10
+
EditorDocument, EditorRope, PlainEditor, RenderCache, UndoableBuffer, apply_formatting,
11
+
execute_action_with_clipboard,
12
+
};
13
+
14
+
use crate::actions::{ActionKind, parse_action};
15
+
use crate::types::{EntryEmbeds, EntryJson, FinalizedImage, JsResolvedContent, PendingImage};
16
+
17
+
type InnerEditor = PlainEditor<UndoableBuffer<EditorRope>>;
18
+
19
+
/// The main editor instance exposed to JavaScript.
20
+
///
21
+
/// Wraps the core editor with WASM bindings for browser use.
22
+
#[wasm_bindgen]
23
+
pub struct JsEditor {
24
+
doc: InnerEditor,
25
+
cache: RenderCache,
26
+
resolved_content: weaver_common::ResolvedContent,
27
+
28
+
// Metadata
29
+
title: String,
30
+
path: String,
31
+
tags: Vec<String>,
32
+
created_at: String,
33
+
34
+
// Image tracking
35
+
pending_images: HashMap<String, PendingImage>,
36
+
finalized_images: HashMap<String, FinalizedImage>,
37
+
}
38
+
39
+
#[wasm_bindgen]
40
+
impl JsEditor {
41
+
/// Create a new empty editor.
42
+
#[wasm_bindgen(constructor)]
43
+
pub fn new() -> Self {
44
+
let rope = EditorRope::new();
45
+
let buffer = UndoableBuffer::new(rope, 100);
46
+
let doc = PlainEditor::new(buffer);
47
+
48
+
Self {
49
+
doc,
50
+
cache: RenderCache::default(),
51
+
resolved_content: weaver_common::ResolvedContent::new(),
52
+
title: String::new(),
53
+
path: String::new(),
54
+
tags: Vec::new(),
55
+
created_at: now_iso(),
56
+
pending_images: HashMap::new(),
57
+
finalized_images: HashMap::new(),
58
+
}
59
+
}
60
+
61
+
/// Create an editor from markdown content.
62
+
#[wasm_bindgen(js_name = fromMarkdown)]
63
+
pub fn from_markdown(content: &str) -> Self {
64
+
let rope = EditorRope::from_str(content);
65
+
let buffer = UndoableBuffer::new(rope, 100);
66
+
let doc = PlainEditor::new(buffer);
67
+
68
+
Self {
69
+
doc,
70
+
cache: RenderCache::default(),
71
+
resolved_content: weaver_common::ResolvedContent::new(),
72
+
title: String::new(),
73
+
path: String::new(),
74
+
tags: Vec::new(),
75
+
created_at: now_iso(),
76
+
pending_images: HashMap::new(),
77
+
finalized_images: HashMap::new(),
78
+
}
79
+
}
80
+
81
+
/// Create an editor from a snapshot (EntryJson).
82
+
#[wasm_bindgen(js_name = fromSnapshot)]
83
+
pub fn from_snapshot(snapshot: JsValue) -> Result<JsEditor, JsError> {
84
+
let entry: EntryJson = serde_wasm_bindgen::from_value(snapshot)
85
+
.map_err(|e| JsError::new(&format!("Invalid snapshot: {}", e)))?;
86
+
87
+
let rope = EditorRope::from_str(&entry.content);
88
+
let buffer = UndoableBuffer::new(rope, 100);
89
+
let doc = PlainEditor::new(buffer);
90
+
91
+
Ok(Self {
92
+
doc,
93
+
cache: RenderCache::default(),
94
+
resolved_content: weaver_common::ResolvedContent::new(),
95
+
title: entry.title,
96
+
path: entry.path,
97
+
tags: entry.tags.unwrap_or_default(),
98
+
created_at: entry.created_at,
99
+
pending_images: HashMap::new(),
100
+
finalized_images: HashMap::new(),
101
+
})
102
+
}
103
+
104
+
/// Set pre-resolved embed content.
105
+
#[wasm_bindgen(js_name = setResolvedContent)]
106
+
pub fn set_resolved_content(&mut self, content: JsResolvedContent) {
107
+
self.resolved_content = content.into_inner();
108
+
}
109
+
110
+
// === Content access ===
111
+
112
+
/// Get the markdown content.
113
+
#[wasm_bindgen(js_name = getMarkdown)]
114
+
pub fn get_markdown(&self) -> String {
115
+
self.doc.content_string()
116
+
}
117
+
118
+
/// Get the current state as a snapshot (EntryJson).
119
+
#[wasm_bindgen(js_name = getSnapshot)]
120
+
pub fn get_snapshot(&self) -> Result<JsValue, JsError> {
121
+
let entry = EntryJson {
122
+
title: self.title.clone(),
123
+
path: self.path.clone(),
124
+
content: self.doc.content_string(),
125
+
created_at: self.created_at.clone(),
126
+
updated_at: Some(now_iso()),
127
+
tags: if self.tags.is_empty() {
128
+
None
129
+
} else {
130
+
Some(self.tags.clone())
131
+
},
132
+
embeds: self.build_embeds(),
133
+
authors: None,
134
+
content_warnings: None,
135
+
rating: None,
136
+
};
137
+
138
+
serde_wasm_bindgen::to_value(&entry)
139
+
.map_err(|e| JsError::new(&format!("Serialization error: {}", e)))
140
+
}
141
+
142
+
/// Get the entry JSON, validating required fields.
143
+
///
144
+
/// Throws if title or path is empty, or if there are pending images.
145
+
#[wasm_bindgen(js_name = toEntry)]
146
+
pub fn to_entry(&self) -> Result<JsValue, JsError> {
147
+
if self.title.is_empty() {
148
+
return Err(JsError::new("Title is required"));
149
+
}
150
+
if self.path.is_empty() {
151
+
return Err(JsError::new("Path is required"));
152
+
}
153
+
if !self.pending_images.is_empty() {
154
+
return Err(JsError::new(
155
+
"Pending images must be finalized before publishing",
156
+
));
157
+
}
158
+
159
+
self.get_snapshot()
160
+
}
161
+
162
+
// === Metadata ===
163
+
164
+
/// Get the title.
165
+
#[wasm_bindgen(js_name = getTitle)]
166
+
pub fn get_title(&self) -> String {
167
+
self.title.clone()
168
+
}
169
+
170
+
/// Set the title.
171
+
#[wasm_bindgen(js_name = setTitle)]
172
+
pub fn set_title(&mut self, title: &str) {
173
+
self.title = title.to_string();
174
+
}
175
+
176
+
/// Get the path.
177
+
#[wasm_bindgen(js_name = getPath)]
178
+
pub fn get_path(&self) -> String {
179
+
self.path.clone()
180
+
}
181
+
182
+
/// Set the path.
183
+
#[wasm_bindgen(js_name = setPath)]
184
+
pub fn set_path(&mut self, path: &str) {
185
+
self.path = path.to_string();
186
+
}
187
+
188
+
/// Get the tags.
189
+
#[wasm_bindgen(js_name = getTags)]
190
+
pub fn get_tags(&self) -> Vec<String> {
191
+
self.tags.clone()
192
+
}
193
+
194
+
/// Set the tags.
195
+
#[wasm_bindgen(js_name = setTags)]
196
+
pub fn set_tags(&mut self, tags: Vec<String>) {
197
+
self.tags = tags;
198
+
}
199
+
200
+
// === Actions ===
201
+
202
+
/// Execute an editor action.
203
+
#[wasm_bindgen(js_name = executeAction)]
204
+
pub fn execute_action(&mut self, action: JsValue) -> Result<(), JsError> {
205
+
let js_action = parse_action(action)?;
206
+
let kind = js_action.to_action_kind();
207
+
208
+
let clipboard = BrowserClipboard::empty();
209
+
match kind {
210
+
ActionKind::Editor(editor_action) => {
211
+
execute_action_with_clipboard(&mut self.doc, &editor_action, &clipboard);
212
+
}
213
+
ActionKind::Format(format_action) => {
214
+
apply_formatting(&mut self.doc, format_action);
215
+
}
216
+
}
217
+
218
+
Ok(())
219
+
}
220
+
221
+
// === Image handling ===
222
+
223
+
/// Add a pending image (called when user adds an image).
224
+
#[wasm_bindgen(js_name = addPendingImage)]
225
+
pub fn add_pending_image(&mut self, image: JsValue) -> Result<(), JsError> {
226
+
let pending: PendingImage = serde_wasm_bindgen::from_value(image)
227
+
.map_err(|e| JsError::new(&format!("Invalid pending image: {}", e)))?;
228
+
229
+
self.pending_images
230
+
.insert(pending.local_id.clone(), pending);
231
+
Ok(())
232
+
}
233
+
234
+
/// Finalize an image after upload.
235
+
#[wasm_bindgen(js_name = finalizeImage)]
236
+
pub fn finalize_image(&mut self, local_id: &str, finalized: JsValue) -> Result<(), JsError> {
237
+
let finalized: FinalizedImage = serde_wasm_bindgen::from_value(finalized)
238
+
.map_err(|e| JsError::new(&format!("Invalid finalized image: {}", e)))?;
239
+
240
+
self.pending_images.remove(local_id);
241
+
self.finalized_images
242
+
.insert(local_id.to_string(), finalized);
243
+
Ok(())
244
+
}
245
+
246
+
/// Remove a pending image.
247
+
#[wasm_bindgen(js_name = removeImage)]
248
+
pub fn remove_image(&mut self, local_id: &str) {
249
+
self.pending_images.remove(local_id);
250
+
self.finalized_images.remove(local_id);
251
+
}
252
+
253
+
/// Get pending images that need upload.
254
+
#[wasm_bindgen(js_name = getPendingImages)]
255
+
pub fn get_pending_images(&self) -> Result<JsValue, JsError> {
256
+
let pending: Vec<_> = self.pending_images.values().cloned().collect();
257
+
serde_wasm_bindgen::to_value(&pending)
258
+
.map_err(|e| JsError::new(&format!("Serialization error: {}", e)))
259
+
}
260
+
261
+
/// Get staging URIs for cleanup after publish.
262
+
#[wasm_bindgen(js_name = getStagingUris)]
263
+
pub fn get_staging_uris(&self) -> Vec<String> {
264
+
self.finalized_images
265
+
.values()
266
+
.map(|f| f.staging_uri.clone())
267
+
.collect()
268
+
}
269
+
270
+
// === Cursor/selection ===
271
+
272
+
/// Get the current cursor offset.
273
+
#[wasm_bindgen(js_name = getCursorOffset)]
274
+
pub fn get_cursor_offset(&self) -> usize {
275
+
self.doc.cursor_offset()
276
+
}
277
+
278
+
/// Set the cursor offset.
279
+
#[wasm_bindgen(js_name = setCursorOffset)]
280
+
pub fn set_cursor_offset(&mut self, offset: usize) {
281
+
self.doc.set_cursor_offset(offset);
282
+
}
283
+
284
+
/// Get the document length in characters.
285
+
#[wasm_bindgen(js_name = getLength)]
286
+
pub fn get_length(&self) -> usize {
287
+
self.doc.len_chars()
288
+
}
289
+
290
+
// === Undo/redo ===
291
+
292
+
/// Check if undo is available.
293
+
#[wasm_bindgen(js_name = canUndo)]
294
+
pub fn can_undo(&self) -> bool {
295
+
self.doc.can_undo()
296
+
}
297
+
298
+
/// Check if redo is available.
299
+
#[wasm_bindgen(js_name = canRedo)]
300
+
pub fn can_redo(&self) -> bool {
301
+
self.doc.can_redo()
302
+
}
303
+
}
304
+
305
+
impl Default for JsEditor {
306
+
fn default() -> Self {
307
+
Self::new()
308
+
}
309
+
}
310
+
311
+
impl JsEditor {
312
+
/// Build embeds from finalized images.
313
+
fn build_embeds(&self) -> Option<EntryEmbeds> {
314
+
if self.finalized_images.is_empty() {
315
+
return None;
316
+
}
317
+
318
+
use crate::types::{ImageEmbed, ImagesEmbed};
319
+
320
+
let images: Vec<ImageEmbed> = self
321
+
.finalized_images
322
+
.values()
323
+
.map(|f| ImageEmbed {
324
+
image: f.blob_ref.clone(),
325
+
alt: String::new(), // TODO: track alt text
326
+
aspect_ratio: None,
327
+
})
328
+
.collect();
329
+
330
+
Some(EntryEmbeds {
331
+
images: Some(ImagesEmbed { images }),
332
+
records: None,
333
+
externals: None,
334
+
videos: None,
335
+
})
336
+
}
337
+
}
338
+
339
+
/// Get current time as ISO string.
340
+
fn now_iso() -> String {
341
+
// Use js_sys::Date for browser-compatible time
342
+
let date = js_sys::Date::new_0();
343
+
date.to_iso_string().into()
344
+
}
+30
crates/weaver-editor-js/src/lib.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! WASM bindings for the weaver markdown editor.
2
+
//!
3
+
//! Provides embeddable editor components for JavaScript/TypeScript apps.
4
+
//!
5
+
//! # Features
6
+
//!
7
+
//! - `collab`: Enable collaborative editing via Loro CRDT + iroh P2P
8
+
//! - `syntax-highlighting`: Enable syntax highlighting for code blocks
9
+
10
+
mod actions;
11
+
mod editor;
12
+
mod types;
13
+
14
+
#[cfg(feature = "collab")]
15
+
mod collab;
16
+
17
+
pub use actions::*;
18
+
pub use editor::*;
19
+
pub use types::*;
20
+
21
+
#[cfg(feature = "collab")]
22
+
pub use collab::*;
23
+
24
+
use wasm_bindgen::prelude::*;
25
+
26
+
/// Initialize panic hook for better error messages in console.
27
+
#[wasm_bindgen(start)]
28
+
pub fn init() {
29
+
console_error_panic_hook::set_once();
30
+
}
+225
crates/weaver-editor-js/src/types.rs
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
//! Types exposed to JavaScript via wasm-bindgen.
2
+
3
+
use serde::{Deserialize, Serialize};
4
+
use tsify_next::Tsify;
5
+
use wasm_bindgen::prelude::*;
6
+
7
+
/// Pending image waiting for upload.
8
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
9
+
#[tsify(into_wasm_abi, from_wasm_abi)]
10
+
#[serde(rename_all = "camelCase")]
11
+
pub struct PendingImage {
12
+
pub local_id: String,
13
+
#[tsify(type = "Uint8Array")]
14
+
#[serde(with = "serde_bytes")]
15
+
pub data: Vec<u8>,
16
+
pub mime_type: String,
17
+
pub name: String,
18
+
}
19
+
20
+
/// Finalized image with blob ref and staging URI.
21
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
22
+
#[tsify(into_wasm_abi, from_wasm_abi)]
23
+
#[serde(rename_all = "camelCase")]
24
+
pub struct FinalizedImage {
25
+
pub blob_ref: JsBlobRef,
26
+
/// AT URI of the staging record (sh.weaver.publish.blob).
27
+
pub staging_uri: String,
28
+
}
29
+
30
+
/// Blob reference matching AT Protocol blob format.
31
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
32
+
#[tsify(into_wasm_abi, from_wasm_abi)]
33
+
#[serde(rename_all = "camelCase")]
34
+
pub struct JsBlobRef {
35
+
#[serde(rename = "$type")]
36
+
pub type_marker: String, // "blob"
37
+
pub r#ref: BlobLink,
38
+
pub mime_type: String,
39
+
pub size: u64,
40
+
}
41
+
42
+
/// CID link for blob.
43
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
44
+
#[tsify(into_wasm_abi, from_wasm_abi)]
45
+
pub struct BlobLink {
46
+
#[serde(rename = "$link")]
47
+
pub link: String,
48
+
}
49
+
50
+
/// Entry JSON matching sh.weaver.notebook.entry lexicon.
51
+
///
52
+
/// Used for snapshots (drafts) and final entry output.
53
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
54
+
#[tsify(into_wasm_abi, from_wasm_abi)]
55
+
#[serde(rename_all = "camelCase")]
56
+
pub struct EntryJson {
57
+
pub title: String,
58
+
pub path: String,
59
+
pub content: String,
60
+
pub created_at: String,
61
+
#[serde(skip_serializing_if = "Option::is_none")]
62
+
pub updated_at: Option<String>,
63
+
#[serde(skip_serializing_if = "Option::is_none")]
64
+
pub tags: Option<Vec<String>>,
65
+
#[serde(skip_serializing_if = "Option::is_none")]
66
+
pub embeds: Option<EntryEmbeds>,
67
+
#[serde(skip_serializing_if = "Option::is_none")]
68
+
pub authors: Option<Vec<Author>>,
69
+
#[serde(skip_serializing_if = "Option::is_none")]
70
+
pub content_warnings: Option<Vec<String>>,
71
+
#[serde(skip_serializing_if = "Option::is_none")]
72
+
pub rating: Option<String>,
73
+
}
74
+
75
+
/// Entry embeds container.
76
+
#[derive(Debug, Clone, Default, Serialize, Deserialize, Tsify)]
77
+
#[tsify(into_wasm_abi, from_wasm_abi)]
78
+
#[serde(rename_all = "camelCase")]
79
+
pub struct EntryEmbeds {
80
+
#[serde(skip_serializing_if = "Option::is_none")]
81
+
pub images: Option<ImagesEmbed>,
82
+
#[serde(skip_serializing_if = "Option::is_none")]
83
+
pub records: Option<RecordsEmbed>,
84
+
#[serde(skip_serializing_if = "Option::is_none")]
85
+
pub externals: Option<ExternalsEmbed>,
86
+
#[serde(skip_serializing_if = "Option::is_none")]
87
+
pub videos: Option<VideosEmbed>,
88
+
}
89
+
90
+
/// Image embed container.
91
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
92
+
#[tsify(into_wasm_abi, from_wasm_abi)]
93
+
pub struct ImagesEmbed {
94
+
pub images: Vec<ImageEmbed>,
95
+
}
96
+
97
+
/// Single image embed.
98
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
99
+
#[tsify(into_wasm_abi, from_wasm_abi)]
100
+
#[serde(rename_all = "camelCase")]
101
+
pub struct ImageEmbed {
102
+
pub image: JsBlobRef,
103
+
pub alt: String,
104
+
#[serde(skip_serializing_if = "Option::is_none")]
105
+
pub aspect_ratio: Option<AspectRatio>,
106
+
}
107
+
108
+
/// Aspect ratio for images/videos.
109
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
110
+
#[tsify(into_wasm_abi, from_wasm_abi)]
111
+
pub struct AspectRatio {
112
+
pub width: u32,
113
+
pub height: u32,
114
+
}
115
+
116
+
/// Record embed container.
117
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
118
+
#[tsify(into_wasm_abi, from_wasm_abi)]
119
+
pub struct RecordsEmbed {
120
+
pub records: Vec<RecordEmbed>,
121
+
}
122
+
123
+
/// Single record embed (strong ref).
124
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
125
+
#[tsify(into_wasm_abi, from_wasm_abi)]
126
+
pub struct RecordEmbed {
127
+
pub uri: String,
128
+
pub cid: String,
129
+
}
130
+
131
+
/// External link embed container.
132
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
133
+
#[tsify(into_wasm_abi, from_wasm_abi)]
134
+
pub struct ExternalsEmbed {
135
+
pub externals: Vec<ExternalEmbed>,
136
+
}
137
+
138
+
/// Single external link embed.
139
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
140
+
#[tsify(into_wasm_abi, from_wasm_abi)]
141
+
pub struct ExternalEmbed {
142
+
pub uri: String,
143
+
pub title: String,
144
+
pub description: String,
145
+
#[serde(skip_serializing_if = "Option::is_none")]
146
+
pub thumb: Option<JsBlobRef>,
147
+
}
148
+
149
+
/// Video embed container.
150
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
151
+
#[tsify(into_wasm_abi, from_wasm_abi)]
152
+
pub struct VideosEmbed {
153
+
pub videos: Vec<VideoEmbed>,
154
+
}
155
+
156
+
/// Single video embed.
157
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
158
+
#[tsify(into_wasm_abi, from_wasm_abi)]
159
+
#[serde(rename_all = "camelCase")]
160
+
pub struct VideoEmbed {
161
+
pub video: JsBlobRef,
162
+
#[serde(skip_serializing_if = "Option::is_none")]
163
+
pub alt: Option<String>,
164
+
#[serde(skip_serializing_if = "Option::is_none")]
165
+
pub aspect_ratio: Option<AspectRatio>,
166
+
}
167
+
168
+
/// Author reference.
169
+
#[derive(Debug, Clone, Serialize, Deserialize, Tsify)]
170
+
#[tsify(into_wasm_abi, from_wasm_abi)]
171
+
pub struct Author {
172
+
pub did: String,
173
+
}
174
+
175
+
/// Pre-rendered embed content for initial load.
176
+
#[wasm_bindgen]
177
+
pub struct JsResolvedContent {
178
+
inner: weaver_common::ResolvedContent,
179
+
}
180
+
181
+
#[wasm_bindgen]
182
+
impl JsResolvedContent {
183
+
#[wasm_bindgen(constructor)]
184
+
pub fn new() -> Self {
185
+
Self {
186
+
inner: weaver_common::ResolvedContent::new(),
187
+
}
188
+
}
189
+
190
+
/// Add pre-rendered HTML for an AT URI.
191
+
#[wasm_bindgen(js_name = addEmbed)]
192
+
pub fn add_embed(&mut self, at_uri: &str, html: &str) -> Result<(), JsError> {
193
+
use weaver_common::jacquard::{CowStr, IntoStatic, types::string::AtUri};
194
+
195
+
let uri = AtUri::new(at_uri)
196
+
.map_err(|e| JsError::new(&format!("Invalid AT URI: {}", e)))?
197
+
.into_static();
198
+
199
+
self.inner
200
+
.add_embed(uri, CowStr::from(html.to_string()), None);
201
+
Ok(())
202
+
}
203
+
}
204
+
205
+
impl Default for JsResolvedContent {
206
+
fn default() -> Self {
207
+
Self::new()
208
+
}
209
+
}
210
+
211
+
impl JsResolvedContent {
212
+
pub fn into_inner(self) -> weaver_common::ResolvedContent {
213
+
self.inner
214
+
}
215
+
216
+
pub fn inner_ref(&self) -> &weaver_common::ResolvedContent {
217
+
&self.inner
218
+
}
219
+
}
220
+
221
+
/// Create an empty resolved content container.
222
+
#[wasm_bindgen]
223
+
pub fn create_resolved_content() -> JsResolvedContent {
224
+
JsResolvedContent::new()
225
+
}