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