+7
.vscode/settings.json
+7
.vscode/settings.json
···
3
3
"apdisk",
4
4
"atproto",
5
5
"bafk",
6
+
"bafkreiabc",
6
7
"bafyb",
8
+
"bgbsh",
7
9
"blockquotes",
8
10
"bsky",
9
11
"colour",
10
12
"colours",
11
13
"Customise",
12
14
"donotpresent",
15
+
"emph",
13
16
"Ewan",
17
+
"ewanc",
14
18
"fseventsd",
15
19
"icns",
16
20
"myblog",
21
+
"pdsls",
17
22
"rkey",
18
23
"timemachine",
24
+
"vercel",
25
+
"Vite",
19
26
"whitewind",
20
27
"whtwnd",
21
28
"xrpc"
+283
-35
README.md
+283
-35
README.md
···
1
1
# whtwnd-to-leaflet
2
2
3
-
A simple browser-based tool for converting WhiteWind blog entries into the Leaflet publication format. It helps migrate content, metadata, and themes from WhiteWind to Leaflet on the AT Protocol.
3
+
A browser-based tool for converting WhiteWind blog entries into the Leaflet publication format. Migrate content, metadata, and themes from WhiteWind to Leaflet on the AT Protocol with automatic fetching and publishing capabilities.
4
4
5
-
⚠️ **Experimental** — output is not guaranteed to be fully accurate yet.
5
+
⚠️ **Now with Auto-Publishing!** - Log in to automatically fetch your WhiteWind entries and publish directly to Leaflet.
6
6
7
7
---
8
8
9
-
## How It Works
9
+
## ✨ Features
10
10
11
-
The app is a single-page web tool (built with **SvelteKit + TypeScript + Tailwind** on Svelte 5) that runs entirely in the browser. No backend is required. It produces a Leaflet-compatible **publication record** plus one or more **document records** from your WhiteWind entries.
11
+
### Core Conversion
12
+
- **Remark-based Markdown Parsing** - Uses the robust `unified`/`remark` ecosystem for accurate parsing
13
+
- **Proper ATProto Blob Handling** - Converts blob URLs to correct Leaflet image block format with `{ $type: 'blob', ref: { $link: 'cid' } }`
14
+
- **Full Lexicon Compliance** - Strictly follows Leaflet lexicons for all block types
15
+
- **Image Blocks** - Standalone images converted to `pub.leaflet.blocks.image` blocks with proper aspectRatio
16
+
- **List Support** - Proper `pub.leaflet.blocks.unorderedList` with nested list items
17
+
- **Rich Text Facets** - Preserves bold, italic, code, and links with proper UTF-8 byte offsets
12
18
13
-
The process involves three steps:
19
+
### New Auto-Publishing Features
20
+
- **AT Protocol Authentication** - Secure login with app passwords
21
+
- **Auto-Fetch WhiteWind Entries** - Automatically retrieves all your WhiteWind blog posts
22
+
- **Direct Publishing** - Publish converted entries directly to Leaflet without manual file uploads
23
+
- **Progress Tracking** - Real-time progress bar showing publication status
24
+
- **Manual Mode** - Still supports manual JSON paste for offline conversion
14
25
15
-
1. **Publication Setup**
16
-
Provide publication details such as name, description, base path, and AT Protocol DID. Configure preferences like enabling comments and whether to appear in the Leaflet Discover feed.
26
+
### Other Features
27
+
- **Add to Existing Publications** - Can add converted posts to an existing Leaflet publication
28
+
- **Metadata Cleanup** - Only preserves Leaflet-compatible metadata, strips WhiteWind-specific fields
29
+
- **ZIP Export** - Download all converted files as a ZIP archive
17
30
18
-
2. **Theme Configuration**
19
-
Choose colours for the publication’s primary, background, and page background.
31
+
---
20
32
21
-
3. **Entry Conversion**
22
-
Paste a JSON export of WhiteWind entries (either an array or an object with `records`/`data`). The converter will:
23
-
- **Parse Markdown** → transforms into Leaflet blocks (headers, text, blockquotes, code, images, horizontal rules).
24
-
- **Convert AT-URIs** → changes WhiteWind blob/CID URLs into `at://` URIs where possible.
25
-
- **Output JSON** → emits one publication record and document records (ZIP export supported).
33
+
## How It Works
34
+
35
+
The app is a single-page web tool (built with **SvelteKit + TypeScript + Tailwind** on Svelte 5) that runs entirely in the browser. It produces Leaflet-compatible **publication record** plus one or more **document records** from your WhiteWind entries.
36
+
37
+
### Two Modes of Operation
38
+
39
+
#### Auto Mode (Recommended) 🚀
40
+
1. **Login** - Authenticate with your AT Protocol handle and app password
41
+
2. **Fetch** - Automatically retrieve all your WhiteWind entries
42
+
3. **Configure** - Set up your publication details and theme
43
+
4. **Convert** - Transform entries to Leaflet format
44
+
5. **Publish** - Directly publish to your PDS with one click
45
+
46
+
#### Manual Mode 📝
47
+
1. **Publication Setup** - Choose to create new or add to existing publication
48
+
2. **Theme Configuration** - Customize colors (if creating new publication)
49
+
3. **Entry Conversion** - Paste WhiteWind JSON and convert
50
+
4. **Download** - Export as ZIP or copy JSON
26
51
27
52
---
28
53
29
54
## Usage
30
55
31
-
**Web app:**
56
+
### Auto Mode (With Login)
57
+
58
+
1. **Log in** with your AT Protocol credentials:
59
+
- Enter your handle (e.g., `alice.bsky.social`) or DID
60
+
- Use an **app password** (never your main password!)
61
+
- Create app passwords in your AT Protocol client settings
32
62
33
-
1. Fill out Publication Setup and Theme Configuration.
34
-
2. Paste your WhiteWind JSON entries into the text area. You can fetch them from your PDS with:
63
+
2. **Select Auto Mode** and click **Fetch My WhiteWind Entries**
64
+
- Your entries will be automatically retrieved from your PDS
65
+
66
+
3. **Configure your publication**:
67
+
- Create new or add to existing publication
68
+
- Set publication name, description, and preferences
69
+
- Choose theme colors
70
+
71
+
4. **Convert** your entries to Leaflet format
72
+
73
+
5. **Publish** directly to AT Protocol with one click
74
+
- Progress bar shows real-time publishing status
75
+
- All records created automatically
76
+
77
+
### Manual Mode (Without Login)
78
+
79
+
1. Choose whether to create a new publication or add to an existing one
80
+
2. Fill out the required fields (Publication Setup or Existing Rkey)
81
+
3. If creating new, configure the theme
82
+
4. Paste your WhiteWind JSON entries. You can fetch them from your PDS with:
35
83
36
84
```plaintext
37
-
https\://\[pds domain]/xrpc/com.atproto.repo.listRecords?repo=\[did]\&collection=com.whtwnd.blog.entry
85
+
https://[pds domain]/xrpc/com.atproto.repo.listRecords?repo=[did]&collection=com.whtwnd.blog.entry
38
86
```
39
87
40
-
3. Enter your Author DID.
41
-
4. Click **Convert to Leaflet**.
42
-
5. Copy or download the generated JSON files, or use the ZIP export.
88
+
5. Enter your Author DID and PDS URL
89
+
6. Click **Convert to Leaflet**
90
+
7. Download the ZIP export or manually upload using pdsls.dev
91
+
92
+
### Importing into a PDS (Manual Mode)
93
+
94
+
When manually importing the converted records:
95
+
96
+
**For new publications:**
97
+
- Use pdsls.dev and the ZIP export
98
+
- Upload `00.json` first (the publication record) with your chosen rkey
99
+
- Then upload each document record (`1.json`, `2.json`, etc.) individually
100
+
- Don't change the autogenerated rkeys for documents
101
+
102
+
**For existing publications:**
103
+
- The ZIP will only contain document records (`0.json`, `1.json`, etc.)
104
+
- Upload each document individually to your existing publication
105
+
- Don't change the autogenerated rkeys
106
+
107
+
---
108
+
109
+
## Technical Details
110
+
111
+
### Markdown Parsing
112
+
113
+
The converter uses the `unified`/`remark` ecosystem for robust Markdown parsing:
114
+
115
+
- **unified** - Core parser framework
116
+
- **remark-parse** - Markdown parser
117
+
- **remark-gfm** - GitHub Flavored Markdown support
118
+
- **mdast-util-to-string** - AST utilities
119
+
120
+
This provides:
121
+
- Better edge case handling
122
+
- Support for GFM features
123
+
- Proper nested element parsing
124
+
- Extensible with plugins
125
+
126
+
### Leaflet Block Support
127
+
128
+
Fully compliant with Leaflet lexicons:
129
+
130
+
- ✅ **pub.leaflet.blocks.text** - Paragraphs with rich text facets
131
+
- ✅ **pub.leaflet.blocks.header** - Headings (levels 1-6) with facets
132
+
- ✅ **pub.leaflet.blocks.image** - Images with required aspectRatio
133
+
- ✅ **pub.leaflet.blocks.code** - Code blocks with language
134
+
- ✅ **pub.leaflet.blocks.blockquote** - Block quotes
135
+
- ✅ **pub.leaflet.blocks.horizontalRule** - Horizontal rules
136
+
- ✅ **pub.leaflet.blocks.unorderedList** - Lists with nested items
137
+
138
+
### Rich Text Facets
139
+
140
+
Inline formatting preserved with proper UTF-8 byte offsets:
43
141
44
-
### Importing into a PDS
142
+
- **Bold** (`**text**`) → `pub.leaflet.richtext.facet#bold`
143
+
- **Italic** (`*text*`) → `pub.leaflet.richtext.facet#italic`
144
+
- **Code** (`` `text` ``) → `pub.leaflet.richtext.facet#code`
145
+
- **Links** (`[text](url)`) → `pub.leaflet.richtext.facet#link`
45
146
46
-
When importing the converted records into a PDS you should follow these rules:
147
+
### Metadata Handling
47
148
48
-
- Use `pdsls.dev` and the ZIP export.
49
-
- You’ll need to copy each document record individually; don’t change the autogenerated record key (`rkey`).
50
-
- Only change the `rkey` for the one generated by the converter for the publication record itself.
149
+
The converter **only preserves Leaflet-compatible metadata**:
51
150
52
-
**Local development:**
151
+
**Preserved:**
152
+
- `title` - Post title
153
+
- `description` - From WhiteWind's subtitle field
154
+
- `publishedAt` - Original creation timestamp
155
+
- `theme` - Visual theme
156
+
157
+
**Discarded (WhiteWind-specific):**
158
+
- `rkey` - WhiteWind record key (replaced with new TID)
159
+
- `cid` - Content identifier
160
+
- `value` - Wrapper object
161
+
- `uri` - Original WhiteWind URI
162
+
- `visibility` - Not in Leaflet document schema
163
+
- Any other fields not in Leaflet's lexicon
164
+
165
+
### Blob URL Conversion
166
+
167
+
Handles two types of blob references:
168
+
169
+
1. **XRPC getBlob URLs**: `xrpc/com.atproto.sync.getBlob?did=X&cid=Y`
170
+
2. **Direct CID references**: URLs containing `bafk...` or `bafyb...`
171
+
172
+
For standalone images, creates proper image blocks:
173
+
174
+
```json
175
+
{
176
+
"block": {
177
+
"$type": "pub.leaflet.blocks.image",
178
+
"image": {
179
+
"$type": "blob",
180
+
"ref": {
181
+
"$link": "bafkreiabc123..."
182
+
}
183
+
},
184
+
"aspectRatio": {
185
+
"width": 1920,
186
+
"height": 1080
187
+
},
188
+
"alt": "Image description"
189
+
}
190
+
}
191
+
```
192
+
193
+
For inline images and blob links, converts to AT-URI format: `at://did/com.atproto.blob/cid`
194
+
195
+
### Image Dimensions
196
+
197
+
The converter attempts to fetch actual image dimensions for proper aspectRatio:
198
+
199
+
1. Checks blob metadata if available
200
+
2. Fetches and measures actual image if PDS URL provided
201
+
3. Falls back to 512x512 if dimensions unavailable
202
+
203
+
This ensures compliance with Leaflet's required `aspectRatio` field.
204
+
205
+
### AT Protocol Authentication
206
+
207
+
Uses `@atproto/api` for secure authentication:
208
+
209
+
- Resolves handles/DIDs using Slingshot identity resolver
210
+
- Supports app passwords (2FA-compatible)
211
+
- Session management with localStorage
212
+
- Automatic PDS URL resolution
213
+
214
+
---
215
+
216
+
## Local Development
217
+
218
+
**Install dependencies:**
53
219
54
220
```bash
55
221
npm install
222
+
```
223
+
224
+
**Run development server:**
225
+
226
+
```bash
56
227
npm run dev
57
228
```
58
229
···
69
240
70
241
## Files of Interest
71
242
72
-
* `src/routes/+page.svelte` — main UI and form handling
73
-
* `src/lib/convert.ts` — core conversion logic (TID generation, markdown → blocks, URL conversions)
74
-
* `src/lib/styles.css`, `src/lib/variables.css` — styles and theme variables
75
-
* `src/types/file-saver.d.ts` — small type declaration
243
+
- `src/routes/+page.svelte` - Main UI with auth and publishing
244
+
- `src/lib/convert.ts` - Core conversion logic with remark integration
245
+
- `src/lib/auth.ts` - AT Protocol authentication and publishing
246
+
- `src/lib/styles.css`, `src/lib/variables.css` - Styles and theme variables
247
+
- `package.json` - Dependencies including unified/remark and @atproto/api
248
+
249
+
---
250
+
251
+
## Migration Tips
252
+
253
+
For migrating ~86 posts from WhiteWind:
254
+
255
+
### Using Auto Mode (Recommended)
256
+
1. **Log in** with your AT Protocol credentials
257
+
2. **Fetch all entries** automatically
258
+
3. **Configure publication** settings
259
+
4. **Convert and publish** in one go
260
+
5. **Monitor progress** with the real-time progress bar
261
+
262
+
### Using Manual Mode
263
+
1. **Start with existing publication mode** - Use your existing Leaflet publication rkey
264
+
2. **Process in batches** - Convert and upload 10-15 posts at a time
265
+
3. **Review after conversion** - Check the first few converted posts for formatting issues
266
+
4. **Iterate if needed** - The remark parser handles most cases, but custom formatting may need adjustment
267
+
5. **Preserve timestamps** - Original publish dates are maintained automatically
268
+
269
+
### Common Issues
270
+
271
+
**Image dimensions missing:**
272
+
- Make sure to provide your PDS URL
273
+
- The converter will attempt to fetch actual dimensions
274
+
- Falls back to 512x512 if unavailable
275
+
276
+
**List formatting:**
277
+
- WhiteWind lists are converted to proper Leaflet unorderedList blocks
278
+
- Nested lists are supported with proper structure
279
+
280
+
**Rich text facets:**
281
+
- All inline formatting (bold, italic, code, links) is preserved
282
+
- Byte offsets calculated automatically for UTF-8 compliance
76
283
77
284
---
78
285
79
-
## Development Notes
286
+
## Security Notes
80
287
81
-
* Supports JSON arrays of entries or objects with `records`/`data` arrays.
82
-
* Inline rich-text facets (links, bold, italic, code) are extracted when possible and attached to text blocks.
288
+
- **Never use your main password** - Always create an app password
289
+
- **App passwords** support 2FA and can be revoked individually
290
+
- **Sessions stored locally** - Clear browser data to logout completely
291
+
- **No server-side storage** - All processing happens in your browser
83
292
84
293
---
85
294
···
89
298
90
299
---
91
300
92
-
**Project:** [ewanc26/whtwnd-to-leaflet](https://github.com/ewanc26/whtwnd-to-leaflet)
301
+
## Credits
302
+
303
+
**Project:** [ewanc26/whtwnd-to-leaflet](https://github.com/ewanc26/whtwnd-to-leaflet)
304
+
305
+
Built with 🍃 by [Ewan](https://ewancroft.uk)
306
+
307
+
Not affiliated with [WhiteWind](https://whtwnd.com) or [Leaflet](https://leaflet.pub).
308
+
309
+
---
310
+
311
+
## Changelog
312
+
313
+
### v2.0.0 - Auto-Publishing Update
314
+
315
+
**New Features:**
316
+
- AT Protocol authentication with app passwords
317
+
- Auto-fetch WhiteWind entries from your PDS
318
+
- Direct publishing to Leaflet with progress tracking
319
+
- Two modes: Auto (with login) and Manual (without login)
320
+
321
+
**Improvements:**
322
+
- Full Leaflet lexicon compliance
323
+
- Proper `pub.leaflet.blocks.unorderedList` support
324
+
- Improved image block handling with aspectRatio
325
+
- Better error messages and user feedback
326
+
- Real-time publishing progress
327
+
328
+
**Bug Fixes:**
329
+
- Removed invalid `visibility` field from documents
330
+
- Fixed list conversion to use proper Leaflet structure
331
+
- Improved blob URL detection and conversion
332
+
- Better handling of missing image dimensions
333
+
334
+
### v1.0.0 - Initial Release
335
+
336
+
- Remark-based Markdown parsing
337
+
- Proper ATProto blob handling
338
+
- Image and text block conversion
339
+
- ZIP export functionality
340
+
- Add to existing publications
+1188
-3
package-lock.json
+1188
-3
package-lock.json
···
8
8
"name": "sv",
9
9
"version": "0.0.1",
10
10
"dependencies": {
11
+
"@atproto/api": "^0.13.16",
11
12
"file-saver": "^2.0.5",
12
-
"jszip": "^3.10.1"
13
+
"jszip": "^3.10.1",
14
+
"mdast-util-to-string": "^4.0.0",
15
+
"remark-gfm": "^4.0.0",
16
+
"remark-parse": "^11.0.0",
17
+
"unified": "^11.0.4"
13
18
},
14
19
"devDependencies": {
15
20
"@sveltejs/adapter-auto": "^6.0.0",
···
24
29
"tailwindcss": "^4.0.0",
25
30
"typescript": "^5.0.0",
26
31
"vite": "^7.0.4"
32
+
}
33
+
},
34
+
"node_modules/@atproto/api": {
35
+
"version": "0.13.35",
36
+
"resolved": "https://registry.npmjs.org/@atproto/api/-/api-0.13.35.tgz",
37
+
"integrity": "sha512-vsEfBj0C333TLjDppvTdTE0IdKlXuljKSveAeI4PPx/l6eUKNnDTsYxvILtXUVzwUlTDmSRqy5O4Ryh78n1b7g==",
38
+
"license": "MIT",
39
+
"dependencies": {
40
+
"@atproto/common-web": "^0.4.0",
41
+
"@atproto/lexicon": "^0.4.6",
42
+
"@atproto/syntax": "^0.3.2",
43
+
"@atproto/xrpc": "^0.6.8",
44
+
"await-lock": "^2.2.2",
45
+
"multiformats": "^9.9.0",
46
+
"tlds": "^1.234.0",
47
+
"zod": "^3.23.8"
48
+
}
49
+
},
50
+
"node_modules/@atproto/common-web": {
51
+
"version": "0.4.3",
52
+
"resolved": "https://registry.npmjs.org/@atproto/common-web/-/common-web-0.4.3.tgz",
53
+
"integrity": "sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg==",
54
+
"license": "MIT",
55
+
"dependencies": {
56
+
"graphemer": "^1.4.0",
57
+
"multiformats": "^9.9.0",
58
+
"uint8arrays": "3.0.0",
59
+
"zod": "^3.23.8"
60
+
}
61
+
},
62
+
"node_modules/@atproto/lexicon": {
63
+
"version": "0.4.14",
64
+
"resolved": "https://registry.npmjs.org/@atproto/lexicon/-/lexicon-0.4.14.tgz",
65
+
"integrity": "sha512-jiKpmH1QER3Gvc7JVY5brwrfo+etFoe57tKPQX/SmPwjvUsFnJAow5xLIryuBaJgFAhnTZViXKs41t//pahGHQ==",
66
+
"license": "MIT",
67
+
"dependencies": {
68
+
"@atproto/common-web": "^0.4.2",
69
+
"@atproto/syntax": "^0.4.0",
70
+
"iso-datestring-validator": "^2.2.2",
71
+
"multiformats": "^9.9.0",
72
+
"zod": "^3.23.8"
73
+
}
74
+
},
75
+
"node_modules/@atproto/lexicon/node_modules/@atproto/syntax": {
76
+
"version": "0.4.1",
77
+
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.4.1.tgz",
78
+
"integrity": "sha512-CJdImtLAiFO+0z3BWTtxwk6aY5w4t8orHTMVJgkf++QRJWTxPbIFko/0hrkADB7n2EruDxDSeAgfUGehpH6ngw==",
79
+
"license": "MIT"
80
+
},
81
+
"node_modules/@atproto/syntax": {
82
+
"version": "0.3.4",
83
+
"resolved": "https://registry.npmjs.org/@atproto/syntax/-/syntax-0.3.4.tgz",
84
+
"integrity": "sha512-8CNmi5DipOLaVeSMPggMe7FCksVag0aO6XZy9WflbduTKM4dFZVCs4686UeMLfGRXX+X966XgwECHoLYrovMMg==",
85
+
"license": "MIT"
86
+
},
87
+
"node_modules/@atproto/xrpc": {
88
+
"version": "0.6.12",
89
+
"resolved": "https://registry.npmjs.org/@atproto/xrpc/-/xrpc-0.6.12.tgz",
90
+
"integrity": "sha512-Ut3iISNLujlmY9Gu8sNU+SPDJDvqlVzWddU8qUr0Yae5oD4SguaUFjjhireMGhQ3M5E0KljQgDbTmnBo1kIZ3w==",
91
+
"license": "MIT",
92
+
"dependencies": {
93
+
"@atproto/lexicon": "^0.4.10",
94
+
"zod": "^3.23.8"
27
95
}
28
96
},
29
97
"node_modules/@esbuild/aix-ppc64": {
···
1221
1289
"dev": true,
1222
1290
"license": "MIT"
1223
1291
},
1292
+
"node_modules/@types/debug": {
1293
+
"version": "4.1.12",
1294
+
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
1295
+
"integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==",
1296
+
"license": "MIT",
1297
+
"dependencies": {
1298
+
"@types/ms": "*"
1299
+
}
1300
+
},
1224
1301
"node_modules/@types/estree": {
1225
1302
"version": "1.0.8",
1226
1303
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
···
1228
1305
"dev": true,
1229
1306
"license": "MIT"
1230
1307
},
1308
+
"node_modules/@types/mdast": {
1309
+
"version": "4.0.4",
1310
+
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
1311
+
"integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==",
1312
+
"license": "MIT",
1313
+
"dependencies": {
1314
+
"@types/unist": "*"
1315
+
}
1316
+
},
1317
+
"node_modules/@types/ms": {
1318
+
"version": "2.1.0",
1319
+
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
1320
+
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
1321
+
"license": "MIT"
1322
+
},
1323
+
"node_modules/@types/unist": {
1324
+
"version": "3.0.3",
1325
+
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
1326
+
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
1327
+
"license": "MIT"
1328
+
},
1231
1329
"node_modules/acorn": {
1232
1330
"version": "8.15.0",
1233
1331
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
···
1251
1349
"node": ">= 0.4"
1252
1350
}
1253
1351
},
1352
+
"node_modules/await-lock": {
1353
+
"version": "2.2.2",
1354
+
"resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
1355
+
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==",
1356
+
"license": "MIT"
1357
+
},
1254
1358
"node_modules/axobject-query": {
1255
1359
"version": "4.1.0",
1256
1360
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
···
1259
1363
"license": "Apache-2.0",
1260
1364
"engines": {
1261
1365
"node": ">= 0.4"
1366
+
}
1367
+
},
1368
+
"node_modules/bail": {
1369
+
"version": "2.0.2",
1370
+
"resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz",
1371
+
"integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==",
1372
+
"license": "MIT",
1373
+
"funding": {
1374
+
"type": "github",
1375
+
"url": "https://github.com/sponsors/wooorm"
1376
+
}
1377
+
},
1378
+
"node_modules/ccount": {
1379
+
"version": "2.0.1",
1380
+
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
1381
+
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
1382
+
"license": "MIT",
1383
+
"funding": {
1384
+
"type": "github",
1385
+
"url": "https://github.com/sponsors/wooorm"
1386
+
}
1387
+
},
1388
+
"node_modules/character-entities": {
1389
+
"version": "2.0.2",
1390
+
"resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz",
1391
+
"integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==",
1392
+
"license": "MIT",
1393
+
"funding": {
1394
+
"type": "github",
1395
+
"url": "https://github.com/sponsors/wooorm"
1262
1396
}
1263
1397
},
1264
1398
"node_modules/chokidar": {
···
1317
1451
"version": "4.4.1",
1318
1452
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
1319
1453
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
1320
-
"dev": true,
1321
1454
"license": "MIT",
1322
1455
"dependencies": {
1323
1456
"ms": "^2.1.3"
···
1331
1464
}
1332
1465
}
1333
1466
},
1467
+
"node_modules/decode-named-character-reference": {
1468
+
"version": "1.2.0",
1469
+
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz",
1470
+
"integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==",
1471
+
"license": "MIT",
1472
+
"dependencies": {
1473
+
"character-entities": "^2.0.0"
1474
+
},
1475
+
"funding": {
1476
+
"type": "github",
1477
+
"url": "https://github.com/sponsors/wooorm"
1478
+
}
1479
+
},
1334
1480
"node_modules/deepmerge": {
1335
1481
"version": "4.3.1",
1336
1482
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz",
···
1341
1487
"node": ">=0.10.0"
1342
1488
}
1343
1489
},
1490
+
"node_modules/dequal": {
1491
+
"version": "2.0.3",
1492
+
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
1493
+
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
1494
+
"license": "MIT",
1495
+
"engines": {
1496
+
"node": ">=6"
1497
+
}
1498
+
},
1344
1499
"node_modules/detect-libc": {
1345
1500
"version": "2.0.4",
1346
1501
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
···
1358
1513
"dev": true,
1359
1514
"license": "MIT"
1360
1515
},
1516
+
"node_modules/devlop": {
1517
+
"version": "1.1.0",
1518
+
"resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz",
1519
+
"integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==",
1520
+
"license": "MIT",
1521
+
"dependencies": {
1522
+
"dequal": "^2.0.0"
1523
+
},
1524
+
"funding": {
1525
+
"type": "github",
1526
+
"url": "https://github.com/sponsors/wooorm"
1527
+
}
1528
+
},
1361
1529
"node_modules/enhanced-resolve": {
1362
1530
"version": "5.18.3",
1363
1531
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
···
1414
1582
"@esbuild/win32-x64": "0.25.9"
1415
1583
}
1416
1584
},
1585
+
"node_modules/escape-string-regexp": {
1586
+
"version": "5.0.0",
1587
+
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz",
1588
+
"integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==",
1589
+
"license": "MIT",
1590
+
"engines": {
1591
+
"node": ">=12"
1592
+
},
1593
+
"funding": {
1594
+
"url": "https://github.com/sponsors/sindresorhus"
1595
+
}
1596
+
},
1417
1597
"node_modules/esm-env": {
1418
1598
"version": "1.2.2",
1419
1599
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
···
1431
1611
"@jridgewell/sourcemap-codec": "^1.4.15"
1432
1612
}
1433
1613
},
1614
+
"node_modules/extend": {
1615
+
"version": "3.0.2",
1616
+
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
1617
+
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
1618
+
"license": "MIT"
1619
+
},
1434
1620
"node_modules/fdir": {
1435
1621
"version": "6.5.0",
1436
1622
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
···
1477
1663
"dev": true,
1478
1664
"license": "ISC"
1479
1665
},
1666
+
"node_modules/graphemer": {
1667
+
"version": "1.4.0",
1668
+
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
1669
+
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
1670
+
"license": "MIT"
1671
+
},
1480
1672
"node_modules/immediate": {
1481
1673
"version": "3.0.6",
1482
1674
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
···
1489
1681
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1490
1682
"license": "ISC"
1491
1683
},
1684
+
"node_modules/is-plain-obj": {
1685
+
"version": "4.1.0",
1686
+
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz",
1687
+
"integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==",
1688
+
"license": "MIT",
1689
+
"engines": {
1690
+
"node": ">=12"
1691
+
},
1692
+
"funding": {
1693
+
"url": "https://github.com/sponsors/sindresorhus"
1694
+
}
1695
+
},
1492
1696
"node_modules/is-reference": {
1493
1697
"version": "3.0.3",
1494
1698
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
···
1503
1707
"version": "1.0.0",
1504
1708
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
1505
1709
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
1710
+
"license": "MIT"
1711
+
},
1712
+
"node_modules/iso-datestring-validator": {
1713
+
"version": "2.2.2",
1714
+
"resolved": "https://registry.npmjs.org/iso-datestring-validator/-/iso-datestring-validator-2.2.2.tgz",
1715
+
"integrity": "sha512-yLEMkBbLZTlVQqOnQ4FiMujR6T4DEcCb1xizmvXS+OxuhwcbtynoosRzdMA69zZCShCNAbi+gJ71FxZBBXx1SA==",
1506
1716
"license": "MIT"
1507
1717
},
1508
1718
"node_modules/jiti": {
···
1792
2002
"dev": true,
1793
2003
"license": "MIT"
1794
2004
},
2005
+
"node_modules/longest-streak": {
2006
+
"version": "3.1.0",
2007
+
"resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz",
2008
+
"integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==",
2009
+
"license": "MIT",
2010
+
"funding": {
2011
+
"type": "github",
2012
+
"url": "https://github.com/sponsors/wooorm"
2013
+
}
2014
+
},
1795
2015
"node_modules/magic-string": {
1796
2016
"version": "0.30.18",
1797
2017
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz",
···
1802
2022
"@jridgewell/sourcemap-codec": "^1.5.5"
1803
2023
}
1804
2024
},
2025
+
"node_modules/markdown-table": {
2026
+
"version": "3.0.4",
2027
+
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
2028
+
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
2029
+
"license": "MIT",
2030
+
"funding": {
2031
+
"type": "github",
2032
+
"url": "https://github.com/sponsors/wooorm"
2033
+
}
2034
+
},
2035
+
"node_modules/mdast-util-find-and-replace": {
2036
+
"version": "3.0.2",
2037
+
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
2038
+
"integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==",
2039
+
"license": "MIT",
2040
+
"dependencies": {
2041
+
"@types/mdast": "^4.0.0",
2042
+
"escape-string-regexp": "^5.0.0",
2043
+
"unist-util-is": "^6.0.0",
2044
+
"unist-util-visit-parents": "^6.0.0"
2045
+
},
2046
+
"funding": {
2047
+
"type": "opencollective",
2048
+
"url": "https://opencollective.com/unified"
2049
+
}
2050
+
},
2051
+
"node_modules/mdast-util-from-markdown": {
2052
+
"version": "2.0.2",
2053
+
"resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz",
2054
+
"integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==",
2055
+
"license": "MIT",
2056
+
"dependencies": {
2057
+
"@types/mdast": "^4.0.0",
2058
+
"@types/unist": "^3.0.0",
2059
+
"decode-named-character-reference": "^1.0.0",
2060
+
"devlop": "^1.0.0",
2061
+
"mdast-util-to-string": "^4.0.0",
2062
+
"micromark": "^4.0.0",
2063
+
"micromark-util-decode-numeric-character-reference": "^2.0.0",
2064
+
"micromark-util-decode-string": "^2.0.0",
2065
+
"micromark-util-normalize-identifier": "^2.0.0",
2066
+
"micromark-util-symbol": "^2.0.0",
2067
+
"micromark-util-types": "^2.0.0",
2068
+
"unist-util-stringify-position": "^4.0.0"
2069
+
},
2070
+
"funding": {
2071
+
"type": "opencollective",
2072
+
"url": "https://opencollective.com/unified"
2073
+
}
2074
+
},
2075
+
"node_modules/mdast-util-gfm": {
2076
+
"version": "3.1.0",
2077
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
2078
+
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
2079
+
"license": "MIT",
2080
+
"dependencies": {
2081
+
"mdast-util-from-markdown": "^2.0.0",
2082
+
"mdast-util-gfm-autolink-literal": "^2.0.0",
2083
+
"mdast-util-gfm-footnote": "^2.0.0",
2084
+
"mdast-util-gfm-strikethrough": "^2.0.0",
2085
+
"mdast-util-gfm-table": "^2.0.0",
2086
+
"mdast-util-gfm-task-list-item": "^2.0.0",
2087
+
"mdast-util-to-markdown": "^2.0.0"
2088
+
},
2089
+
"funding": {
2090
+
"type": "opencollective",
2091
+
"url": "https://opencollective.com/unified"
2092
+
}
2093
+
},
2094
+
"node_modules/mdast-util-gfm-autolink-literal": {
2095
+
"version": "2.0.1",
2096
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
2097
+
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
2098
+
"license": "MIT",
2099
+
"dependencies": {
2100
+
"@types/mdast": "^4.0.0",
2101
+
"ccount": "^2.0.0",
2102
+
"devlop": "^1.0.0",
2103
+
"mdast-util-find-and-replace": "^3.0.0",
2104
+
"micromark-util-character": "^2.0.0"
2105
+
},
2106
+
"funding": {
2107
+
"type": "opencollective",
2108
+
"url": "https://opencollective.com/unified"
2109
+
}
2110
+
},
2111
+
"node_modules/mdast-util-gfm-footnote": {
2112
+
"version": "2.1.0",
2113
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
2114
+
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
2115
+
"license": "MIT",
2116
+
"dependencies": {
2117
+
"@types/mdast": "^4.0.0",
2118
+
"devlop": "^1.1.0",
2119
+
"mdast-util-from-markdown": "^2.0.0",
2120
+
"mdast-util-to-markdown": "^2.0.0",
2121
+
"micromark-util-normalize-identifier": "^2.0.0"
2122
+
},
2123
+
"funding": {
2124
+
"type": "opencollective",
2125
+
"url": "https://opencollective.com/unified"
2126
+
}
2127
+
},
2128
+
"node_modules/mdast-util-gfm-strikethrough": {
2129
+
"version": "2.0.0",
2130
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
2131
+
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
2132
+
"license": "MIT",
2133
+
"dependencies": {
2134
+
"@types/mdast": "^4.0.0",
2135
+
"mdast-util-from-markdown": "^2.0.0",
2136
+
"mdast-util-to-markdown": "^2.0.0"
2137
+
},
2138
+
"funding": {
2139
+
"type": "opencollective",
2140
+
"url": "https://opencollective.com/unified"
2141
+
}
2142
+
},
2143
+
"node_modules/mdast-util-gfm-table": {
2144
+
"version": "2.0.0",
2145
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
2146
+
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
2147
+
"license": "MIT",
2148
+
"dependencies": {
2149
+
"@types/mdast": "^4.0.0",
2150
+
"devlop": "^1.0.0",
2151
+
"markdown-table": "^3.0.0",
2152
+
"mdast-util-from-markdown": "^2.0.0",
2153
+
"mdast-util-to-markdown": "^2.0.0"
2154
+
},
2155
+
"funding": {
2156
+
"type": "opencollective",
2157
+
"url": "https://opencollective.com/unified"
2158
+
}
2159
+
},
2160
+
"node_modules/mdast-util-gfm-task-list-item": {
2161
+
"version": "2.0.0",
2162
+
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
2163
+
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
2164
+
"license": "MIT",
2165
+
"dependencies": {
2166
+
"@types/mdast": "^4.0.0",
2167
+
"devlop": "^1.0.0",
2168
+
"mdast-util-from-markdown": "^2.0.0",
2169
+
"mdast-util-to-markdown": "^2.0.0"
2170
+
},
2171
+
"funding": {
2172
+
"type": "opencollective",
2173
+
"url": "https://opencollective.com/unified"
2174
+
}
2175
+
},
2176
+
"node_modules/mdast-util-phrasing": {
2177
+
"version": "4.1.0",
2178
+
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
2179
+
"integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==",
2180
+
"license": "MIT",
2181
+
"dependencies": {
2182
+
"@types/mdast": "^4.0.0",
2183
+
"unist-util-is": "^6.0.0"
2184
+
},
2185
+
"funding": {
2186
+
"type": "opencollective",
2187
+
"url": "https://opencollective.com/unified"
2188
+
}
2189
+
},
2190
+
"node_modules/mdast-util-to-markdown": {
2191
+
"version": "2.1.2",
2192
+
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
2193
+
"integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==",
2194
+
"license": "MIT",
2195
+
"dependencies": {
2196
+
"@types/mdast": "^4.0.0",
2197
+
"@types/unist": "^3.0.0",
2198
+
"longest-streak": "^3.0.0",
2199
+
"mdast-util-phrasing": "^4.0.0",
2200
+
"mdast-util-to-string": "^4.0.0",
2201
+
"micromark-util-classify-character": "^2.0.0",
2202
+
"micromark-util-decode-string": "^2.0.0",
2203
+
"unist-util-visit": "^5.0.0",
2204
+
"zwitch": "^2.0.0"
2205
+
},
2206
+
"funding": {
2207
+
"type": "opencollective",
2208
+
"url": "https://opencollective.com/unified"
2209
+
}
2210
+
},
2211
+
"node_modules/mdast-util-to-string": {
2212
+
"version": "4.0.0",
2213
+
"resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz",
2214
+
"integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==",
2215
+
"license": "MIT",
2216
+
"dependencies": {
2217
+
"@types/mdast": "^4.0.0"
2218
+
},
2219
+
"funding": {
2220
+
"type": "opencollective",
2221
+
"url": "https://opencollective.com/unified"
2222
+
}
2223
+
},
2224
+
"node_modules/micromark": {
2225
+
"version": "4.0.2",
2226
+
"resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
2227
+
"integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==",
2228
+
"funding": [
2229
+
{
2230
+
"type": "GitHub Sponsors",
2231
+
"url": "https://github.com/sponsors/unifiedjs"
2232
+
},
2233
+
{
2234
+
"type": "OpenCollective",
2235
+
"url": "https://opencollective.com/unified"
2236
+
}
2237
+
],
2238
+
"license": "MIT",
2239
+
"dependencies": {
2240
+
"@types/debug": "^4.0.0",
2241
+
"debug": "^4.0.0",
2242
+
"decode-named-character-reference": "^1.0.0",
2243
+
"devlop": "^1.0.0",
2244
+
"micromark-core-commonmark": "^2.0.0",
2245
+
"micromark-factory-space": "^2.0.0",
2246
+
"micromark-util-character": "^2.0.0",
2247
+
"micromark-util-chunked": "^2.0.0",
2248
+
"micromark-util-combine-extensions": "^2.0.0",
2249
+
"micromark-util-decode-numeric-character-reference": "^2.0.0",
2250
+
"micromark-util-encode": "^2.0.0",
2251
+
"micromark-util-normalize-identifier": "^2.0.0",
2252
+
"micromark-util-resolve-all": "^2.0.0",
2253
+
"micromark-util-sanitize-uri": "^2.0.0",
2254
+
"micromark-util-subtokenize": "^2.0.0",
2255
+
"micromark-util-symbol": "^2.0.0",
2256
+
"micromark-util-types": "^2.0.0"
2257
+
}
2258
+
},
2259
+
"node_modules/micromark-core-commonmark": {
2260
+
"version": "2.0.3",
2261
+
"resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz",
2262
+
"integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==",
2263
+
"funding": [
2264
+
{
2265
+
"type": "GitHub Sponsors",
2266
+
"url": "https://github.com/sponsors/unifiedjs"
2267
+
},
2268
+
{
2269
+
"type": "OpenCollective",
2270
+
"url": "https://opencollective.com/unified"
2271
+
}
2272
+
],
2273
+
"license": "MIT",
2274
+
"dependencies": {
2275
+
"decode-named-character-reference": "^1.0.0",
2276
+
"devlop": "^1.0.0",
2277
+
"micromark-factory-destination": "^2.0.0",
2278
+
"micromark-factory-label": "^2.0.0",
2279
+
"micromark-factory-space": "^2.0.0",
2280
+
"micromark-factory-title": "^2.0.0",
2281
+
"micromark-factory-whitespace": "^2.0.0",
2282
+
"micromark-util-character": "^2.0.0",
2283
+
"micromark-util-chunked": "^2.0.0",
2284
+
"micromark-util-classify-character": "^2.0.0",
2285
+
"micromark-util-html-tag-name": "^2.0.0",
2286
+
"micromark-util-normalize-identifier": "^2.0.0",
2287
+
"micromark-util-resolve-all": "^2.0.0",
2288
+
"micromark-util-subtokenize": "^2.0.0",
2289
+
"micromark-util-symbol": "^2.0.0",
2290
+
"micromark-util-types": "^2.0.0"
2291
+
}
2292
+
},
2293
+
"node_modules/micromark-extension-gfm": {
2294
+
"version": "3.0.0",
2295
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
2296
+
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
2297
+
"license": "MIT",
2298
+
"dependencies": {
2299
+
"micromark-extension-gfm-autolink-literal": "^2.0.0",
2300
+
"micromark-extension-gfm-footnote": "^2.0.0",
2301
+
"micromark-extension-gfm-strikethrough": "^2.0.0",
2302
+
"micromark-extension-gfm-table": "^2.0.0",
2303
+
"micromark-extension-gfm-tagfilter": "^2.0.0",
2304
+
"micromark-extension-gfm-task-list-item": "^2.0.0",
2305
+
"micromark-util-combine-extensions": "^2.0.0",
2306
+
"micromark-util-types": "^2.0.0"
2307
+
},
2308
+
"funding": {
2309
+
"type": "opencollective",
2310
+
"url": "https://opencollective.com/unified"
2311
+
}
2312
+
},
2313
+
"node_modules/micromark-extension-gfm-autolink-literal": {
2314
+
"version": "2.1.0",
2315
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
2316
+
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
2317
+
"license": "MIT",
2318
+
"dependencies": {
2319
+
"micromark-util-character": "^2.0.0",
2320
+
"micromark-util-sanitize-uri": "^2.0.0",
2321
+
"micromark-util-symbol": "^2.0.0",
2322
+
"micromark-util-types": "^2.0.0"
2323
+
},
2324
+
"funding": {
2325
+
"type": "opencollective",
2326
+
"url": "https://opencollective.com/unified"
2327
+
}
2328
+
},
2329
+
"node_modules/micromark-extension-gfm-footnote": {
2330
+
"version": "2.1.0",
2331
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
2332
+
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
2333
+
"license": "MIT",
2334
+
"dependencies": {
2335
+
"devlop": "^1.0.0",
2336
+
"micromark-core-commonmark": "^2.0.0",
2337
+
"micromark-factory-space": "^2.0.0",
2338
+
"micromark-util-character": "^2.0.0",
2339
+
"micromark-util-normalize-identifier": "^2.0.0",
2340
+
"micromark-util-sanitize-uri": "^2.0.0",
2341
+
"micromark-util-symbol": "^2.0.0",
2342
+
"micromark-util-types": "^2.0.0"
2343
+
},
2344
+
"funding": {
2345
+
"type": "opencollective",
2346
+
"url": "https://opencollective.com/unified"
2347
+
}
2348
+
},
2349
+
"node_modules/micromark-extension-gfm-strikethrough": {
2350
+
"version": "2.1.0",
2351
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
2352
+
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
2353
+
"license": "MIT",
2354
+
"dependencies": {
2355
+
"devlop": "^1.0.0",
2356
+
"micromark-util-chunked": "^2.0.0",
2357
+
"micromark-util-classify-character": "^2.0.0",
2358
+
"micromark-util-resolve-all": "^2.0.0",
2359
+
"micromark-util-symbol": "^2.0.0",
2360
+
"micromark-util-types": "^2.0.0"
2361
+
},
2362
+
"funding": {
2363
+
"type": "opencollective",
2364
+
"url": "https://opencollective.com/unified"
2365
+
}
2366
+
},
2367
+
"node_modules/micromark-extension-gfm-table": {
2368
+
"version": "2.1.1",
2369
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
2370
+
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
2371
+
"license": "MIT",
2372
+
"dependencies": {
2373
+
"devlop": "^1.0.0",
2374
+
"micromark-factory-space": "^2.0.0",
2375
+
"micromark-util-character": "^2.0.0",
2376
+
"micromark-util-symbol": "^2.0.0",
2377
+
"micromark-util-types": "^2.0.0"
2378
+
},
2379
+
"funding": {
2380
+
"type": "opencollective",
2381
+
"url": "https://opencollective.com/unified"
2382
+
}
2383
+
},
2384
+
"node_modules/micromark-extension-gfm-tagfilter": {
2385
+
"version": "2.0.0",
2386
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
2387
+
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
2388
+
"license": "MIT",
2389
+
"dependencies": {
2390
+
"micromark-util-types": "^2.0.0"
2391
+
},
2392
+
"funding": {
2393
+
"type": "opencollective",
2394
+
"url": "https://opencollective.com/unified"
2395
+
}
2396
+
},
2397
+
"node_modules/micromark-extension-gfm-task-list-item": {
2398
+
"version": "2.1.0",
2399
+
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
2400
+
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
2401
+
"license": "MIT",
2402
+
"dependencies": {
2403
+
"devlop": "^1.0.0",
2404
+
"micromark-factory-space": "^2.0.0",
2405
+
"micromark-util-character": "^2.0.0",
2406
+
"micromark-util-symbol": "^2.0.0",
2407
+
"micromark-util-types": "^2.0.0"
2408
+
},
2409
+
"funding": {
2410
+
"type": "opencollective",
2411
+
"url": "https://opencollective.com/unified"
2412
+
}
2413
+
},
2414
+
"node_modules/micromark-factory-destination": {
2415
+
"version": "2.0.1",
2416
+
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
2417
+
"integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==",
2418
+
"funding": [
2419
+
{
2420
+
"type": "GitHub Sponsors",
2421
+
"url": "https://github.com/sponsors/unifiedjs"
2422
+
},
2423
+
{
2424
+
"type": "OpenCollective",
2425
+
"url": "https://opencollective.com/unified"
2426
+
}
2427
+
],
2428
+
"license": "MIT",
2429
+
"dependencies": {
2430
+
"micromark-util-character": "^2.0.0",
2431
+
"micromark-util-symbol": "^2.0.0",
2432
+
"micromark-util-types": "^2.0.0"
2433
+
}
2434
+
},
2435
+
"node_modules/micromark-factory-label": {
2436
+
"version": "2.0.1",
2437
+
"resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz",
2438
+
"integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==",
2439
+
"funding": [
2440
+
{
2441
+
"type": "GitHub Sponsors",
2442
+
"url": "https://github.com/sponsors/unifiedjs"
2443
+
},
2444
+
{
2445
+
"type": "OpenCollective",
2446
+
"url": "https://opencollective.com/unified"
2447
+
}
2448
+
],
2449
+
"license": "MIT",
2450
+
"dependencies": {
2451
+
"devlop": "^1.0.0",
2452
+
"micromark-util-character": "^2.0.0",
2453
+
"micromark-util-symbol": "^2.0.0",
2454
+
"micromark-util-types": "^2.0.0"
2455
+
}
2456
+
},
2457
+
"node_modules/micromark-factory-space": {
2458
+
"version": "2.0.1",
2459
+
"resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz",
2460
+
"integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==",
2461
+
"funding": [
2462
+
{
2463
+
"type": "GitHub Sponsors",
2464
+
"url": "https://github.com/sponsors/unifiedjs"
2465
+
},
2466
+
{
2467
+
"type": "OpenCollective",
2468
+
"url": "https://opencollective.com/unified"
2469
+
}
2470
+
],
2471
+
"license": "MIT",
2472
+
"dependencies": {
2473
+
"micromark-util-character": "^2.0.0",
2474
+
"micromark-util-types": "^2.0.0"
2475
+
}
2476
+
},
2477
+
"node_modules/micromark-factory-title": {
2478
+
"version": "2.0.1",
2479
+
"resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz",
2480
+
"integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==",
2481
+
"funding": [
2482
+
{
2483
+
"type": "GitHub Sponsors",
2484
+
"url": "https://github.com/sponsors/unifiedjs"
2485
+
},
2486
+
{
2487
+
"type": "OpenCollective",
2488
+
"url": "https://opencollective.com/unified"
2489
+
}
2490
+
],
2491
+
"license": "MIT",
2492
+
"dependencies": {
2493
+
"micromark-factory-space": "^2.0.0",
2494
+
"micromark-util-character": "^2.0.0",
2495
+
"micromark-util-symbol": "^2.0.0",
2496
+
"micromark-util-types": "^2.0.0"
2497
+
}
2498
+
},
2499
+
"node_modules/micromark-factory-whitespace": {
2500
+
"version": "2.0.1",
2501
+
"resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz",
2502
+
"integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==",
2503
+
"funding": [
2504
+
{
2505
+
"type": "GitHub Sponsors",
2506
+
"url": "https://github.com/sponsors/unifiedjs"
2507
+
},
2508
+
{
2509
+
"type": "OpenCollective",
2510
+
"url": "https://opencollective.com/unified"
2511
+
}
2512
+
],
2513
+
"license": "MIT",
2514
+
"dependencies": {
2515
+
"micromark-factory-space": "^2.0.0",
2516
+
"micromark-util-character": "^2.0.0",
2517
+
"micromark-util-symbol": "^2.0.0",
2518
+
"micromark-util-types": "^2.0.0"
2519
+
}
2520
+
},
2521
+
"node_modules/micromark-util-character": {
2522
+
"version": "2.1.1",
2523
+
"resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz",
2524
+
"integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==",
2525
+
"funding": [
2526
+
{
2527
+
"type": "GitHub Sponsors",
2528
+
"url": "https://github.com/sponsors/unifiedjs"
2529
+
},
2530
+
{
2531
+
"type": "OpenCollective",
2532
+
"url": "https://opencollective.com/unified"
2533
+
}
2534
+
],
2535
+
"license": "MIT",
2536
+
"dependencies": {
2537
+
"micromark-util-symbol": "^2.0.0",
2538
+
"micromark-util-types": "^2.0.0"
2539
+
}
2540
+
},
2541
+
"node_modules/micromark-util-chunked": {
2542
+
"version": "2.0.1",
2543
+
"resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz",
2544
+
"integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==",
2545
+
"funding": [
2546
+
{
2547
+
"type": "GitHub Sponsors",
2548
+
"url": "https://github.com/sponsors/unifiedjs"
2549
+
},
2550
+
{
2551
+
"type": "OpenCollective",
2552
+
"url": "https://opencollective.com/unified"
2553
+
}
2554
+
],
2555
+
"license": "MIT",
2556
+
"dependencies": {
2557
+
"micromark-util-symbol": "^2.0.0"
2558
+
}
2559
+
},
2560
+
"node_modules/micromark-util-classify-character": {
2561
+
"version": "2.0.1",
2562
+
"resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz",
2563
+
"integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==",
2564
+
"funding": [
2565
+
{
2566
+
"type": "GitHub Sponsors",
2567
+
"url": "https://github.com/sponsors/unifiedjs"
2568
+
},
2569
+
{
2570
+
"type": "OpenCollective",
2571
+
"url": "https://opencollective.com/unified"
2572
+
}
2573
+
],
2574
+
"license": "MIT",
2575
+
"dependencies": {
2576
+
"micromark-util-character": "^2.0.0",
2577
+
"micromark-util-symbol": "^2.0.0",
2578
+
"micromark-util-types": "^2.0.0"
2579
+
}
2580
+
},
2581
+
"node_modules/micromark-util-combine-extensions": {
2582
+
"version": "2.0.1",
2583
+
"resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz",
2584
+
"integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==",
2585
+
"funding": [
2586
+
{
2587
+
"type": "GitHub Sponsors",
2588
+
"url": "https://github.com/sponsors/unifiedjs"
2589
+
},
2590
+
{
2591
+
"type": "OpenCollective",
2592
+
"url": "https://opencollective.com/unified"
2593
+
}
2594
+
],
2595
+
"license": "MIT",
2596
+
"dependencies": {
2597
+
"micromark-util-chunked": "^2.0.0",
2598
+
"micromark-util-types": "^2.0.0"
2599
+
}
2600
+
},
2601
+
"node_modules/micromark-util-decode-numeric-character-reference": {
2602
+
"version": "2.0.2",
2603
+
"resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz",
2604
+
"integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==",
2605
+
"funding": [
2606
+
{
2607
+
"type": "GitHub Sponsors",
2608
+
"url": "https://github.com/sponsors/unifiedjs"
2609
+
},
2610
+
{
2611
+
"type": "OpenCollective",
2612
+
"url": "https://opencollective.com/unified"
2613
+
}
2614
+
],
2615
+
"license": "MIT",
2616
+
"dependencies": {
2617
+
"micromark-util-symbol": "^2.0.0"
2618
+
}
2619
+
},
2620
+
"node_modules/micromark-util-decode-string": {
2621
+
"version": "2.0.1",
2622
+
"resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz",
2623
+
"integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==",
2624
+
"funding": [
2625
+
{
2626
+
"type": "GitHub Sponsors",
2627
+
"url": "https://github.com/sponsors/unifiedjs"
2628
+
},
2629
+
{
2630
+
"type": "OpenCollective",
2631
+
"url": "https://opencollective.com/unified"
2632
+
}
2633
+
],
2634
+
"license": "MIT",
2635
+
"dependencies": {
2636
+
"decode-named-character-reference": "^1.0.0",
2637
+
"micromark-util-character": "^2.0.0",
2638
+
"micromark-util-decode-numeric-character-reference": "^2.0.0",
2639
+
"micromark-util-symbol": "^2.0.0"
2640
+
}
2641
+
},
2642
+
"node_modules/micromark-util-encode": {
2643
+
"version": "2.0.1",
2644
+
"resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz",
2645
+
"integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==",
2646
+
"funding": [
2647
+
{
2648
+
"type": "GitHub Sponsors",
2649
+
"url": "https://github.com/sponsors/unifiedjs"
2650
+
},
2651
+
{
2652
+
"type": "OpenCollective",
2653
+
"url": "https://opencollective.com/unified"
2654
+
}
2655
+
],
2656
+
"license": "MIT"
2657
+
},
2658
+
"node_modules/micromark-util-html-tag-name": {
2659
+
"version": "2.0.1",
2660
+
"resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz",
2661
+
"integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==",
2662
+
"funding": [
2663
+
{
2664
+
"type": "GitHub Sponsors",
2665
+
"url": "https://github.com/sponsors/unifiedjs"
2666
+
},
2667
+
{
2668
+
"type": "OpenCollective",
2669
+
"url": "https://opencollective.com/unified"
2670
+
}
2671
+
],
2672
+
"license": "MIT"
2673
+
},
2674
+
"node_modules/micromark-util-normalize-identifier": {
2675
+
"version": "2.0.1",
2676
+
"resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz",
2677
+
"integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==",
2678
+
"funding": [
2679
+
{
2680
+
"type": "GitHub Sponsors",
2681
+
"url": "https://github.com/sponsors/unifiedjs"
2682
+
},
2683
+
{
2684
+
"type": "OpenCollective",
2685
+
"url": "https://opencollective.com/unified"
2686
+
}
2687
+
],
2688
+
"license": "MIT",
2689
+
"dependencies": {
2690
+
"micromark-util-symbol": "^2.0.0"
2691
+
}
2692
+
},
2693
+
"node_modules/micromark-util-resolve-all": {
2694
+
"version": "2.0.1",
2695
+
"resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz",
2696
+
"integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==",
2697
+
"funding": [
2698
+
{
2699
+
"type": "GitHub Sponsors",
2700
+
"url": "https://github.com/sponsors/unifiedjs"
2701
+
},
2702
+
{
2703
+
"type": "OpenCollective",
2704
+
"url": "https://opencollective.com/unified"
2705
+
}
2706
+
],
2707
+
"license": "MIT",
2708
+
"dependencies": {
2709
+
"micromark-util-types": "^2.0.0"
2710
+
}
2711
+
},
2712
+
"node_modules/micromark-util-sanitize-uri": {
2713
+
"version": "2.0.1",
2714
+
"resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz",
2715
+
"integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==",
2716
+
"funding": [
2717
+
{
2718
+
"type": "GitHub Sponsors",
2719
+
"url": "https://github.com/sponsors/unifiedjs"
2720
+
},
2721
+
{
2722
+
"type": "OpenCollective",
2723
+
"url": "https://opencollective.com/unified"
2724
+
}
2725
+
],
2726
+
"license": "MIT",
2727
+
"dependencies": {
2728
+
"micromark-util-character": "^2.0.0",
2729
+
"micromark-util-encode": "^2.0.0",
2730
+
"micromark-util-symbol": "^2.0.0"
2731
+
}
2732
+
},
2733
+
"node_modules/micromark-util-subtokenize": {
2734
+
"version": "2.1.0",
2735
+
"resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz",
2736
+
"integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==",
2737
+
"funding": [
2738
+
{
2739
+
"type": "GitHub Sponsors",
2740
+
"url": "https://github.com/sponsors/unifiedjs"
2741
+
},
2742
+
{
2743
+
"type": "OpenCollective",
2744
+
"url": "https://opencollective.com/unified"
2745
+
}
2746
+
],
2747
+
"license": "MIT",
2748
+
"dependencies": {
2749
+
"devlop": "^1.0.0",
2750
+
"micromark-util-chunked": "^2.0.0",
2751
+
"micromark-util-symbol": "^2.0.0",
2752
+
"micromark-util-types": "^2.0.0"
2753
+
}
2754
+
},
2755
+
"node_modules/micromark-util-symbol": {
2756
+
"version": "2.0.1",
2757
+
"resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz",
2758
+
"integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==",
2759
+
"funding": [
2760
+
{
2761
+
"type": "GitHub Sponsors",
2762
+
"url": "https://github.com/sponsors/unifiedjs"
2763
+
},
2764
+
{
2765
+
"type": "OpenCollective",
2766
+
"url": "https://opencollective.com/unified"
2767
+
}
2768
+
],
2769
+
"license": "MIT"
2770
+
},
2771
+
"node_modules/micromark-util-types": {
2772
+
"version": "2.0.2",
2773
+
"resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz",
2774
+
"integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==",
2775
+
"funding": [
2776
+
{
2777
+
"type": "GitHub Sponsors",
2778
+
"url": "https://github.com/sponsors/unifiedjs"
2779
+
},
2780
+
{
2781
+
"type": "OpenCollective",
2782
+
"url": "https://opencollective.com/unified"
2783
+
}
2784
+
],
2785
+
"license": "MIT"
2786
+
},
1805
2787
"node_modules/minipass": {
1806
2788
"version": "7.1.2",
1807
2789
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
···
1865
2847
"version": "2.1.3",
1866
2848
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1867
2849
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1868
-
"dev": true,
1869
2850
"license": "MIT"
2851
+
},
2852
+
"node_modules/multiformats": {
2853
+
"version": "9.9.0",
2854
+
"resolved": "https://registry.npmjs.org/multiformats/-/multiformats-9.9.0.tgz",
2855
+
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
2856
+
"license": "(Apache-2.0 AND MIT)"
1870
2857
},
1871
2858
"node_modules/nanoid": {
1872
2859
"version": "3.3.11",
···
2091
3078
"url": "https://paulmillr.com/funding/"
2092
3079
}
2093
3080
},
3081
+
"node_modules/remark-gfm": {
3082
+
"version": "4.0.1",
3083
+
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
3084
+
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
3085
+
"license": "MIT",
3086
+
"dependencies": {
3087
+
"@types/mdast": "^4.0.0",
3088
+
"mdast-util-gfm": "^3.0.0",
3089
+
"micromark-extension-gfm": "^3.0.0",
3090
+
"remark-parse": "^11.0.0",
3091
+
"remark-stringify": "^11.0.0",
3092
+
"unified": "^11.0.0"
3093
+
},
3094
+
"funding": {
3095
+
"type": "opencollective",
3096
+
"url": "https://opencollective.com/unified"
3097
+
}
3098
+
},
3099
+
"node_modules/remark-parse": {
3100
+
"version": "11.0.0",
3101
+
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
3102
+
"integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==",
3103
+
"license": "MIT",
3104
+
"dependencies": {
3105
+
"@types/mdast": "^4.0.0",
3106
+
"mdast-util-from-markdown": "^2.0.0",
3107
+
"micromark-util-types": "^2.0.0",
3108
+
"unified": "^11.0.0"
3109
+
},
3110
+
"funding": {
3111
+
"type": "opencollective",
3112
+
"url": "https://opencollective.com/unified"
3113
+
}
3114
+
},
3115
+
"node_modules/remark-stringify": {
3116
+
"version": "11.0.0",
3117
+
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
3118
+
"integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==",
3119
+
"license": "MIT",
3120
+
"dependencies": {
3121
+
"@types/mdast": "^4.0.0",
3122
+
"mdast-util-to-markdown": "^2.0.0",
3123
+
"unified": "^11.0.0"
3124
+
},
3125
+
"funding": {
3126
+
"type": "opencollective",
3127
+
"url": "https://opencollective.com/unified"
3128
+
}
3129
+
},
2094
3130
"node_modules/rollup": {
2095
3131
"version": "4.50.0",
2096
3132
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.0.tgz",
···
2304
3340
"url": "https://github.com/sponsors/SuperchupuDev"
2305
3341
}
2306
3342
},
3343
+
"node_modules/tlds": {
3344
+
"version": "1.261.0",
3345
+
"resolved": "https://registry.npmjs.org/tlds/-/tlds-1.261.0.tgz",
3346
+
"integrity": "sha512-QXqwfEl9ddlGBaRFXIvNKK6OhipSiLXuRuLJX5DErz0o0Q0rYxulWLdFryTkV5PkdZct5iMInwYEGe/eR++1AA==",
3347
+
"license": "MIT",
3348
+
"bin": {
3349
+
"tlds": "bin.js"
3350
+
}
3351
+
},
2307
3352
"node_modules/totalist": {
2308
3353
"version": "3.0.1",
2309
3354
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
···
2314
3359
"node": ">=6"
2315
3360
}
2316
3361
},
3362
+
"node_modules/trough": {
3363
+
"version": "2.2.0",
3364
+
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
3365
+
"integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==",
3366
+
"license": "MIT",
3367
+
"funding": {
3368
+
"type": "github",
3369
+
"url": "https://github.com/sponsors/wooorm"
3370
+
}
3371
+
},
2317
3372
"node_modules/typescript": {
2318
3373
"version": "5.9.2",
2319
3374
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
···
2328
3383
"node": ">=14.17"
2329
3384
}
2330
3385
},
3386
+
"node_modules/uint8arrays": {
3387
+
"version": "3.0.0",
3388
+
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.0.0.tgz",
3389
+
"integrity": "sha512-HRCx0q6O9Bfbp+HHSfQQKD7wU70+lydKVt4EghkdOvlK/NlrF90z+eXV34mUd48rNvVJXwkrMSPpCATkct8fJA==",
3390
+
"license": "MIT",
3391
+
"dependencies": {
3392
+
"multiformats": "^9.4.2"
3393
+
}
3394
+
},
3395
+
"node_modules/unified": {
3396
+
"version": "11.0.5",
3397
+
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
3398
+
"integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==",
3399
+
"license": "MIT",
3400
+
"dependencies": {
3401
+
"@types/unist": "^3.0.0",
3402
+
"bail": "^2.0.0",
3403
+
"devlop": "^1.0.0",
3404
+
"extend": "^3.0.0",
3405
+
"is-plain-obj": "^4.0.0",
3406
+
"trough": "^2.0.0",
3407
+
"vfile": "^6.0.0"
3408
+
},
3409
+
"funding": {
3410
+
"type": "opencollective",
3411
+
"url": "https://opencollective.com/unified"
3412
+
}
3413
+
},
3414
+
"node_modules/unist-util-is": {
3415
+
"version": "6.0.1",
3416
+
"resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz",
3417
+
"integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==",
3418
+
"license": "MIT",
3419
+
"dependencies": {
3420
+
"@types/unist": "^3.0.0"
3421
+
},
3422
+
"funding": {
3423
+
"type": "opencollective",
3424
+
"url": "https://opencollective.com/unified"
3425
+
}
3426
+
},
3427
+
"node_modules/unist-util-stringify-position": {
3428
+
"version": "4.0.0",
3429
+
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
3430
+
"integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==",
3431
+
"license": "MIT",
3432
+
"dependencies": {
3433
+
"@types/unist": "^3.0.0"
3434
+
},
3435
+
"funding": {
3436
+
"type": "opencollective",
3437
+
"url": "https://opencollective.com/unified"
3438
+
}
3439
+
},
3440
+
"node_modules/unist-util-visit": {
3441
+
"version": "5.0.0",
3442
+
"resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz",
3443
+
"integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==",
3444
+
"license": "MIT",
3445
+
"dependencies": {
3446
+
"@types/unist": "^3.0.0",
3447
+
"unist-util-is": "^6.0.0",
3448
+
"unist-util-visit-parents": "^6.0.0"
3449
+
},
3450
+
"funding": {
3451
+
"type": "opencollective",
3452
+
"url": "https://opencollective.com/unified"
3453
+
}
3454
+
},
3455
+
"node_modules/unist-util-visit-parents": {
3456
+
"version": "6.0.2",
3457
+
"resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz",
3458
+
"integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==",
3459
+
"license": "MIT",
3460
+
"dependencies": {
3461
+
"@types/unist": "^3.0.0",
3462
+
"unist-util-is": "^6.0.0"
3463
+
},
3464
+
"funding": {
3465
+
"type": "opencollective",
3466
+
"url": "https://opencollective.com/unified"
3467
+
}
3468
+
},
2331
3469
"node_modules/util-deprecate": {
2332
3470
"version": "1.0.2",
2333
3471
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
2334
3472
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
2335
3473
"license": "MIT"
3474
+
},
3475
+
"node_modules/vfile": {
3476
+
"version": "6.0.3",
3477
+
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
3478
+
"integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==",
3479
+
"license": "MIT",
3480
+
"dependencies": {
3481
+
"@types/unist": "^3.0.0",
3482
+
"vfile-message": "^4.0.0"
3483
+
},
3484
+
"funding": {
3485
+
"type": "opencollective",
3486
+
"url": "https://opencollective.com/unified"
3487
+
}
3488
+
},
3489
+
"node_modules/vfile-message": {
3490
+
"version": "4.0.3",
3491
+
"resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz",
3492
+
"integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==",
3493
+
"license": "MIT",
3494
+
"dependencies": {
3495
+
"@types/unist": "^3.0.0",
3496
+
"unist-util-stringify-position": "^4.0.0"
3497
+
},
3498
+
"funding": {
3499
+
"type": "opencollective",
3500
+
"url": "https://opencollective.com/unified"
3501
+
}
2336
3502
},
2337
3503
"node_modules/vite": {
2338
3504
"version": "7.1.4",
···
2445
3611
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
2446
3612
"dev": true,
2447
3613
"license": "MIT"
3614
+
},
3615
+
"node_modules/zod": {
3616
+
"version": "3.25.76",
3617
+
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
3618
+
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
3619
+
"license": "MIT",
3620
+
"funding": {
3621
+
"url": "https://github.com/sponsors/colinhacks"
3622
+
}
3623
+
},
3624
+
"node_modules/zwitch": {
3625
+
"version": "2.0.4",
3626
+
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
3627
+
"integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==",
3628
+
"license": "MIT",
3629
+
"funding": {
3630
+
"type": "github",
3631
+
"url": "https://github.com/sponsors/wooorm"
3632
+
}
2448
3633
}
2449
3634
}
2450
3635
}
+6
-1
package.json
+6
-1
package.json
···
28
28
"vite": "^7.0.4"
29
29
}
30
30
,"dependencies": {
31
+
"@atproto/api": "^0.13.16",
31
32
"jszip": "^3.10.1",
32
-
"file-saver": "^2.0.5"
33
+
"file-saver": "^2.0.5",
34
+
"unified": "^11.0.4",
35
+
"remark-parse": "^11.0.0",
36
+
"remark-gfm": "^4.0.0",
37
+
"mdast-util-to-string": "^4.0.0"
33
38
}
34
39
}
+305
src/lib/auth.ts
+305
src/lib/auth.ts
···
1
+
// AT Protocol authentication utilities for Leaflet publishing
2
+
import { AtpAgent } from '@atproto/api';
3
+
4
+
let agent: AtpAgent | null = null;
5
+
6
+
interface ResolvedIdentity {
7
+
did: string;
8
+
handle: string;
9
+
pds: string;
10
+
signing_key: string;
11
+
}
12
+
13
+
/**
14
+
* Resolves an AT Protocol identifier (handle or DID) to get PDS information
15
+
*/
16
+
async function resolveIdentifier(identifier: string): Promise<ResolvedIdentity> {
17
+
const response = await fetch(
18
+
`https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(identifier)}`
19
+
);
20
+
21
+
if (!response.ok) {
22
+
throw new Error(`Failed to resolve identifier: ${response.status} ${response.statusText}`);
23
+
}
24
+
25
+
const data = await response.json();
26
+
27
+
if (!data.did || !data.pds) {
28
+
throw new Error('Invalid response from identity resolver');
29
+
}
30
+
31
+
return data;
32
+
}
33
+
34
+
/**
35
+
* Logs in a user with their AT Protocol handle/DID and app password
36
+
*/
37
+
export async function login(identifier: string, password: string): Promise<void> {
38
+
try {
39
+
const resolved = await resolveIdentifier(identifier);
40
+
41
+
agent = new AtpAgent({
42
+
service: resolved.pds
43
+
});
44
+
45
+
await agent.login({
46
+
identifier: resolved.did,
47
+
password: password
48
+
});
49
+
50
+
localStorage.setItem(
51
+
'atproto_session',
52
+
JSON.stringify({
53
+
session: agent.session,
54
+
pdsUrl: resolved.pds,
55
+
resolvedData: resolved
56
+
})
57
+
);
58
+
} catch (e: any) {
59
+
console.error('Login failed:', e);
60
+
localStorage.removeItem('atproto_session');
61
+
62
+
if (e.message.includes('Failed to resolve identifier')) {
63
+
throw new Error('Handle not found. Please check your AT Protocol handle.');
64
+
} else if (e.message.includes('AuthFactorTokenRequired')) {
65
+
throw new Error(
66
+
'Two-factor authentication required. Please use your app password.'
67
+
);
68
+
} else if (
69
+
e.message.includes('AccountTakedown') ||
70
+
e.message.includes('AccountSuspended')
71
+
) {
72
+
throw new Error('Account is suspended or has been taken down.');
73
+
} else if (e.message.includes('InvalidCredentials')) {
74
+
throw new Error('Invalid credentials. Please check your handle and app password.');
75
+
} else {
76
+
throw new Error(`Login failed: ${e.message || 'Unknown error'}`);
77
+
}
78
+
}
79
+
}
80
+
81
+
/**
82
+
* Checks if a user is currently logged in
83
+
*/
84
+
export function isLoggedIn(): boolean {
85
+
if (agent?.session) {
86
+
return true;
87
+
}
88
+
const session = localStorage.getItem('atproto_session');
89
+
return !!session;
90
+
}
91
+
92
+
/**
93
+
* Refreshes the current user session
94
+
*/
95
+
export async function refreshSession(): Promise<void> {
96
+
let currentSession = agent?.session;
97
+
let pdsServiceUrl: string | undefined;
98
+
99
+
if (!currentSession) {
100
+
const storedData = localStorage.getItem('atproto_session');
101
+
if (storedData) {
102
+
try {
103
+
const parsedData = JSON.parse(storedData);
104
+
currentSession = parsedData.session;
105
+
pdsServiceUrl = parsedData.pdsUrl;
106
+
} catch (e) {
107
+
console.error('Failed to parse stored session:', e);
108
+
localStorage.removeItem('atproto_session');
109
+
throw new Error('Invalid stored session. Please log in again.');
110
+
}
111
+
}
112
+
} else if (agent?.service) {
113
+
pdsServiceUrl = agent.service.toString();
114
+
}
115
+
116
+
if (!currentSession || !pdsServiceUrl) {
117
+
throw new Error('No session or PDS URL found to refresh.');
118
+
}
119
+
120
+
if (!agent || agent.service.toString() !== pdsServiceUrl) {
121
+
agent = new AtpAgent({
122
+
service: pdsServiceUrl
123
+
});
124
+
}
125
+
126
+
try {
127
+
await agent.resumeSession(currentSession);
128
+
129
+
const storedData = localStorage.getItem('atproto_session');
130
+
if (storedData) {
131
+
const parsedData = JSON.parse(storedData);
132
+
localStorage.setItem(
133
+
'atproto_session',
134
+
JSON.stringify({
135
+
...parsedData,
136
+
session: agent.session
137
+
})
138
+
);
139
+
}
140
+
} catch (e) {
141
+
console.error('Failed to refresh session:', e);
142
+
localStorage.removeItem('atproto_session');
143
+
throw new Error('Session refresh failed. Please log in again.');
144
+
}
145
+
}
146
+
147
+
/**
148
+
* Logs out the current user
149
+
*/
150
+
export function logout(): void {
151
+
agent = null;
152
+
localStorage.removeItem('atproto_session');
153
+
}
154
+
155
+
/**
156
+
* Gets the current user's handle
157
+
*/
158
+
export function getCurrentUserHandle(): string | null {
159
+
return agent?.session?.handle || null;
160
+
}
161
+
162
+
/**
163
+
* Gets the current user's DID
164
+
*/
165
+
export function getCurrentUserDid(): string | null {
166
+
return agent?.session?.did || null;
167
+
}
168
+
169
+
/**
170
+
* Gets the PDS URL for the current user
171
+
*/
172
+
export function getCurrentPdsUrl(): string | null {
173
+
const storedData = localStorage.getItem('atproto_session');
174
+
if (storedData) {
175
+
try {
176
+
const parsedData = JSON.parse(storedData);
177
+
return parsedData.pdsUrl || null;
178
+
} catch (e) {
179
+
return null;
180
+
}
181
+
}
182
+
return null;
183
+
}
184
+
185
+
/**
186
+
* Fetches all WhiteWind blog entries for the current user
187
+
*/
188
+
export async function fetchWhiteWindEntries(): Promise<any[]> {
189
+
if (!agent || !agent.session) {
190
+
throw new Error('Not logged in. Cannot fetch entries.');
191
+
}
192
+
193
+
try {
194
+
const response = await agent.com.atproto.repo.listRecords({
195
+
repo: agent.session.did,
196
+
collection: 'com.whtwnd.blog.entry'
197
+
});
198
+
199
+
return response.data.records || [];
200
+
} catch (e) {
201
+
console.error('Failed to fetch WhiteWind entries:', e);
202
+
throw new Error(
203
+
`Failed to fetch entries: ${e instanceof Error ? e.message : 'Unknown error'}`
204
+
);
205
+
}
206
+
}
207
+
208
+
/**
209
+
* Creates a new Leaflet publication
210
+
*/
211
+
export async function createPublication(publication: any, rkey: string): Promise<string> {
212
+
if (!agent || !agent.session) {
213
+
throw new Error('Not logged in. Cannot create publication.');
214
+
}
215
+
216
+
try {
217
+
await agent.com.atproto.repo.createRecord({
218
+
repo: agent.session.did,
219
+
collection: 'pub.leaflet.publication',
220
+
rkey: rkey,
221
+
record: publication
222
+
});
223
+
224
+
return `at://${agent.session.did}/pub.leaflet.publication/${rkey}`;
225
+
} catch (e) {
226
+
console.error('Failed to create publication:', e);
227
+
throw new Error(
228
+
`Failed to create publication: ${e instanceof Error ? e.message : 'Unknown error'}`
229
+
);
230
+
}
231
+
}
232
+
233
+
/**
234
+
* Creates a new Leaflet document
235
+
*/
236
+
export async function createDocument(document: any, rkey: string): Promise<string> {
237
+
if (!agent || !agent.session) {
238
+
throw new Error('Not logged in. Cannot create document.');
239
+
}
240
+
241
+
try {
242
+
await agent.com.atproto.repo.createRecord({
243
+
repo: agent.session.did,
244
+
collection: 'pub.leaflet.document',
245
+
rkey: rkey,
246
+
record: document
247
+
});
248
+
249
+
return `at://${agent.session.did}/pub.leaflet.document/${rkey}`;
250
+
} catch (e) {
251
+
console.error('Failed to create document:', e);
252
+
throw new Error(
253
+
`Failed to create document: ${e instanceof Error ? e.message : 'Unknown error'}`
254
+
);
255
+
}
256
+
}
257
+
258
+
/**
259
+
* Publishes all converted documents to AT Protocol
260
+
*/
261
+
export async function publishToAtProto(
262
+
publicationRecord: any | null,
263
+
documents: any[],
264
+
onProgress?: (current: number, total: number, message: string) => void
265
+
): Promise<void> {
266
+
if (!agent || !agent.session) {
267
+
throw new Error('Not logged in. Cannot publish.');
268
+
}
269
+
270
+
try {
271
+
// Create publication if provided
272
+
if (publicationRecord) {
273
+
onProgress?.(0, documents.length + 1, 'Creating publication...');
274
+
await createPublication(publicationRecord, publicationRecord.rkey);
275
+
}
276
+
277
+
// Create each document
278
+
for (let i = 0; i < documents.length; i++) {
279
+
const doc = documents[i];
280
+
onProgress?.(
281
+
i + (publicationRecord ? 1 : 0),
282
+
documents.length + (publicationRecord ? 1 : 0),
283
+
`Publishing document ${i + 1}/${documents.length}...`
284
+
);
285
+
286
+
// Remove rkey from document before publishing (it's in the URI)
287
+
const { rkey, ...documentWithoutRkey } = doc;
288
+
await createDocument(documentWithoutRkey, rkey);
289
+
}
290
+
291
+
onProgress?.(
292
+
documents.length + (publicationRecord ? 1 : 0),
293
+
documents.length + (publicationRecord ? 1 : 0),
294
+
'Publishing complete!'
295
+
);
296
+
} catch (e) {
297
+
console.error('Failed to publish:', e);
298
+
throw e;
299
+
}
300
+
}
301
+
302
+
// Initialize agent on page load if a session exists
303
+
if (typeof window !== 'undefined' && localStorage.getItem('atproto_session')) {
304
+
refreshSession().catch((e) => console.error('Initial session refresh failed:', e));
305
+
}
+508
-177
src/lib/convert.ts
+508
-177
src/lib/convert.ts
···
1
-
// Conversion utilities ported from the original script.js and typed for TS/Svelte use
1
+
// Conversion utilities with remark-based markdown parsing
2
+
// Fully compliant with Leaflet lexicons
3
+
4
+
import { unified } from 'unified';
5
+
import remarkParse from 'remark-parse';
6
+
import remarkGfm from 'remark-gfm';
7
+
import { toString } from 'mdast-util-to-string';
8
+
import type {
9
+
Root,
10
+
PhrasingContent,
11
+
Heading,
12
+
Paragraph,
13
+
Code,
14
+
Blockquote,
15
+
ThematicBreak,
16
+
Image,
17
+
List,
18
+
ListItem,
19
+
Link,
20
+
Text,
21
+
Strong,
22
+
Emphasis,
23
+
InlineCode
24
+
} from 'mdast';
2
25
3
26
const BASE32_SORTABLE = '234567abcdefghijklmnopqrstuvwxyz';
4
27
···
31
54
: null;
32
55
}
33
56
34
-
export function convertBlobUrlToAtUri(url: string, did: string) {
35
-
const blobUrlRegex = /xrpc\/com\.atproto\.sync\.getBlob\?did=([^&]+)&cid=([^&\s]+)/;
36
-
const match = url.match(blobUrlRegex);
57
+
/**
58
+
* Extracts CID from various blob URL formats
59
+
*/
60
+
function extractCid(url: string): string | null {
61
+
// Handle XRPC getBlob URLs: xrpc/com.atproto.sync.getBlob?did=X&cid=Y
62
+
const xrpcRegex = /xrpc\/com\.atproto\.sync\.getBlob\?did=([^&]+)&cid=([^&\s]+)/;
63
+
const xrpcMatch = url.match(xrpcRegex);
37
64
38
-
if (match) {
39
-
const [, extractedDid, cid] = match;
40
-
return `at://${decodeURIComponent(extractedDid)}/com.whtwnd.blog.entry/${cid}`;
65
+
if (xrpcMatch) {
66
+
return xrpcMatch[2];
41
67
}
42
68
43
-
if (url.includes('bafk') || url.includes('bafyb')) {
44
-
const cidMatch = url.match(/(bafk[a-z0-9]+|bafyb[a-z0-9]+)/);
45
-
if (cidMatch) return `at://${did}/com.atproto.blob/${cidMatch[1]}`;
69
+
// Handle direct CID references (bafk or bafyb prefixes)
70
+
const cidRegex = /(bafk[a-z0-9]+|bafyb[a-z0-9]+)/i;
71
+
const cidMatch = url.match(cidRegex);
72
+
73
+
if (cidMatch) {
74
+
return cidMatch[1];
75
+
}
76
+
77
+
return null;
78
+
}
79
+
80
+
/**
81
+
* Fetches actual image dimensions from a blob URL
82
+
*/
83
+
async function getImageDimensions(
84
+
url: string
85
+
): Promise<{ width: number; height: number } | null> {
86
+
try {
87
+
const img = new Image();
88
+
89
+
return new Promise((resolve) => {
90
+
img.onload = () => {
91
+
resolve({ width: img.naturalWidth, height: img.naturalHeight });
92
+
};
93
+
94
+
img.onerror = () => {
95
+
console.warn(`Failed to load image for dimensions: ${url}`);
96
+
resolve(null);
97
+
};
98
+
99
+
setTimeout(() => {
100
+
console.warn(`Timeout loading image for dimensions: ${url}`);
101
+
resolve(null);
102
+
}, 10000);
103
+
104
+
img.src = url;
105
+
});
106
+
} catch (error) {
107
+
console.error('Error getting image dimensions:', error);
108
+
return null;
109
+
}
110
+
}
111
+
112
+
/**
113
+
* Converts XRPC blob URLs to proper Leaflet image blob format
114
+
*/
115
+
export function convertBlobToImageRef(
116
+
url: string,
117
+
did: string
118
+
): { $type: string; ref: { $link: string } } | null {
119
+
const cid = extractCid(url);
120
+
121
+
if (!cid) {
122
+
return null;
123
+
}
124
+
125
+
return {
126
+
$type: 'blob',
127
+
ref: {
128
+
$link: cid
129
+
}
130
+
};
131
+
}
132
+
133
+
/**
134
+
* Converts blob URLs to AT-URI format for links
135
+
*/
136
+
export function convertBlobUrlToAtUri(url: string, did: string): string {
137
+
const cid = extractCid(url);
138
+
139
+
if (cid) {
140
+
return `at://${did}/com.atproto.blob/${cid}`;
46
141
}
47
142
48
143
return url;
49
144
}
50
145
51
-
export function parseRichText(text: string, authorDid: string) {
52
-
let plaintext = text;
146
+
/**
147
+
* Process phrasing content (inline elements) and extract facets
148
+
* Strictly follows pub.leaflet.richtext.facet lexicon
149
+
*/
150
+
function processPhrasingContent(
151
+
content: PhrasingContent[],
152
+
authorDid: string
153
+
): { plaintext: string; facets: any[] } {
154
+
let plaintext = '';
53
155
const facets: any[] = [];
54
156
const utf8Encoder = new TextEncoder();
55
157
56
-
const linkRegex = /\[([^\]]+)\]\(([^)]+)\)/g;
57
-
let linkMatch: RegExpExecArray | null;
58
-
const linkReplacements: { start: number; end: number; text: string; uri: string }[] = [];
59
-
while ((linkMatch = linkRegex.exec(text)) !== null) {
60
-
const fullMatch = linkMatch[0];
61
-
const linkText = linkMatch[1];
62
-
const uri = linkMatch[2];
63
-
const convertedUri = convertBlobUrlToAtUri(uri, authorDid);
158
+
for (const node of content) {
159
+
if (node.type === 'text') {
160
+
plaintext += (node as Text).value;
161
+
} else if (node.type === 'strong') {
162
+
const strongNode = node as Strong;
163
+
const innerText = toString(strongNode);
164
+
const byteStart = utf8Encoder.encode(plaintext).length;
165
+
plaintext += innerText;
166
+
const byteEnd = utf8Encoder.encode(plaintext).length;
167
+
168
+
facets.push({
169
+
index: { byteStart, byteEnd },
170
+
features: [{ $type: 'pub.leaflet.richtext.facet#bold' }]
171
+
});
172
+
} else if (node.type === 'emphasis') {
173
+
const emphNode = node as Emphasis;
174
+
const innerText = toString(emphNode);
175
+
const byteStart = utf8Encoder.encode(plaintext).length;
176
+
plaintext += innerText;
177
+
const byteEnd = utf8Encoder.encode(plaintext).length;
178
+
179
+
facets.push({
180
+
index: { byteStart, byteEnd },
181
+
features: [{ $type: 'pub.leaflet.richtext.facet#italic' }]
182
+
});
183
+
} else if (node.type === 'inlineCode') {
184
+
const codeNode = node as InlineCode;
185
+
const byteStart = utf8Encoder.encode(plaintext).length;
186
+
plaintext += codeNode.value;
187
+
const byteEnd = utf8Encoder.encode(plaintext).length;
188
+
189
+
facets.push({
190
+
index: { byteStart, byteEnd },
191
+
features: [{ $type: 'pub.leaflet.richtext.facet#code' }]
192
+
});
193
+
} else if (node.type === 'link') {
194
+
const linkNode = node as Link;
195
+
const linkText = toString(linkNode);
196
+
const convertedUri = convertBlobUrlToAtUri(linkNode.url, authorDid);
197
+
const byteStart = utf8Encoder.encode(plaintext).length;
198
+
plaintext += linkText;
199
+
const byteEnd = utf8Encoder.encode(plaintext).length;
200
+
201
+
facets.push({
202
+
index: { byteStart, byteEnd },
203
+
features: [{ $type: 'pub.leaflet.richtext.facet#link', uri: convertedUri }]
204
+
});
205
+
} else if (node.type === 'image') {
206
+
// Images within text are converted to links
207
+
const imgNode = node as Image;
208
+
const convertedUrl = convertBlobUrlToAtUri(imgNode.url, authorDid);
209
+
const linkText = imgNode.alt || 'Image';
210
+
const byteStart = utf8Encoder.encode(plaintext).length;
211
+
plaintext += linkText;
212
+
const byteEnd = utf8Encoder.encode(plaintext).length;
64
213
65
-
linkReplacements.push({ start: linkMatch.index, end: linkMatch.index + fullMatch.length, text: linkText, uri: convertedUri });
214
+
facets.push({
215
+
index: { byteStart, byteEnd },
216
+
features: [{ $type: 'pub.leaflet.richtext.facet#link', uri: convertedUrl }]
217
+
});
218
+
} else {
219
+
plaintext += toString(node);
220
+
}
66
221
}
67
222
68
-
for (let i = linkReplacements.length - 1; i >= 0; i--) {
69
-
const rep = linkReplacements[i];
70
-
const byteStart = utf8Encoder.encode(plaintext.substring(0, rep.start)).length;
71
-
const byteEnd = byteStart + utf8Encoder.encode(rep.text).length;
223
+
return { plaintext, facets };
224
+
}
72
225
73
-
facets.push({ index: { byteStart, byteEnd }, features: [{ $type: 'pub.leaflet.richtext.facet#link', uri: rep.uri }] });
74
-
plaintext = plaintext.substring(0, rep.start) + rep.text + plaintext.substring(rep.end);
75
-
}
226
+
/**
227
+
* Process a list item recursively following pub.leaflet.blocks.unorderedList#listItem
228
+
*/
229
+
async function processListItem(
230
+
item: ListItem,
231
+
authorDid: string,
232
+
blobs: any[],
233
+
pdsUrl?: string
234
+
): Promise<any> {
235
+
// Process the first paragraph/block as the content
236
+
let content: any;
76
237
77
-
const otherFacets: any[] = [];
238
+
if (item.children.length > 0) {
239
+
const firstChild = item.children[0];
240
+
241
+
if (firstChild.type === 'paragraph') {
242
+
const paraNode = firstChild as Paragraph;
243
+
244
+
// Check if it's a standalone image
245
+
if (paraNode.children.length === 1 && paraNode.children[0].type === 'image') {
246
+
const imgNode = paraNode.children[0] as Image;
247
+
const imageBlob = convertBlobToImageRef(imgNode.url, authorDid);
248
+
249
+
if (imageBlob) {
250
+
const cid = extractCid(imgNode.url);
251
+
const blobMeta = blobs.find((b: any) => {
252
+
const blobCid = b.cid || b.ref?.$link || b.$link;
253
+
return blobCid === cid;
254
+
});
255
+
256
+
let width = blobMeta?.width || blobMeta?.aspectRatio?.width;
257
+
let height = blobMeta?.height || blobMeta?.aspectRatio?.height;
258
+
259
+
if ((!width || !height) && pdsUrl && cid) {
260
+
const imageUrl = `${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${authorDid}&cid=${cid}`;
261
+
const dimensions = await getImageDimensions(imageUrl);
262
+
263
+
if (dimensions) {
264
+
width = dimensions.width;
265
+
height = dimensions.height;
266
+
}
267
+
}
78
268
79
-
let boldRegex = /\*\*([^*]+)\*\*/g;
80
-
let boldMatch: RegExpExecArray | null;
81
-
while ((boldMatch = boldRegex.exec(plaintext)) !== null) {
82
-
const start = utf8Encoder.encode(plaintext.substring(0, boldMatch.index)).length;
83
-
const end = start + utf8Encoder.encode(boldMatch[1]).length;
84
-
otherFacets.push({ index: { byteStart: start, byteEnd: end }, features: [{ $type: 'pub.leaflet.richtext.facet#bold' }] });
85
-
}
269
+
if (!width || !height) {
270
+
width = 512;
271
+
height = 512;
272
+
}
86
273
87
-
let italicRegex = /(?<!\*)\*([^*]+)\*(?!\*)/g;
88
-
let italicMatch: RegExpExecArray | null;
89
-
while ((italicMatch = italicRegex.exec(plaintext)) !== null) {
90
-
const start = utf8Encoder.encode(plaintext.substring(0, italicMatch.index)).length;
91
-
const end = start + utf8Encoder.encode(italicMatch[1]).length;
92
-
otherFacets.push({ index: { byteStart: start, byteEnd: end }, features: [{ $type: 'pub.leaflet.richtext.facet#italic' }] });
274
+
content = {
275
+
$type: 'pub.leaflet.blocks.image',
276
+
image: imageBlob,
277
+
aspectRatio: { width, height },
278
+
...(imgNode.alt && { alt: imgNode.alt })
279
+
};
280
+
}
281
+
} else {
282
+
// Regular text content
283
+
const { plaintext, facets } = processPhrasingContent(paraNode.children, authorDid);
284
+
content = {
285
+
$type: 'pub.leaflet.blocks.text',
286
+
plaintext,
287
+
...(facets.length > 0 && { facets })
288
+
};
289
+
}
290
+
} else if (firstChild.type === 'heading') {
291
+
const headingNode = firstChild as Heading;
292
+
const { plaintext, facets } = processPhrasingContent(headingNode.children, authorDid);
293
+
content = {
294
+
$type: 'pub.leaflet.blocks.header',
295
+
level: headingNode.depth,
296
+
plaintext,
297
+
...(facets.length > 0 && { facets })
298
+
};
299
+
}
93
300
}
94
301
95
-
let codeRegex = /`([^`]+)`/g;
96
-
let codeMatch: RegExpExecArray | null;
97
-
while ((codeMatch = codeRegex.exec(plaintext)) !== null) {
98
-
const start = utf8Encoder.encode(plaintext.substring(0, codeMatch.index)).length;
99
-
const end = start + utf8Encoder.encode(codeMatch[1]).length;
100
-
otherFacets.push({ index: { byteStart: start, byteEnd: end }, features: [{ $type: 'pub.leaflet.richtext.facet#code' }] });
302
+
// If no content was created, use plain text
303
+
if (!content) {
304
+
const itemText = toString(item);
305
+
content = {
306
+
$type: 'pub.leaflet.blocks.text',
307
+
plaintext: itemText
308
+
};
101
309
}
102
310
103
-
const allFacets = [...facets, ...otherFacets];
104
-
allFacets.sort((a, b) => a.index.byteStart - b.index.byteStart);
311
+
const listItem: any = { content };
105
312
106
-
plaintext = plaintext.replace(boldRegex, '$1');
107
-
plaintext = plaintext.replace(italicRegex, '$1');
108
-
plaintext = plaintext.replace(codeRegex, '$1');
313
+
// Process nested lists (children beyond the first element)
314
+
const nestedListItems = item.children.slice(1).filter((child) => child.type === 'list');
315
+
if (nestedListItems.length > 0) {
316
+
const children = [];
317
+
for (const nestedList of nestedListItems) {
318
+
const listNode = nestedList as List;
319
+
for (const nestedItem of listNode.children) {
320
+
const processedNested = await processListItem(
321
+
nestedItem as ListItem,
322
+
authorDid,
323
+
blobs,
324
+
pdsUrl
325
+
);
326
+
children.push(processedNested);
327
+
}
328
+
}
329
+
if (children.length > 0) {
330
+
listItem.children = children;
331
+
}
332
+
}
109
333
110
-
return { plaintext, facets: allFacets.length > 0 ? allFacets : undefined };
334
+
return listItem;
111
335
}
112
336
113
-
export function parseMarkdownToBlocks(content: string, authorDid: string) {
337
+
/**
338
+
* Convert markdown AST to Leaflet blocks using remark
339
+
* Fully compliant with Leaflet lexicons
340
+
*/
341
+
export async function parseMarkdownToBlocks(
342
+
content: string,
343
+
authorDid: string,
344
+
blobs: any[] = [],
345
+
pdsUrl?: string
346
+
): Promise<any[]> {
347
+
const processor = unified().use(remarkParse).use(remarkGfm);
348
+
349
+
const tree = processor.parse(content) as Root;
114
350
const blocks: any[] = [];
115
-
const lines = content.split('\n');
116
-
let currentBlock = '';
117
-
let blockType: string = 'text';
118
351
119
-
function finishCurrent() {
120
-
if (!currentBlock.trim()) return;
121
-
const { plaintext, facets } = parseRichText(currentBlock.trim(), authorDid);
122
-
blocks.push(createBlock(blockType, plaintext, authorDid, { facets }));
123
-
currentBlock = '';
124
-
blockType = 'text';
125
-
}
352
+
for (const node of tree.children) {
353
+
if (node.type === 'heading') {
354
+
// pub.leaflet.blocks.header
355
+
const headingNode = node as Heading;
356
+
const { plaintext, facets } = processPhrasingContent(headingNode.children, authorDid);
126
357
127
-
for (let i = 0; i < lines.length; i++) {
128
-
const line = lines[i];
358
+
blocks.push({
359
+
$type: 'pub.leaflet.pages.linearDocument#block',
360
+
block: {
361
+
$type: 'pub.leaflet.blocks.header',
362
+
level: headingNode.depth,
363
+
plaintext,
364
+
...(facets.length > 0 && { facets })
365
+
}
366
+
});
367
+
} else if (node.type === 'paragraph') {
368
+
const paraNode = node as Paragraph;
129
369
130
-
// Header
131
-
if (line.startsWith('#')) {
132
-
if (currentBlock.trim()) {
133
-
const { plaintext, facets } = parseRichText(currentBlock.trim(), authorDid);
134
-
blocks.push(createBlock(blockType, plaintext, authorDid, { facets }));
135
-
currentBlock = '';
136
-
}
137
-
const levelMatch = line.match(/^#+/);
138
-
const level = levelMatch ? levelMatch[0].length : 1;
139
-
const text = line.replace(/^#+\s*/, '');
140
-
const { plaintext, facets } = parseRichText(text, authorDid);
141
-
blocks.push(createBlock('header', plaintext, authorDid, { level, facets }));
142
-
continue;
143
-
}
370
+
// Check if paragraph contains only an image - convert to image block
371
+
if (paraNode.children.length === 1 && paraNode.children[0].type === 'image') {
372
+
const imgNode = paraNode.children[0] as Image;
373
+
const imageBlob = convertBlobToImageRef(imgNode.url, authorDid);
374
+
375
+
if (imageBlob) {
376
+
// pub.leaflet.blocks.image - REQUIRES aspectRatio
377
+
const cid = extractCid(imgNode.url);
378
+
const blobMeta = blobs.find((b: any) => {
379
+
const blobCid = b.cid || b.ref?.$link || b.$link;
380
+
return blobCid === cid;
381
+
});
382
+
383
+
let width = blobMeta?.width || blobMeta?.aspectRatio?.width;
384
+
let height = blobMeta?.height || blobMeta?.aspectRatio?.height;
385
+
386
+
// Try to fetch actual dimensions if not in metadata
387
+
if ((!width || !height) && pdsUrl && cid) {
388
+
const imageUrl = `${pdsUrl}/xrpc/com.atproto.sync.getBlob?did=${authorDid}&cid=${cid}`;
389
+
const dimensions = await getImageDimensions(imageUrl);
390
+
391
+
if (dimensions) {
392
+
width = dimensions.width;
393
+
height = dimensions.height;
394
+
console.log(`Fetched image dimensions for ${cid}: ${width}x${height}`);
395
+
}
396
+
}
397
+
398
+
// Final fallback to square aspect ratio
399
+
if (!width || !height) {
400
+
width = 512;
401
+
height = 512;
402
+
console.warn(`Using default 512x512 dimensions for image: ${cid}`);
403
+
}
144
404
145
-
if (line.match(/^[-*_]{3,}$/)) {
146
-
finishCurrent();
147
-
blocks.push(createBlock('horizontalRule', '', authorDid));
148
-
continue;
149
-
}
405
+
blocks.push({
406
+
$type: 'pub.leaflet.pages.linearDocument#block',
407
+
block: {
408
+
$type: 'pub.leaflet.blocks.image',
409
+
image: imageBlob,
410
+
aspectRatio: {
411
+
width,
412
+
height
413
+
},
414
+
...(imgNode.alt && { alt: imgNode.alt })
415
+
}
416
+
});
417
+
} else {
418
+
// Fallback if URL isn't a blob
419
+
const convertedUrl = convertBlobUrlToAtUri(imgNode.url, authorDid);
420
+
const linkText = imgNode.alt || 'Image';
150
421
151
-
if (line.startsWith('```')) {
152
-
if (blockType === 'code') {
153
-
blocks.push(createBlock('code', currentBlock, authorDid, { language: 'javascript' }));
154
-
currentBlock = '';
155
-
blockType = 'text';
422
+
blocks.push({
423
+
$type: 'pub.leaflet.pages.linearDocument#block',
424
+
block: {
425
+
$type: 'pub.leaflet.blocks.text',
426
+
plaintext: linkText,
427
+
facets: [
428
+
{
429
+
index: {
430
+
byteStart: 0,
431
+
byteEnd: new TextEncoder().encode(linkText).length
432
+
},
433
+
features: [
434
+
{ $type: 'pub.leaflet.richtext.facet#link', uri: convertedUrl }
435
+
]
436
+
}
437
+
]
438
+
}
439
+
});
440
+
}
156
441
} else {
157
-
if (currentBlock.trim()) {
158
-
const { plaintext, facets } = parseRichText(currentBlock.trim(), authorDid);
159
-
blocks.push(createBlock(blockType, plaintext, authorDid, { facets }));
160
-
currentBlock = '';
442
+
// pub.leaflet.blocks.text
443
+
const { plaintext, facets } = processPhrasingContent(paraNode.children, authorDid);
444
+
445
+
if (plaintext.trim()) {
446
+
blocks.push({
447
+
$type: 'pub.leaflet.pages.linearDocument#block',
448
+
block: {
449
+
$type: 'pub.leaflet.blocks.text',
450
+
plaintext,
451
+
...(facets.length > 0 && { facets })
452
+
}
453
+
});
161
454
}
162
-
blockType = 'code';
163
-
currentBlock = '';
164
455
}
165
-
continue;
166
-
}
456
+
} else if (node.type === 'code') {
457
+
// pub.leaflet.blocks.code
458
+
const codeNode = node as Code;
167
459
168
-
if (line.startsWith('>')) {
169
-
finishCurrent();
170
-
blockType = 'blockquote';
171
-
currentBlock = line.replace(/^>\s*/, '') + '\n';
172
-
finishCurrent();
173
-
continue;
174
-
}
460
+
blocks.push({
461
+
$type: 'pub.leaflet.pages.linearDocument#block',
462
+
block: {
463
+
$type: 'pub.leaflet.blocks.code',
464
+
plaintext: codeNode.value,
465
+
...(codeNode.lang && { language: codeNode.lang })
466
+
}
467
+
});
468
+
} else if (node.type === 'blockquote') {
469
+
// pub.leaflet.blocks.blockquote
470
+
const quoteNode = node as Blockquote;
471
+
const quoteText = toString(quoteNode);
175
472
176
-
const imgMatch = line.match(/!\[([^\]]*)\]\(([^)]+)\)/);
177
-
if (imgMatch) {
178
-
if (currentBlock.trim()) {
179
-
const { plaintext, facets } = parseRichText(currentBlock.trim(), authorDid);
180
-
blocks.push(createBlock(blockType, plaintext, authorDid, { facets }));
181
-
currentBlock = '';
473
+
blocks.push({
474
+
$type: 'pub.leaflet.pages.linearDocument#block',
475
+
block: {
476
+
$type: 'pub.leaflet.blocks.blockquote',
477
+
plaintext: quoteText
478
+
}
479
+
});
480
+
} else if (node.type === 'thematicBreak') {
481
+
// pub.leaflet.blocks.horizontalRule
482
+
blocks.push({
483
+
$type: 'pub.leaflet.pages.linearDocument#block',
484
+
block: {
485
+
$type: 'pub.leaflet.blocks.horizontalRule'
486
+
}
487
+
});
488
+
} else if (node.type === 'list') {
489
+
// pub.leaflet.blocks.unorderedList
490
+
const listNode = node as List;
491
+
492
+
// Process each list item
493
+
const children = [];
494
+
for (const item of listNode.children) {
495
+
const processedItem = await processListItem(
496
+
item as ListItem,
497
+
authorDid,
498
+
blobs,
499
+
pdsUrl
500
+
);
501
+
children.push(processedItem);
182
502
}
183
-
const [, alt, src] = imgMatch;
184
-
const convertedSrc = convertBlobUrlToAtUri(src, authorDid);
185
-
const { plaintext, facets } = parseRichText(`[Image: ${alt || 'Image'}] (${convertedSrc})`, authorDid);
186
-
blocks.push(createBlock('text', plaintext, authorDid, { facets }));
187
-
blockType = 'text';
188
-
continue;
189
-
}
190
503
191
-
if (!line.trim()) {
192
-
finishCurrent();
193
-
continue;
504
+
blocks.push({
505
+
$type: 'pub.leaflet.pages.linearDocument#block',
506
+
block: {
507
+
$type: 'pub.leaflet.blocks.unorderedList',
508
+
children
509
+
}
510
+
});
194
511
}
512
+
}
195
513
196
-
if (blockType !== 'text' && blockType !== 'blockquote' && blockType !== 'code') {
197
-
finishCurrent();
198
-
blockType = 'text';
199
-
}
514
+
// If no blocks were created, return a single text block
515
+
if (blocks.length === 0) {
516
+
blocks.push({
517
+
$type: 'pub.leaflet.pages.linearDocument#block',
518
+
block: {
519
+
$type: 'pub.leaflet.blocks.text',
520
+
plaintext: content
521
+
}
522
+
});
523
+
}
524
+
525
+
return blocks;
526
+
}
527
+
528
+
/**
529
+
* Convert WhiteWind entries to Leaflet documents
530
+
* Strictly follows pub.leaflet.document lexicon (no visibility field!)
531
+
*/
532
+
export async function convertEntriesToLeaflet(
533
+
publication: any,
534
+
entries: any[],
535
+
authorDid: string,
536
+
pdsUrl?: string,
537
+
existingPublicationRkey?: string
538
+
): Promise<any[]> {
539
+
const publicationUri = existingPublicationRkey
540
+
? `at://${authorDid}/pub.leaflet.publication/${existingPublicationRkey}`
541
+
: `at://${authorDid}/pub.leaflet.publication/${publication.rkey}`;
200
542
201
-
currentBlock += line + '\n';
202
-
}
543
+
const documentRecords = await Promise.all(
544
+
entries.map(async (entry: any) => {
545
+
// Extract content from various possible fields
546
+
const content = entry.value?.content || entry.content || entry.body || '';
547
+
548
+
// Extract only Leaflet-compatible metadata
549
+
const title = entry.value?.title || entry.title || entry.name || 'Untitled Post';
550
+
const description = entry.value?.subtitle || entry.subtitle;
551
+
const publishedAt = entry.value?.createdAt || entry.createdAt;
203
552
204
-
finishCurrent();
553
+
// Preserve the original rkey for proper AT-URI references
554
+
const rkey = entry.rkey || entry.value?.rkey || generateTID();
205
555
206
-
return blocks.length > 0 ? blocks : [createBlock('text', content, authorDid)];
207
-
}
556
+
if (!content) {
557
+
throw new Error('One or more WhiteWind entries is missing a "content" field');
558
+
}
208
559
209
-
function createBlock(type: string, content: string, authorDid: string, options: any = {}) {
210
-
const block: any = { block: {} };
560
+
// Extract blobs metadata if available
561
+
const blobs = entry.value?.blobs || entry.blobs || [];
562
+
const blocks = await parseMarkdownToBlocks(content, authorDid, blobs, pdsUrl);
211
563
212
-
switch (type) {
213
-
case 'header':
214
-
block.block = {
215
-
$type: 'pub.leaflet.blocks.header',
216
-
level: options.level || 1,
217
-
plaintext: content,
218
-
...(options.facets && { facets: options.facets })
564
+
// Build Leaflet document with ONLY fields from the lexicon
565
+
const document: any = {
566
+
$type: 'pub.leaflet.document',
567
+
rkey, // Preserve for publishing
568
+
title,
569
+
author: authorDid,
570
+
publication: publicationUri,
571
+
pages: [
572
+
{
573
+
$type: 'pub.leaflet.pages.linearDocument',
574
+
blocks
575
+
}
576
+
]
219
577
};
220
-
break;
221
-
case 'blockquote':
222
-
block.block = { $type: 'pub.leaflet.blocks.blockquote', plaintext: content, ...(options.facets && { facets: options.facets }) };
223
-
break;
224
-
case 'code':
225
-
block.block = { $type: 'pub.leaflet.blocks.code', plaintext: content, language: options.language || 'javascript' };
226
-
break;
227
-
case 'horizontalRule':
228
-
block.block = { $type: 'pub.leaflet.blocks.horizontalRule' };
229
-
break;
230
-
default:
231
-
block.block = { $type: 'pub.leaflet.blocks.text', plaintext: content, ...(options.facets && { facets: options.facets }) };
232
-
}
233
-
return block;
234
-
}
235
578
236
-
export function convertEntriesToLeaflet(publication: any, entries: any[], authorDid: string) {
237
-
const documentRecords = entries.map((entry: any, idx: number) => {
238
-
const rkey = generateTID();
239
-
const content = entry.value?.content || entry.content || entry.body || '';
240
-
const title = entry.value?.title || entry.title || entry.name;
241
-
const subtitle = entry.value?.subtitle || entry.subtitle;
242
-
const createdAt = entry.value?.createdAt || entry.createdAt;
579
+
// Add optional fields only if present
580
+
if (description) {
581
+
document.description = description;
582
+
}
243
583
244
-
if (!content) {
245
-
throw new Error('One or more WhiteWind entries is missing a "content" field');
246
-
}
247
-
const blocks = parseMarkdownToBlocks(content, authorDid);
248
-
const publicationUri = `at://${authorDid}/pub.leaflet.publication/${publication.rkey}`;
584
+
if (publishedAt) {
585
+
document.publishedAt = publishedAt;
586
+
}
249
587
250
-
return {
251
-
$type: 'pub.leaflet.document',
252
-
title: title || 'Untitled Post',
253
-
...(subtitle && { description: subtitle }),
254
-
author: authorDid,
255
-
publication: publicationUri,
256
-
...(createdAt && { publishedAt: createdAt }),
257
-
pages: [{ $type: 'pub.leaflet.pages.linearDocument', blocks }]
258
-
};
259
-
});
588
+
return document;
589
+
})
590
+
);
260
591
261
592
return documentRecords;
262
593
}
+9
src/lib/styles.css
+9
src/lib/styles.css
···
239
239
}
240
240
241
241
/* Messages */
242
+
.error {
243
+
background-color: var(--danger-bg, #fee);
244
+
border: 1px solid var(--danger-border, #fcc);
245
+
color: var(--danger-text, #c00);
246
+
padding: 1rem;
247
+
border-radius: 0;
248
+
margin-bottom: 1.5rem;
249
+
}
250
+
242
251
.warning {
243
252
background-color: var(--warning-bg);
244
253
border: 1px solid var(--warning-border);
+20
src/lib/variables.css
+20
src/lib/variables.css
···
20
20
--success-bg: #0d2a1f;
21
21
--success-border: #184a33;
22
22
--success-text: #74d4a5;
23
+
--danger: #dc2626;
24
+
--danger-hover: #b91c1c;
25
+
--danger-bg: #2a0d0d;
26
+
--danger-border: #4a1818;
27
+
--danger-text: #fca5a5;
28
+
--primary: #667eea;
29
+
--primary-dark: #5568d3;
30
+
--primary-light: #1a1f3a;
31
+
--info-bg: #0d1f2a;
32
+
--info-border: #183d4a;
23
33
--input-bg: #2a2a2a;
24
34
--input-border: #4a4a4a;
25
35
--input-focus-border: #6a6a6a;
···
44
54
--success-bg: #d1fae5;
45
55
--success-border: #a7f3d0;
46
56
--success-text: #065f46;
57
+
--danger: #dc2626;
58
+
--danger-hover: #b91c1c;
59
+
--danger-bg: #fee2e2;
60
+
--danger-border: #fecaca;
61
+
--danger-text: #991b1b;
62
+
--primary: #667eea;
63
+
--primary-dark: #5568d3;
64
+
--primary-light: #e0e7ff;
65
+
--info-bg: #dbeafe;
66
+
--info-border: #bfdbfe;
47
67
--input-bg: #ffffff;
48
68
--input-border: #d0d0d0;
49
69
--input-focus-border: #9a9a9a;
+520
-64
src/routes/+page.svelte
+520
-64
src/routes/+page.svelte
···
1
1
<script lang="ts">
2
2
import '../app.css';
3
3
import '../lib/styles.css';
4
-
import { generateTID, hexToRgb, convertBlobUrlToAtUri, parseMarkdownToBlocks, convertEntriesToLeaflet } from '$lib/convert';
4
+
import {
5
+
generateTID,
6
+
hexToRgb,
7
+
convertEntriesToLeaflet
8
+
} from '$lib/convert';
9
+
import {
10
+
login,
11
+
logout,
12
+
isLoggedIn,
13
+
getCurrentUserHandle,
14
+
getCurrentUserDid,
15
+
getCurrentPdsUrl,
16
+
fetchWhiteWindEntries,
17
+
publishToAtProto
18
+
} from '$lib/auth';
19
+
import { onMount } from 'svelte';
20
+
21
+
// Auth state
22
+
let loggedIn = $state(false);
23
+
let currentHandle = $state<string | null>(null);
24
+
let currentDid = $state<string | null>(null);
25
+
let currentPds = $state<string | null>(null);
26
+
27
+
// Login form
28
+
let loginIdentifier = $state('');
29
+
let loginPassword = $state('');
30
+
let loggingIn = $state(false);
31
+
let loginError = $state('');
32
+
33
+
// Mode selection
34
+
let mode = $state<'manual' | 'auto'>('manual');
5
35
6
36
// Form state
7
-
let pubName = '';
8
-
let basePath = '';
9
-
let pubDescription = '';
10
-
let showInDiscover: string = 'true';
11
-
let showComments: string = 'true';
12
-
let primaryColor = '#667eea';
13
-
let backgroundColor = '#ffffff';
14
-
let pageBackground = '#f8fafc';
15
-
let showPageBg: string = 'false';
16
-
let whitewindJson = '';
17
-
let authorDid = '';
37
+
let useExisting = $state('false');
38
+
let existingRkey = $state('');
39
+
let pubName = $state('');
40
+
let basePath = $state('');
41
+
let pubDescription = $state('');
42
+
let showInDiscover = $state('true');
43
+
let showComments = $state('true');
44
+
let primaryColor = $state('#667eea');
45
+
let backgroundColor = $state('#ffffff');
46
+
let pageBackground = $state('#f8fafc');
47
+
let showPageBg = $state('false');
48
+
let whitewindJson = $state('');
49
+
let authorDid = $state('');
50
+
let pdsUrl = $state('https://bsky.social');
18
51
19
-
let converting = false;
20
-
let publicationOutput = '';
21
-
let documentOutput = '';
22
-
let showOutput = false;
52
+
// Auto-fetch state
53
+
let fetching = $state(false);
54
+
let fetchError = $state('');
55
+
56
+
// Conversion state
57
+
let converting = $state(false);
58
+
let publicationOutput = $state('');
59
+
let documentOutput = $state('');
60
+
let showOutput = $state(false);
61
+
62
+
// Publishing state
63
+
let publishing = $state(false);
64
+
let publishProgress = $state(0);
65
+
let publishTotal = $state(0);
66
+
let publishMessage = $state('');
67
+
let publishComplete = $state(false);
68
+
69
+
onMount(() => {
70
+
loggedIn = isLoggedIn();
71
+
if (loggedIn) {
72
+
currentHandle = getCurrentUserHandle();
73
+
currentDid = getCurrentUserDid();
74
+
currentPds = getCurrentPdsUrl();
75
+
76
+
// Pre-fill DID and PDS if logged in
77
+
if (currentDid) authorDid = currentDid;
78
+
if (currentPds) pdsUrl = currentPds;
79
+
}
80
+
});
23
81
24
82
function alertMsg(msg: string) {
25
-
// simple wrapper for alerts; could be replaced with nicer UI
26
83
alert(msg);
27
84
}
28
85
86
+
async function handleLogin() {
87
+
if (!loginIdentifier || !loginPassword) {
88
+
loginError = 'Please enter your handle and app password';
89
+
return;
90
+
}
91
+
92
+
loggingIn = true;
93
+
loginError = '';
94
+
95
+
try {
96
+
await login(loginIdentifier, loginPassword);
97
+
loggedIn = true;
98
+
currentHandle = getCurrentUserHandle();
99
+
currentDid = getCurrentUserDid();
100
+
currentPds = getCurrentPdsUrl();
101
+
102
+
// Pre-fill DID and PDS
103
+
if (currentDid) authorDid = currentDid;
104
+
if (currentPds) pdsUrl = currentPds;
105
+
106
+
loginIdentifier = '';
107
+
loginPassword = '';
108
+
} catch (e: any) {
109
+
loginError = e.message;
110
+
} finally {
111
+
loggingIn = false;
112
+
}
113
+
}
114
+
115
+
function handleLogout() {
116
+
logout();
117
+
loggedIn = false;
118
+
currentHandle = null;
119
+
currentDid = null;
120
+
currentPds = null;
121
+
mode = 'manual';
122
+
}
123
+
124
+
async function handleFetchEntries() {
125
+
fetching = true;
126
+
fetchError = '';
127
+
128
+
try {
129
+
const entries = await fetchWhiteWindEntries();
130
+
131
+
if (entries.length === 0) {
132
+
fetchError = 'No WhiteWind entries found for this account';
133
+
return;
134
+
}
135
+
136
+
// Pre-fill the JSON textarea with fetched entries
137
+
whitewindJson = JSON.stringify(entries, null, 2);
138
+
alertMsg(`Fetched ${entries.length} WhiteWind entries!`);
139
+
} catch (e: any) {
140
+
fetchError = e.message;
141
+
} finally {
142
+
fetching = false;
143
+
}
144
+
}
145
+
29
146
async function convertEntries() {
30
147
converting = true;
31
148
try {
32
-
if (!pubName || !authorDid || !whitewindJson) {
33
-
alertMsg('Please fill Publication Name, Author DID and paste WhiteWind JSON entries.');
149
+
if (!authorDid || !whitewindJson) {
150
+
alertMsg('Please fill Author DID and paste WhiteWind JSON entries.');
151
+
return;
152
+
}
153
+
154
+
if (useExisting === 'true' && !existingRkey) {
155
+
alertMsg('Please provide an existing publication rkey.');
156
+
return;
157
+
}
158
+
159
+
if (useExisting === 'false' && !pubName) {
160
+
alertMsg('Please fill Publication Name.');
34
161
return;
35
162
}
36
163
37
-
let entries: any[];
38
-
try {
39
-
const parsed = JSON.parse(whitewindJson);
40
-
if (Array.isArray(parsed)) {
41
-
entries = parsed;
42
-
} else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.records)) {
43
-
entries = parsed.records;
44
-
} else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.data)) {
45
-
entries = parsed.data;
46
-
} else {
47
-
throw new Error('JSON must be an array of entries or an object with a `records`/`data` array');
48
-
}
49
-
} catch (e: any) {
50
-
alertMsg('Invalid JSON: ' + e.message);
51
-
return;
52
-
}
164
+
let entries: any[];
165
+
try {
166
+
const parsed = JSON.parse(whitewindJson);
167
+
if (Array.isArray(parsed)) {
168
+
entries = parsed;
169
+
} else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.records)) {
170
+
entries = parsed.records;
171
+
} else if (parsed && typeof parsed === 'object' && Array.isArray(parsed.data)) {
172
+
entries = parsed.data;
173
+
} else {
174
+
throw new Error(
175
+
'JSON must be an array of entries or an object with a `records`/`data` array'
176
+
);
177
+
}
178
+
} catch (e: any) {
179
+
alertMsg('Invalid JSON: ' + e.message);
180
+
return;
181
+
}
53
182
54
183
const rkey = generateTID();
55
184
const primaryRgb = hexToRgb(primaryColor) || { r: 102, g: 126, b: 234 };
···
62
191
name: pubName,
63
192
...(basePath && { base_path: basePath }),
64
193
...(pubDescription && { description: pubDescription }),
65
-
preferences: { showInDiscover: showInDiscover === 'true', showComments: showComments === 'true' },
194
+
preferences: {
195
+
showInDiscover: showInDiscover === 'true',
196
+
showComments: showComments === 'true'
197
+
},
66
198
theme: {
67
199
primary: { $type: 'pub.leaflet.theme.color#rgb', ...primaryRgb },
68
200
backgroundColor: { $type: 'pub.leaflet.theme.color#rgb', ...backgroundRgb },
69
201
pageBackground: { $type: 'pub.leaflet.theme.color#rgb', ...pageBackgroundRgb },
70
-
showPageBackground: showPageBg === 'true'
202
+
showPageBackground: showPageBg === 'true'
71
203
}
72
204
};
73
205
74
-
const documents = convertEntriesToLeaflet(publicationRecord, entries, authorDid);
206
+
const documents = await convertEntriesToLeaflet(
207
+
publicationRecord,
208
+
entries,
209
+
authorDid,
210
+
pdsUrl,
211
+
useExisting === 'true' ? existingRkey : undefined
212
+
);
75
213
76
-
publicationOutput = JSON.stringify(publicationRecord, null, 2);
214
+
if (useExisting === 'false') {
215
+
publicationOutput = JSON.stringify(publicationRecord, null, 2);
216
+
} else {
217
+
publicationOutput = `at://${authorDid}/pub.leaflet.publication/${existingRkey}`;
218
+
}
77
219
documentOutput = JSON.stringify(documents, null, 2);
78
220
showOutput = true;
221
+
publishComplete = false;
222
+
79
223
// scroll into view
80
224
setTimeout(() => {
81
225
const el = document.getElementById('outputSection');
···
86
230
}
87
231
}
88
232
233
+
async function handlePublish() {
234
+
if (!loggedIn) {
235
+
alertMsg('Please log in to publish to AT Protocol');
236
+
return;
237
+
}
238
+
239
+
if (!showOutput) {
240
+
alertMsg('Please convert your entries first');
241
+
return;
242
+
}
243
+
244
+
publishing = true;
245
+
publishComplete = false;
246
+
publishMessage = '';
247
+
248
+
try {
249
+
const publicationRecord = useExisting === 'false' ? JSON.parse(publicationOutput) : null;
250
+
const documents = JSON.parse(documentOutput);
251
+
252
+
await publishToAtProto(
253
+
publicationRecord,
254
+
documents,
255
+
(current, total, message) => {
256
+
publishProgress = current;
257
+
publishTotal = total;
258
+
publishMessage = message;
259
+
}
260
+
);
261
+
262
+
publishComplete = true;
263
+
publishMessage = `✅ Successfully published ${documents.length} documents!`;
264
+
} catch (e: any) {
265
+
publishMessage = `❌ Publishing failed: ${e.message}`;
266
+
} finally {
267
+
publishing = false;
268
+
}
269
+
}
270
+
89
271
async function downloadZip() {
90
272
const JSZip = (await import('jszip')).default;
91
273
const { saveAs } = await import('file-saver');
92
274
const zip = new JSZip();
93
-
zip.file('00.json', publicationOutput);
275
+
276
+
if (useExisting === 'false') {
277
+
zip.file('00.json', publicationOutput);
278
+
}
279
+
94
280
const docs = JSON.parse(documentOutput || '[]');
95
-
docs.forEach((d: any, i: number) => zip.file(`${i + 1}.json`, JSON.stringify(d, null, 2)));
281
+
docs.forEach((d: any, i: number) => zip.file(`${useExisting === 'true' ? i : i + 1}.json`, JSON.stringify(d, null, 2)));
96
282
const blob = await zip.generateAsync({ type: 'blob' });
97
-
saveAs(blob, `${pubName || 'publication'}-leaflet.zip`);
283
+
saveAs(blob, `${pubName || existingRkey || 'publication'}-leaflet.zip`);
98
284
}
99
285
</script>
100
286
101
287
<header>
102
288
<h1>🍃 WhiteWind → Leaflet Converter</h1>
103
289
<p>Convert your WhiteWind blog entries to Leaflet publication format</p>
290
+
{#if loggedIn}
291
+
<div class="auth-status">
292
+
<span>✅ Logged in as <strong>{currentHandle}</strong></span>
293
+
<button class="btn-logout" onclick={handleLogout}>Logout</button>
294
+
</div>
295
+
{/if}
104
296
</header>
105
297
106
298
<div class="container">
107
299
<main class="main-content">
300
+
{#if !loggedIn}
301
+
<section class="step login-section">
302
+
<h2><span class="step-number">🔐</span>Login (Optional)</h2>
303
+
<div class="info-box">
304
+
<p><strong>New Feature:</strong> Log in to automatically fetch your WhiteWind entries and publish directly to Leaflet!</p>
305
+
<p>You can still use the manual mode without logging in.</p>
306
+
</div>
307
+
<div class="form-group">
308
+
<label for="loginIdentifier">AT Protocol Handle or DID</label>
309
+
<input
310
+
id="loginIdentifier"
311
+
bind:value={loginIdentifier}
312
+
type="text"
313
+
placeholder="alice.bsky.social or did:plc:..."
314
+
/>
315
+
</div>
316
+
<div class="form-group">
317
+
<label for="loginPassword">App Password</label>
318
+
<input
319
+
id="loginPassword"
320
+
bind:value={loginPassword}
321
+
type="password"
322
+
placeholder="Your app password"
323
+
/>
324
+
<div class="example">
325
+
Create an app password in your AT Protocol client settings. Never use your main password!
326
+
</div>
327
+
</div>
328
+
{#if loginError}
329
+
<div class="error">{loginError}</div>
330
+
{/if}
331
+
<button class="btn-primary" onclick={handleLogin} disabled={loggingIn}>
332
+
{loggingIn ? '🔄 Logging in...' : '🔐 Login'}
333
+
</button>
334
+
</section>
335
+
{/if}
336
+
337
+
{#if loggedIn}
338
+
<section class="step">
339
+
<h2><span class="step-number">⚙️</span>Mode Selection</h2>
340
+
<div class="mode-selector">
341
+
<button
342
+
class="mode-button {mode === 'auto' ? 'active' : ''}"
343
+
onclick={() => (mode = 'auto')}
344
+
>
345
+
<span class="mode-icon">🚀</span>
346
+
<span class="mode-title">Auto Mode</span>
347
+
<span class="mode-desc">Fetch & publish automatically</span>
348
+
</button>
349
+
<button
350
+
class="mode-button {mode === 'manual' ? 'active' : ''}"
351
+
onclick={() => (mode = 'manual')}
352
+
>
353
+
<span class="mode-icon">📝</span>
354
+
<span class="mode-title">Manual Mode</span>
355
+
<span class="mode-desc">Paste JSON manually</span>
356
+
</button>
357
+
</div>
358
+
</section>
359
+
{/if}
360
+
361
+
{#if loggedIn && mode === 'auto'}
362
+
<section class="step">
363
+
<h2><span class="step-number">📥</span>Fetch WhiteWind Entries</h2>
364
+
<div class="info-box">
365
+
<p>Click the button below to automatically fetch all your WhiteWind blog entries.</p>
366
+
</div>
367
+
{#if fetchError}
368
+
<div class="error">{fetchError}</div>
369
+
{/if}
370
+
<button class="btn-primary" onclick={handleFetchEntries} disabled={fetching}>
371
+
{fetching ? '🔄 Fetching entries...' : '📥 Fetch My WhiteWind Entries'}
372
+
</button>
373
+
</section>
374
+
{/if}
375
+
108
376
<section class="step">
109
377
<h2><span class="step-number">1</span>Publication Setup</h2>
378
+
<div class="form-group">
379
+
<label for="useExisting">Add to Existing Publication?</label>
380
+
<select id="useExisting" bind:value={useExisting}>
381
+
<option value="false">No - Create New Publication</option>
382
+
<option value="true">Yes - Use Existing Publication</option>
383
+
</select>
384
+
<div class="example">Choose whether to create a new publication or add posts to an existing one</div>
385
+
</div>
386
+
{#if useExisting === 'true'}
387
+
<div class="form-group">
388
+
<label for="existingRkey">Existing Publication Rkey*</label>
389
+
<input id="existingRkey" bind:value={existingRkey} type="text" placeholder="3m3x4bgbsh22k" />
390
+
<div class="example">The rkey from your existing publication URI (e.g., from at://did:plc:.../pub.leaflet.publication/<strong>3m3x4bgbsh22k</strong>)</div>
391
+
</div>
392
+
{:else}
110
393
<div class="grid">
111
394
<div class="form-group">
112
395
<label for="pubName">Publication Name*</label>
···
124
407
<div class="grid">
125
408
<div class="form-group">
126
409
<label for="showInDiscover">Show in Discover</label>
127
-
<select id="showInDiscover" bind:value={showInDiscover}>
128
-
<option value="true">Yes</option>
129
-
<option value="false">No</option>
130
-
</select>
410
+
<select id="showInDiscover" bind:value={showInDiscover}>
411
+
<option value="true">Yes</option>
412
+
<option value="false">No</option>
413
+
</select>
131
414
</div>
132
415
<div class="form-group">
133
416
<label for="showComments">Enable Comments</label>
134
-
<select id="showComments" bind:value={showComments}>
135
-
<option value="true">Yes</option>
136
-
<option value="false">No</option>
137
-
</select>
417
+
<select id="showComments" bind:value={showComments}>
418
+
<option value="true">Yes</option>
419
+
<option value="false">No</option>
420
+
</select>
138
421
</div>
139
422
</div>
423
+
{/if}
140
424
</section>
141
425
426
+
{#if useExisting === 'false'}
142
427
<section class="step">
143
428
<h2><span class="step-number">2</span>Theme Configuration</h2>
144
429
<div class="grid">
···
158
443
</div>
159
444
<div class="form-group">
160
445
<label for="showPageBg">Show Page Background</label>
161
-
<select id="showPageBg" bind:value={showPageBg}>
162
-
<option value="false">No</option>
163
-
<option value="true">Yes</option>
164
-
</select>
446
+
<select id="showPageBg" bind:value={showPageBg}>
447
+
<option value="false">No</option>
448
+
<option value="true">Yes</option>
449
+
</select>
165
450
</div>
166
451
</div>
167
452
</section>
453
+
{/if}
168
454
169
455
<section class="step">
170
-
<h2><span class="step-number">3</span>WhiteWind Blog Entries</h2>
171
-
<div class="warning"><strong>Note:</strong> Paste a JSON array of your WhiteWind blog entries below. The converter will automatically handle markdown parsing, AT-URI conversion, and schema transformation for all entries.</div>
456
+
<h2><span class="step-number">{useExisting === 'true' ? '2' : '3'}</span>WhiteWind Blog Entries</h2>
457
+
{#if mode === 'auto' && loggedIn}
458
+
<div class="info-box">
459
+
<p>Your entries have been fetched automatically. Review the JSON below or edit if needed.</p>
460
+
</div>
461
+
{:else}
462
+
<div class="warning"><strong>Note:</strong> Paste a JSON array of your WhiteWind blog entries below. The converter will automatically handle markdown parsing, AT-URI conversion, and schema transformation for all entries.</div>
463
+
{/if}
172
464
<div class="form-group">
173
465
<label for="whitewindJson">WhiteWind Entries JSON*</label>
174
466
<textarea id="whitewindJson" class="textarea-large" bind:value={whitewindJson} placeholder='Paste your WhiteWind entries JSON array here...'></textarea>
175
467
<div class="example">Example: [{"content": "# Post 1\n\nContent...", "title": "My First Post"}]</div>
176
468
</div>
177
-
<div class="form-group">
178
-
<label for="authorDid">Author DID*</label>
179
-
<input id="authorDid" bind:value={authorDid} type="text" placeholder="did:plc:..." />
180
-
<div class="example">Format: did:plc:example123... or did:web:example.com</div>
469
+
<div class="grid">
470
+
<div class="form-group">
471
+
<label for="authorDid">Author DID*</label>
472
+
<input id="authorDid" bind:value={authorDid} type="text" placeholder="did:plc:..." readonly={loggedIn} />
473
+
<div class="example">Format: did:plc:example123... or did:web:example.com</div>
474
+
</div>
475
+
<div class="form-group">
476
+
<label for="pdsUrl">PDS URL*</label>
477
+
<input id="pdsUrl" bind:value={pdsUrl} type="url" placeholder="https://bsky.social" />
478
+
<div class="example">The Personal Data Server URL (needed for fetching image dimensions)</div>
479
+
</div>
181
480
</div>
182
-
<button class="btn-primary" on:click|preventDefault={convertEntries} disabled={converting}>{converting ? '🔄 Converting...' : '🔄 Convert to Leaflet'}</button>
481
+
<button class="btn-primary" onclick={convertEntries} disabled={converting}>
482
+
{converting ? '🔄 Converting...' : '🔄 Convert to Leaflet'}
483
+
</button>
183
484
</section>
184
485
185
486
<section class="step" id="outputSection" style:display={showOutput ? 'block' : 'none'}>
186
-
<h2><span class="step-number">4</span>Conversion Results</h2>
487
+
<h2><span class="step-number">{useExisting === 'true' ? '3' : '4'}</span>Conversion Results</h2>
187
488
<div class="success" style:display={showOutput ? 'block' : 'none'}>✅ Conversion completed successfully!</div>
489
+
490
+
{#if loggedIn}
491
+
<div class="publish-section">
492
+
<h3>🚀 Publish to AT Protocol</h3>
493
+
<p>Click below to automatically publish your converted entries to Leaflet on the AT Protocol.</p>
494
+
{#if publishMessage}
495
+
<div class={publishComplete ? 'success' : 'error'}>{publishMessage}</div>
496
+
{/if}
497
+
{#if publishing}
498
+
<div class="progress-bar">
499
+
<div class="progress-fill" style="width: {(publishProgress / publishTotal) * 100}%"></div>
500
+
</div>
501
+
<p class="progress-text">{publishMessage} ({publishProgress}/{publishTotal})</p>
502
+
{/if}
503
+
<button class="btn-primary" onclick={handlePublish} disabled={publishing || publishComplete}>
504
+
{publishing ? '🚀 Publishing...' : publishComplete ? '✅ Published!' : '🚀 Publish to Leaflet'}
505
+
</button>
506
+
</div>
507
+
{/if}
508
+
188
509
<div class="download-section">
189
510
<h3>📦 Download Your Converted Files</h3>
190
-
<p>Download all files as a ZIP archive with the publication record as <code>00.json</code> and each document as <code>1.json</code>, <code>2.json</code>, etc.</p>
191
-
<button class="btn-secondary" on:click={downloadZip}>⬇️ Download ZIP Archive</button>
511
+
{#if useExisting === 'true'}
512
+
<p>Download all documents as a ZIP archive. Each document will be numbered as <code>0.json</code>, <code>1.json</code>, <code>2.json</code>, etc.</p>
513
+
{:else}
514
+
<p>Download all files as a ZIP archive with the publication record as <code>00.json</code> and each document as <code>1.json</code>, <code>2.json</code>, etc.</p>
515
+
{/if}
516
+
<button class="btn-secondary" onclick={downloadZip}>⬇️ Download ZIP Archive</button>
192
517
</div>
518
+
{#if useExisting === 'false'}
193
519
<div class="output-card">
194
520
<h3>Publication Record Preview:</h3>
195
521
<pre>{publicationOutput}</pre>
196
522
</div>
523
+
{:else}
524
+
<div class="output-card">
525
+
<h3>Target Publication URI:</h3>
526
+
<pre>{publicationOutput}</pre>
527
+
</div>
528
+
{/if}
197
529
<div class="output-card">
198
530
<h3>Document Records Preview:</h3>
199
531
<pre>{documentOutput}</pre>
···
206
538
<p>Built with 🍃 by <a href="https://ewancroft.uk" target="_blank" rel="noopener">Ewan</a> • <a href="https://github.com/ewanc26/whtwnd-to-leaflet" target="_blank" rel="noopener">Source Code</a> (GPL-3.0)</p>
207
539
<p>Not affiliated with <a href="https://whtwnd.com" target="_blank" rel="noopener">WhiteWind</a> or <a href="https://leaflet.pub" target="_blank" rel="noopener">Leaflet</a>.</p>
208
540
</footer>
541
+
542
+
<style>
543
+
.auth-status {
544
+
display: flex;
545
+
align-items: center;
546
+
justify-content: center;
547
+
gap: 1rem;
548
+
margin-top: 1rem;
549
+
padding: 0.75rem;
550
+
background: var(--success-bg);
551
+
border: 1px solid var(--success-border);
552
+
border-radius: 8px;
553
+
}
554
+
555
+
.btn-logout {
556
+
padding: 0.5rem 1rem;
557
+
background: var(--danger);
558
+
color: white;
559
+
border: none;
560
+
border-radius: 6px;
561
+
cursor: pointer;
562
+
font-size: 0.9rem;
563
+
transition: all 0.2s;
564
+
}
565
+
566
+
.btn-logout:hover {
567
+
background: var(--danger-hover);
568
+
}
569
+
570
+
.login-section {
571
+
background: var(--info-bg);
572
+
border: 2px solid var(--info-border);
573
+
}
574
+
575
+
.info-box {
576
+
background: var(--info-bg);
577
+
border: 1px solid var(--info-border);
578
+
padding: 1rem;
579
+
border-radius: 8px;
580
+
margin-bottom: 1.5rem;
581
+
}
582
+
583
+
.info-box p {
584
+
margin: 0.5rem 0;
585
+
}
586
+
587
+
.mode-selector {
588
+
display: grid;
589
+
grid-template-columns: 1fr 1fr;
590
+
gap: 1rem;
591
+
margin-top: 1rem;
592
+
}
593
+
594
+
.mode-button {
595
+
display: flex;
596
+
flex-direction: column;
597
+
align-items: center;
598
+
gap: 0.5rem;
599
+
padding: 1.5rem;
600
+
background: white;
601
+
border: 2px solid var(--border-color);
602
+
border-radius: 12px;
603
+
cursor: pointer;
604
+
transition: all 0.2s;
605
+
}
606
+
607
+
.mode-button:hover {
608
+
border-color: var(--primary);
609
+
transform: translateY(-2px);
610
+
}
611
+
612
+
.mode-button.active {
613
+
border-color: var(--primary);
614
+
background: var(--primary-light);
615
+
}
616
+
617
+
.mode-icon {
618
+
font-size: 2rem;
619
+
}
620
+
621
+
.mode-title {
622
+
font-weight: 600;
623
+
font-size: 1.1rem;
624
+
}
625
+
626
+
.mode-desc {
627
+
font-size: 0.9rem;
628
+
color: var(--text-secondary);
629
+
}
630
+
631
+
.publish-section {
632
+
background: var(--primary-light);
633
+
border: 2px solid var(--primary);
634
+
padding: 1.5rem;
635
+
border-radius: 12px;
636
+
margin-bottom: 1.5rem;
637
+
}
638
+
639
+
.publish-section h3 {
640
+
margin-top: 0;
641
+
}
642
+
643
+
.progress-bar {
644
+
width: 100%;
645
+
height: 24px;
646
+
background: white;
647
+
border-radius: 12px;
648
+
overflow: hidden;
649
+
margin: 1rem 0;
650
+
box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.1);
651
+
}
652
+
653
+
.progress-fill {
654
+
height: 100%;
655
+
background: linear-gradient(90deg, var(--primary), var(--primary-dark));
656
+
transition: width 0.3s ease;
657
+
}
658
+
659
+
.progress-text {
660
+
text-align: center;
661
+
font-weight: 500;
662
+
color: var(--text-secondary);
663
+
}
664
+
</style>