A WhiteWind blog to Leaflet publication conversion tool

feat: add auto-publishing support and AT Protocol integration

- Introduced AT Protocol authentication using @atproto/api
- Added auto-fetch and direct publishing features for WhiteWind entries
- Updated README with detailed feature list, setup instructions, and changelog
- Expanded spell-check terms in .vscode/settings.json
- Added remark/unified dependencies for Markdown parsing and GFM support
- Improved Leaflet block compliance and conversion logic documentation

ewancroft.uk 07de69c8 37ca08c1

verified
+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
··· 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
··· 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
··· 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
··· 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
··· 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
··· 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 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
··· 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: [&#123;"content": "# Post 1\n\nContent...", "title": "My First Post"&#125;]</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>