mirror of
https://github.com/dergigi/boris.git
synced 2026-02-16 20:45:01 +01:00
Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5727a38a7e | ||
|
|
9046150d1f | ||
|
|
53b54c77e7 | ||
|
|
d6756dc5a1 | ||
|
|
5ea81bda8e | ||
|
|
6ad273b5f9 | ||
|
|
32bbda0364 | ||
|
|
857337c748 | ||
|
|
c21c29d5ee | ||
|
|
0d956ed692 | ||
|
|
8fe01d5337 | ||
|
|
55c4fe9d4e | ||
|
|
8014ee4ddd | ||
|
|
365b84ba9d | ||
|
|
c419679099 | ||
|
|
e644f07828 | ||
|
|
448c4dac1c | ||
|
|
85695b5934 | ||
|
|
ef3ce445f5 | ||
|
|
436bbf2b43 | ||
|
|
0c2f528a23 | ||
|
|
d2cf27db42 | ||
|
|
53a6c86d8a | ||
|
|
0b058440bc | ||
|
|
0964156bcc | ||
|
|
86de9c9f1f | ||
|
|
974cecb85f | ||
|
|
9b245b3d29 | ||
|
|
4fe9fd5470 | ||
|
|
18af2d02ea | ||
|
|
a80352d8d3 | ||
|
|
6652694304 | ||
|
|
728c269a29 | ||
|
|
91c68a9d48 | ||
|
|
f9d381e451 | ||
|
|
81a48bd0f6 | ||
|
|
386a821c6b | ||
|
|
d10e12b8df | ||
|
|
c3eb29445e | ||
|
|
e0450385ed | ||
|
|
a2620caa29 | ||
|
|
609e15a738 | ||
|
|
fdb8511c87 | ||
|
|
acce3ad4e2 | ||
|
|
bdecb1409e | ||
|
|
2ca350ee5f | ||
|
|
20f37b94e1 | ||
|
|
21890f002d | ||
|
|
7a5dd2f444 | ||
|
|
5495890204 | ||
|
|
6d585dcef6 | ||
|
|
0bae6674ce | ||
|
|
096509baf6 | ||
|
|
4c2626f3c4 | ||
|
|
70fa3bb6a8 | ||
|
|
719ddf3f0b | ||
|
|
80408148fb | ||
|
|
4163ffa4ba | ||
|
|
cf230623a4 | ||
|
|
9cd4b72f98 | ||
|
|
eb57330915 | ||
|
|
96d93d0e17 | ||
|
|
1d10c10a44 | ||
|
|
dab35820b7 | ||
|
|
9d5e8c194b | ||
|
|
de32807995 | ||
|
|
b671e0e259 | ||
|
|
e5d6fe99f3 | ||
|
|
9400faa00f | ||
|
|
5173a37b69 | ||
|
|
50b32a66de | ||
|
|
1b41b6e823 | ||
|
|
2696bdb57a | ||
|
|
e0b042b6c0 | ||
|
|
1226124566 | ||
|
|
8967963535 |
16
.cursor/rules/bookmarks.mdc
Normal file
16
.cursor/rules/bookmarks.mdc
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
description: documentation that's useful when dealing with bookmark events (kind:10003 or kind:30003) or anything related to NIP-51
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
Read the nostrbook to understand how bookmarks work:
|
||||
- https://nostrbook.dev/kinds/10003
|
||||
- https://nostrbook.dev/kinds/30003
|
||||
|
||||
They are defined in NIP-51:
|
||||
- https://github.com/nostr-protocol/nips/blob/master/51.md
|
||||
|
||||
Also refer to the applesauce bookmark helpers:
|
||||
- https://github.com/hzrd149/applesauce/blob/17c9dbb0f2c263e2ebd01729ea2fa138eca12bd1/packages/core/src/helpers/bookmarks.ts
|
||||
|
||||
Make sure to always use applesauce, and use it properly.
|
||||
6
.cursor/rules/fontawesome.mdc
Normal file
6
.cursor/rules/fontawesome.mdc
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
description: when creating or modifying UI elements, especially related to icons and buttons
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
We use FontAwesome. If you can use a fa-icon (instead of text) use a fa-icon.
|
||||
109
README.md
109
README.md
@@ -4,9 +4,16 @@ A minimal nostr client for bookmark management, built with [applesauce](https://
|
||||
|
||||
## Features
|
||||
|
||||
- **Nostr Authentication**: Connect using your nostr account
|
||||
- **Nostr Authentication**: Connect using your nostr account via browser extension
|
||||
- **Bookmark Display**: View your nostr bookmarks as per [NIP-51](https://github.com/nostr-protocol/nips/blob/master/51.md)
|
||||
- **Minimal UI**: Clean, simple interface focused on bookmark management
|
||||
- **Content Classification**: Automatically detect and classify URLs (articles, videos, YouTube, images)
|
||||
- **Reader Mode**: View article content inline with readable formatting
|
||||
- **Collapsible Sidebar**: Expand/collapse bookmark list for focused reading
|
||||
- **Profile Integration**: Display user profile images using applesauce ProfileModel
|
||||
- **Relative Timestamps**: Human-friendly time display (e.g., "2 hours ago")
|
||||
- **Event Links**: Quick access to view bookmarks on search.dergigi.com
|
||||
- **Private Bookmarks**: Support for Amethyst-style hidden/encrypted bookmarks
|
||||
- **Minimal UI**: Clean, modern interface focused on bookmark management
|
||||
|
||||
## Getting Started
|
||||
|
||||
@@ -63,13 +70,62 @@ yarn dev
|
||||
```
|
||||
src/
|
||||
├── components/
|
||||
│ ├── Login.tsx # Authentication component
|
||||
│ └── Bookmarks.tsx # Bookmark display component
|
||||
├── App.tsx # Main application component
|
||||
├── main.tsx # Application entry point
|
||||
└── index.css # Global styles
|
||||
│ ├── Login.tsx # Authentication component
|
||||
│ ├── Bookmarks.tsx # Main bookmarks view with layout
|
||||
│ ├── BookmarkList.tsx # Bookmark list sidebar
|
||||
│ ├── BookmarkItem.tsx # Individual bookmark card
|
||||
│ ├── SidebarHeader.tsx # Header bar with collapse, profile, logout
|
||||
│ ├── ContentPanel.tsx # Content viewer panel
|
||||
│ ├── IconButton.tsx # Reusable icon button component
|
||||
│ ├── ContentWithResolvedProfiles.tsx # Profile mention resolver
|
||||
│ ├── ResolvedMention.tsx # Nostr mention component
|
||||
│ └── kindIcon.ts # Kind-specific icon mapping
|
||||
├── services/
|
||||
│ ├── bookmarkService.ts # Main bookmark fetching orchestration
|
||||
│ ├── bookmarkProcessing.ts # Decryption and processing pipeline
|
||||
│ ├── bookmarkHelpers.ts # Shared types, guards, and utilities
|
||||
│ ├── bookmarkEvents.ts # Event type handling and deduplication
|
||||
│ └── readerService.ts # Content extraction via reader API
|
||||
├── types/
|
||||
│ ├── bookmarks.ts # Bookmark type definitions
|
||||
│ ├── nostr.d.ts # Nostr type augmentations
|
||||
│ └── relative-time.d.ts # relative-time package types
|
||||
├── utils/
|
||||
│ ├── bookmarkUtils.tsx # Bookmark rendering utilities
|
||||
│ └── helpers.ts # General helper functions
|
||||
├── App.tsx # Main application component
|
||||
├── main.tsx # Application entry point
|
||||
└── index.css # Global styles
|
||||
```
|
||||
|
||||
### Private (hidden) bookmarks (Amethyst-style)
|
||||
|
||||
We support Amethyst-style private (hidden) bookmark lists alongside public ones (NIP‑51):
|
||||
|
||||
- **Detection and unlock**
|
||||
- Use `Helpers.hasHiddenTags(evt)` and `Helpers.isHiddenTagsLocked(evt)` to detect hidden tags.
|
||||
- First try `Helpers.unlockHiddenTags(evt, signer)`; if that fails, try with `'nip44'`.
|
||||
- For events with encrypted `content` that aren’t recognized as supporting hidden tags (e.g. kind 30001), manually decrypt:
|
||||
- Prefer `signer.nip44.decrypt(evt.pubkey, evt.content)`, fallback to `signer.nip04.decrypt(evt.pubkey, evt.content)`.
|
||||
|
||||
- **Parsing and rendering**
|
||||
- Decrypted `content` is JSON `string[][]` (tags). Convert with `Helpers.parseBookmarkTags(hiddenTags)`.
|
||||
- Map to `IndividualBookmark[]` via our `processApplesauceBookmarks(..., isPrivate=true)` and append to the private list so they render immediately alongside public items.
|
||||
|
||||
- **Caching for downstream helpers**
|
||||
- Cache manual results on the event with `BookmarkHiddenSymbol` and also store the decrypted blob under `EncryptedContentSymbol` to aid debugging and hydration.
|
||||
|
||||
- **Structure**
|
||||
- `src/services/bookmarkService.ts`: orchestrates fetching, hydration, and assembling the final bookmark payload.
|
||||
- `src/services/bookmarkProcessing.ts`: decryption/collection pipeline (unlock, manual decrypt, parse, merge).
|
||||
- `src/services/bookmarkHelpers.ts`: shared types, guards, mapping, hydration, and symbols.
|
||||
- `src/services/bookmarkEvents.ts`: event type and de‑duplication for NIP‑51 lists/sets.
|
||||
|
||||
- **Notes**
|
||||
- We avoid `any` via narrow type guards for `nip44`/`nip04` decrypt functions.
|
||||
- Files are kept small and DRY per project rules.
|
||||
- Built on applesauce helpers (`Helpers.getPublicBookmarks`, `Helpers.getHiddenBookmarks`, etc.). See applesauce docs: https://hzrd149.github.io/applesauce/typedoc/modules.html
|
||||
|
||||
### Building for Production
|
||||
|
||||
```bash
|
||||
@@ -80,15 +136,42 @@ pnpm build
|
||||
yarn build
|
||||
```
|
||||
|
||||
## TODO
|
||||
|
||||
### High Priority
|
||||
- [ ] **Mobile Responsive Design**: Optimize sidebar and content panel for mobile devices
|
||||
- [ ] **Keyboard Shortcuts**: Add keyboard navigation (collapse sidebar, navigate bookmarks)
|
||||
- [ ] **Search & Filter**: Add ability to search bookmarks by title, URL, or content
|
||||
- [ ] **Error Handling**: Improve error states and retry logic for failed fetches
|
||||
- [ ] **Loading States**: Better skeleton screens and loading indicators
|
||||
|
||||
### Medium Priority
|
||||
- [ ] **Bookmark Creation**: Add ability to create new bookmarks
|
||||
- [ ] **Bookmark Editing**: Edit existing bookmark metadata and tags
|
||||
- [ ] **Bookmark Deletion**: Remove bookmarks from lists
|
||||
- [ ] **Sorting Options**: Sort by date, title, kind, or custom order
|
||||
- [ ] **Bulk Actions**: Select and perform actions on multiple bookmarks
|
||||
- [ ] **Video Embeds**: Inline YouTube and video playback for video bookmarks
|
||||
|
||||
### Nice to Have
|
||||
- [ ] **Dark/Light Mode Toggle**: User preference for color scheme
|
||||
- [ ] **Export Functionality**: Export bookmarks as JSON, CSV, or HTML
|
||||
- [ ] **Import Bookmarks**: Import from browser bookmarks or other formats
|
||||
- [ ] **Tags & Categories**: Better organization with custom tags
|
||||
- [ ] **Bookmark Collections**: Create and manage custom bookmark collections
|
||||
- [ ] **Offline Support**: Cache bookmarks for offline viewing
|
||||
- [ ] **Share Bookmarks**: Generate shareable links to bookmark lists
|
||||
- [ ] **Performance Optimization**: Virtual scrolling for large bookmark lists
|
||||
- [ ] **Browser Extension**: Quick bookmark saving from any page
|
||||
|
||||
## Contributing
|
||||
|
||||
This is a minimal MVP. Future enhancements could include:
|
||||
Contributions are welcome! Please feel free to submit a Pull Request. Make sure to:
|
||||
|
||||
- Bookmark creation and editing
|
||||
- Bookmark organization and tagging
|
||||
- Search functionality
|
||||
- Export capabilities
|
||||
- Mobile-responsive design improvements
|
||||
- Follow the existing code style
|
||||
- Keep files under 210 lines
|
||||
- Use conventional commits
|
||||
- Run linter and type checks before submitting
|
||||
|
||||
## License
|
||||
|
||||
|
||||
19
kind-icons.txt
Normal file
19
kind-icons.txt
Normal file
@@ -0,0 +1,19 @@
|
||||
kind:0 = fa-circle-user
|
||||
kind:1 = fa-feather
|
||||
kind:6 = fa-retweet
|
||||
kind:7 = fa-heart
|
||||
kind:20 = fa-image
|
||||
kind:21 = fa-video
|
||||
kind:22 = fa-video
|
||||
kind:1063 = fa-file
|
||||
kind:1337 = fa-laptop-code
|
||||
kind:1617 = fa-code-pull-request
|
||||
kind:1621 = fa-bug
|
||||
kind:1984 = fa-exclamation-triangle
|
||||
kind:9735 = fa-bolt
|
||||
kind:9321 = fa-cloud-bolt
|
||||
kind:9802 = fa-highlighter
|
||||
kind:30023 = fa-newspaper
|
||||
kind:10000 = fa-eye-slash
|
||||
kind:10001 = fa-thumbtack
|
||||
kind:10003 = fa-bookmark
|
||||
661
node_modules/.package-lock.json
generated
vendored
661
node_modules/.package-lock.json
generated
vendored
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "markr",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
@@ -1541,9 +1541,17 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree-jsx": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
|
||||
"integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
@@ -1579,14 +1587,12 @@
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz",
|
||||
"integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -1818,7 +1824,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
@@ -2476,6 +2481,16 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -2503,6 +2518,36 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-html4": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
||||
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-legacy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
||||
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-reference-invalid": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
|
||||
"integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2523,6 +2568,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -2556,9 +2611,18 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -2901,6 +2965,16 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-is-identifier-name": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
|
||||
"integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
@@ -3193,6 +3267,56 @@
|
||||
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||
"integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"style-to-js": "^1.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-whitespace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||||
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
"integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -3269,6 +3393,46 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
|
||||
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
||||
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphanumerical": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
|
||||
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-alphabetical": "^2.0.0",
|
||||
"is-decimal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-decimal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
|
||||
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -3292,6 +3456,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-hexadecimal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
|
||||
"integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -3497,6 +3671,16 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-find-and-replace": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
||||
@@ -3549,6 +3733,167 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
|
||||
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-gfm-autolink-literal": "^2.0.0",
|
||||
"mdast-util-gfm-footnote": "^2.0.0",
|
||||
"mdast-util-gfm-strikethrough": "^2.0.0",
|
||||
"mdast-util-gfm-table": "^2.0.0",
|
||||
"mdast-util-gfm-task-list-item": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-autolink-literal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
|
||||
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-find-and-replace": "^3.0.0",
|
||||
"micromark-util-character": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-strikethrough": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
|
||||
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-table": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
|
||||
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"markdown-table": "^3.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-task-list-item": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
|
||||
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-expression": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
||||
"integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-jsx": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
|
||||
"integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"parse-entities": "^4.0.0",
|
||||
"stringify-entities": "^4.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdxjs-esm": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
|
||||
"integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-phrasing": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||
@@ -3563,6 +3908,27 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@ungap/structured-clone": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"trim-lines": "^3.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-markdown": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
|
||||
@@ -3676,6 +4042,127 @@
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
|
||||
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-extension-gfm-autolink-literal": "^2.0.0",
|
||||
"micromark-extension-gfm-footnote": "^2.0.0",
|
||||
"micromark-extension-gfm-strikethrough": "^2.0.0",
|
||||
"micromark-extension-gfm-table": "^2.0.0",
|
||||
"micromark-extension-gfm-tagfilter": "^2.0.0",
|
||||
"micromark-extension-gfm-task-list-item": "^2.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-autolink-literal": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
|
||||
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-strikethrough": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
|
||||
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-table": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
|
||||
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-tagfilter": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
|
||||
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-task-list-item": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
|
||||
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-destination": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
||||
@@ -4304,6 +4791,31 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-entities": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
||||
"integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2.0.0",
|
||||
"character-entities-legacy": "^3.0.0",
|
||||
"character-reference-invalid": "^2.0.0",
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"is-alphanumerical": "^2.0.0",
|
||||
"is-decimal": "^2.0.0",
|
||||
"is-hexadecimal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-entities/node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -4422,6 +4934,16 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -4478,6 +5000,33 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.0.0",
|
||||
"html-url-attributes": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
@@ -4504,6 +5053,24 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-gfm": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-gfm": "^3.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-parse": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||
@@ -4520,6 +5087,23 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-rehype": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
|
||||
"integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-stringify": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
||||
@@ -4713,6 +5297,30 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/stringify-entities": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities-html4": "^2.0.0",
|
||||
"character-entities-legacy": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@@ -4739,6 +5347,24 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-js": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz",
|
||||
"integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"style-to-object": "1.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-object": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz",
|
||||
"integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inline-style-parser": "0.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -4772,6 +5398,16 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/trough": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
|
||||
@@ -4873,6 +5509,19 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
|
||||
668
package-lock.json
generated
668
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "markr",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "markr",
|
||||
"version": "0.0.2",
|
||||
"version": "0.1.1",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^7.1.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^7.1.0",
|
||||
@@ -17,9 +17,12 @@
|
||||
"applesauce-loaders": "^3.1.0",
|
||||
"applesauce-react": "^3.1.0",
|
||||
"applesauce-relay": "^3.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"nostr-tools": "^2.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
@@ -1528,9 +1531,17 @@
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/estree-jsx": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz",
|
||||
"integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
@@ -1566,14 +1577,12 @@
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.25.tgz",
|
||||
"integrity": "sha512-oSVZmGtDPmRZtVDqvdKUi/qgCsWp5IDY29wp8na8Bj4B3cc99hfNzvNhlMkVVxctkAOGUA3Km7MMpBHAnWfcIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -1805,7 +1814,6 @@
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@vitejs/plugin-react": {
|
||||
@@ -2463,6 +2471,16 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/ccount": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
|
||||
"integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
@@ -2490,6 +2508,36 @@
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-html4": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz",
|
||||
"integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-entities-legacy": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz",
|
||||
"integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/character-reference-invalid": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz",
|
||||
"integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
@@ -2510,6 +2558,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/comma-separated-tokens": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz",
|
||||
"integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -2543,9 +2601,18 @@
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
@@ -2888,6 +2955,16 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estree-util-is-identifier-name": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz",
|
||||
"integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
@@ -3180,6 +3257,56 @@
|
||||
"integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/hast-util-to-jsx-runtime": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz",
|
||||
"integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"comma-separated-tokens": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"estree-util-is-identifier-name": "^3.0.0",
|
||||
"hast-util-whitespace": "^3.0.0",
|
||||
"mdast-util-mdx-expression": "^2.0.0",
|
||||
"mdast-util-mdx-jsx": "^3.0.0",
|
||||
"mdast-util-mdxjs-esm": "^2.0.0",
|
||||
"property-information": "^7.0.0",
|
||||
"space-separated-tokens": "^2.0.0",
|
||||
"style-to-js": "^1.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/hast-util-whitespace": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz",
|
||||
"integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/html-url-attributes": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
"integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/ieee754": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||
@@ -3256,6 +3383,46 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/inline-style-parser": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz",
|
||||
"integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/is-alphabetical": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
|
||||
"integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-alphanumerical": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz",
|
||||
"integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-alphabetical": "^2.0.0",
|
||||
"is-decimal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-decimal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz",
|
||||
"integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
@@ -3279,6 +3446,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-hexadecimal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz",
|
||||
"integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
@@ -3484,6 +3661,16 @@
|
||||
"yallist": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/markdown-table": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz",
|
||||
"integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-find-and-replace": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz",
|
||||
@@ -3536,6 +3723,167 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz",
|
||||
"integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-gfm-autolink-literal": "^2.0.0",
|
||||
"mdast-util-gfm-footnote": "^2.0.0",
|
||||
"mdast-util-gfm-strikethrough": "^2.0.0",
|
||||
"mdast-util-gfm-table": "^2.0.0",
|
||||
"mdast-util-gfm-task-list-item": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-autolink-literal": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz",
|
||||
"integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-find-and-replace": "^3.0.0",
|
||||
"micromark-util-character": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-strikethrough": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz",
|
||||
"integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-table": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz",
|
||||
"integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"markdown-table": "^3.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-gfm-task-list-item": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz",
|
||||
"integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-expression": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz",
|
||||
"integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdx-jsx": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz",
|
||||
"integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@types/unist": "^3.0.0",
|
||||
"ccount": "^2.0.0",
|
||||
"devlop": "^1.1.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0",
|
||||
"parse-entities": "^4.0.0",
|
||||
"stringify-entities": "^4.0.0",
|
||||
"unist-util-stringify-position": "^4.0.0",
|
||||
"vfile-message": "^4.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-mdxjs-esm": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz",
|
||||
"integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree-jsx": "^1.0.0",
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"mdast-util-from-markdown": "^2.0.0",
|
||||
"mdast-util-to-markdown": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-phrasing": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz",
|
||||
@@ -3550,6 +3898,27 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-hast": {
|
||||
"version": "13.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz",
|
||||
"integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"@ungap/structured-clone": "^1.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"trim-lines": "^3.0.0",
|
||||
"unist-util-position": "^5.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/mdast-util-to-markdown": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz",
|
||||
@@ -3663,6 +4032,127 @@
|
||||
"micromark-util-types": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz",
|
||||
"integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-extension-gfm-autolink-literal": "^2.0.0",
|
||||
"micromark-extension-gfm-footnote": "^2.0.0",
|
||||
"micromark-extension-gfm-strikethrough": "^2.0.0",
|
||||
"micromark-extension-gfm-table": "^2.0.0",
|
||||
"micromark-extension-gfm-tagfilter": "^2.0.0",
|
||||
"micromark-extension-gfm-task-list-item": "^2.0.0",
|
||||
"micromark-util-combine-extensions": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-autolink-literal": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz",
|
||||
"integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-footnote": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz",
|
||||
"integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-core-commonmark": "^2.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-normalize-identifier": "^2.0.0",
|
||||
"micromark-util-sanitize-uri": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-strikethrough": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz",
|
||||
"integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-util-chunked": "^2.0.0",
|
||||
"micromark-util-classify-character": "^2.0.0",
|
||||
"micromark-util-resolve-all": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-table": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz",
|
||||
"integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-tagfilter": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz",
|
||||
"integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-extension-gfm-task-list-item": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz",
|
||||
"integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"devlop": "^1.0.0",
|
||||
"micromark-factory-space": "^2.0.0",
|
||||
"micromark-util-character": "^2.0.0",
|
||||
"micromark-util-symbol": "^2.0.0",
|
||||
"micromark-util-types": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/micromark-factory-destination": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz",
|
||||
@@ -4291,6 +4781,31 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-entities": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz",
|
||||
"integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^2.0.0",
|
||||
"character-entities-legacy": "^3.0.0",
|
||||
"character-reference-invalid": "^2.0.0",
|
||||
"decode-named-character-reference": "^1.0.0",
|
||||
"is-alphanumerical": "^2.0.0",
|
||||
"is-decimal": "^2.0.0",
|
||||
"is-hexadecimal": "^2.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/parse-entities/node_modules/@types/unist": {
|
||||
"version": "2.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz",
|
||||
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||
@@ -4409,6 +4924,16 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/property-information": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz",
|
||||
"integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
@@ -4465,6 +4990,33 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-markdown": {
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz",
|
||||
"integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"devlop": "^1.0.0",
|
||||
"hast-util-to-jsx-runtime": "^2.0.0",
|
||||
"html-url-attributes": "^3.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-rehype": "^11.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"unist-util-visit": "^5.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": ">=18",
|
||||
"react": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||
@@ -4491,6 +5043,24 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-gfm": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz",
|
||||
"integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-gfm": "^3.0.0",
|
||||
"micromark-extension-gfm": "^3.0.0",
|
||||
"remark-parse": "^11.0.0",
|
||||
"remark-stringify": "^11.0.0",
|
||||
"unified": "^11.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-parse": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
|
||||
@@ -4507,6 +5077,23 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-rehype": {
|
||||
"version": "11.1.2",
|
||||
"resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz",
|
||||
"integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/hast": "^3.0.0",
|
||||
"@types/mdast": "^4.0.0",
|
||||
"mdast-util-to-hast": "^13.0.0",
|
||||
"unified": "^11.0.0",
|
||||
"vfile": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/remark-stringify": {
|
||||
"version": "11.0.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz",
|
||||
@@ -4700,6 +5287,30 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/space-separated-tokens": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz",
|
||||
"integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/stringify-entities": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz",
|
||||
"integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"character-entities-html4": "^2.0.0",
|
||||
"character-entities-legacy": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
@@ -4726,6 +5337,24 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-js": {
|
||||
"version": "1.1.17",
|
||||
"resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz",
|
||||
"integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"style-to-object": "1.0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/style-to-object": {
|
||||
"version": "1.0.9",
|
||||
"resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz",
|
||||
"integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"inline-style-parser": "0.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -4759,6 +5388,16 @@
|
||||
"node": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/trim-lines": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
|
||||
"integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/wooorm"
|
||||
}
|
||||
},
|
||||
"node_modules/trough": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz",
|
||||
@@ -4860,6 +5499,19 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-position": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz",
|
||||
"integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/unist": "^3.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/unist-util-stringify-position": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "markr",
|
||||
"version": "0.0.3",
|
||||
"version": "0.1.2",
|
||||
"description": "A minimal nostr client for bookmark management",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
@@ -19,9 +19,12 @@
|
||||
"applesauce-loaders": "^3.1.0",
|
||||
"applesauce-react": "^3.1.0",
|
||||
"applesauce-relay": "^3.1.0",
|
||||
"date-fns": "^4.1.0",
|
||||
"nostr-tools": "^2.4.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"react-dom": "^18.2.0",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.43",
|
||||
|
||||
20
src/App.tsx
20
src/App.tsx
@@ -3,6 +3,7 @@ import { EventStoreProvider, AccountsProvider } from 'applesauce-react'
|
||||
import { EventStore } from 'applesauce-core'
|
||||
import { AccountManager } from 'applesauce-accounts'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { createAddressLoader } from 'applesauce-loaders/loaders'
|
||||
import Login from './components/Login'
|
||||
import Bookmarks from './components/Bookmarks'
|
||||
|
||||
@@ -37,6 +38,18 @@ function App() {
|
||||
console.log('Created relay group with', relayUrls.length, 'relays')
|
||||
console.log('Relay URLs:', relayUrls)
|
||||
|
||||
// Attach address/replaceable loaders so ProfileModel can fetch profiles
|
||||
const addressLoader = createAddressLoader(pool, {
|
||||
eventStore: store,
|
||||
lookupRelays: [
|
||||
'wss://purplepag.es',
|
||||
'wss://relay.primal.net',
|
||||
'wss://relay.nostr.band'
|
||||
]
|
||||
})
|
||||
store.addressableLoader = addressLoader
|
||||
store.replaceableLoader = addressLoader
|
||||
|
||||
setEventStore(store)
|
||||
setAccountManager(accounts)
|
||||
setRelayPool(pool)
|
||||
@@ -50,17 +63,12 @@ function App() {
|
||||
<EventStoreProvider eventStore={eventStore}>
|
||||
<AccountsProvider manager={accountManager}>
|
||||
<div className="app">
|
||||
<header>
|
||||
<h1>Markr</h1>
|
||||
<p>A minimal nostr bookmark client</p>
|
||||
</header>
|
||||
|
||||
{!isAuthenticated ? (
|
||||
<Login onLogin={() => setIsAuthenticated(true)} />
|
||||
) : (
|
||||
<Bookmarks
|
||||
relayPool={relayPool}
|
||||
onLogout={() => setIsAuthenticated(false)}
|
||||
onLogout={() => setIsAuthenticated(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,60 +1,196 @@
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faLock, faGlobe, faCopy } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faBookmark, faUserLock } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faChevronDown, faChevronUp, faBookOpen, faPlay, faEye } from '@fortawesome/free-solid-svg-icons'
|
||||
import IconButton from './IconButton'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import { npubEncode, neventEncode } from 'nostr-tools/nip19'
|
||||
import { IndividualBookmark } from '../types/bookmarks'
|
||||
import { formatDate, renderParsedContent } from '../utils/bookmarkUtils'
|
||||
import { getKindIcon } from './kindIcon'
|
||||
import ContentWithResolvedProfiles from './ContentWithResolvedProfiles'
|
||||
import { extractUrlsFromContent } from '../services/bookmarkHelpers'
|
||||
import { classifyUrl } from '../utils/helpers'
|
||||
|
||||
interface BookmarkItemProps {
|
||||
bookmark: IndividualBookmark
|
||||
index: number
|
||||
onSelectUrl?: (url: string) => void
|
||||
}
|
||||
|
||||
export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index }) => {
|
||||
const copy = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text)
|
||||
} catch (error) {
|
||||
console.warn('Failed to copy to clipboard:', error)
|
||||
export const BookmarkItem: React.FC<BookmarkItemProps> = ({ bookmark, index, onSelectUrl }) => {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const [urlsExpanded, setUrlsExpanded] = useState(false)
|
||||
// removed copy-to-clipboard buttons
|
||||
|
||||
const short = (v: string) => `${v.slice(0, 8)}...${v.slice(-8)}`
|
||||
|
||||
// Extract URLs from bookmark content
|
||||
const extractedUrls = extractUrlsFromContent(bookmark.content)
|
||||
const hasUrls = extractedUrls.length > 0
|
||||
const contentLength = (bookmark.content || '').length
|
||||
const shouldTruncate = !expanded && contentLength > 210
|
||||
|
||||
// Resolve author profile using applesauce
|
||||
const authorProfile = useEventModel(Models.ProfileModel, [bookmark.pubkey])
|
||||
const authorNpub = npubEncode(bookmark.pubkey)
|
||||
const isHexId = /^[0-9a-f]{64}$/i.test(bookmark.id)
|
||||
const eventNevent = isHexId ? neventEncode({ id: bookmark.id }) : undefined
|
||||
|
||||
// Get display name for author
|
||||
const getAuthorDisplayName = () => {
|
||||
if (authorProfile?.name) return authorProfile.name
|
||||
if (authorProfile?.display_name) return authorProfile.display_name
|
||||
if (authorProfile?.nip05) return authorProfile.nip05
|
||||
return short(bookmark.pubkey) // fallback to short pubkey
|
||||
}
|
||||
|
||||
// use helper from kindIcon.ts
|
||||
|
||||
const getIconForUrlType = (url: string) => {
|
||||
const classification = classifyUrl(url)
|
||||
switch (classification.type) {
|
||||
case 'youtube':
|
||||
case 'video':
|
||||
return faPlay
|
||||
case 'image':
|
||||
return faEye
|
||||
default:
|
||||
return faBookOpen
|
||||
}
|
||||
}
|
||||
|
||||
const short = (v: string) => `${v.slice(0, 8)}...${v.slice(-8)}`
|
||||
const handleReadNow = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!hasUrls) return
|
||||
const firstUrl = extractedUrls[0]
|
||||
if (onSelectUrl) {
|
||||
event.preventDefault()
|
||||
onSelectUrl(firstUrl)
|
||||
} else {
|
||||
window.open(firstUrl, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// Get classification for the first URL (for the main button)
|
||||
const firstUrlClassification = hasUrls ? classifyUrl(extractedUrls[0]) : null
|
||||
|
||||
return (
|
||||
<div key={`${bookmark.id}-${index}`} className={`individual-bookmark ${bookmark.isPrivate ? 'private-bookmark' : ''}`}>
|
||||
<div className="bookmark-header">
|
||||
<span className="bookmark-type">
|
||||
<FontAwesomeIcon icon={bookmark.isPrivate ? faLock : faGlobe} className={`bookmark-visibility ${bookmark.isPrivate ? 'private' : 'public'}`} />
|
||||
<span className="bookmark-type-label">{bookmark.type}</span>
|
||||
</span>
|
||||
<span className="bookmark-id">
|
||||
{short(bookmark.id)}
|
||||
<button className="copy-btn" onClick={() => copy(bookmark.id)} title="Copy event id">
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</button>
|
||||
{bookmark.isPrivate ? (
|
||||
<>
|
||||
<FontAwesomeIcon icon={faBookmark} className="bookmark-visibility public" />
|
||||
<FontAwesomeIcon icon={faUserLock} className="bookmark-visibility private" />
|
||||
</>
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faBookmark} className="bookmark-visibility public" />
|
||||
)}
|
||||
</span>
|
||||
|
||||
<span className="bookmark-date">{formatDate(bookmark.created_at)}</span>
|
||||
</div>
|
||||
|
||||
{bookmark.parsedContent ? (
|
||||
<div className="bookmark-content">
|
||||
{renderParsedContent(bookmark.parsedContent)}
|
||||
</div>
|
||||
) : bookmark.content && (
|
||||
<div className="bookmark-content">
|
||||
<p>{bookmark.content}</p>
|
||||
{extractedUrls.length > 0 && (
|
||||
<div className="bookmark-urls">
|
||||
<h4>URLs:</h4>
|
||||
{(urlsExpanded ? extractedUrls : extractedUrls.slice(0, 3)).map((url, urlIndex) => {
|
||||
const classification = classifyUrl(url)
|
||||
return (
|
||||
<div key={urlIndex} className="url-row">
|
||||
<button
|
||||
className="bookmark-url"
|
||||
onClick={() => onSelectUrl?.(url)}
|
||||
title="Open in reader"
|
||||
>
|
||||
{url}
|
||||
</button>
|
||||
<IconButton
|
||||
icon={getIconForUrlType(url)}
|
||||
ariaLabel={classification.buttonText}
|
||||
title={classification.buttonText}
|
||||
variant="success"
|
||||
size={36}
|
||||
onClick={(e) => { e.preventDefault(); onSelectUrl?.(url) }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{extractedUrls.length > 3 && (
|
||||
<button
|
||||
className="expand-toggle"
|
||||
onClick={() => setUrlsExpanded(v => !v)}
|
||||
aria-label={urlsExpanded ? 'Collapse URLs' : 'Expand URLs'}
|
||||
title={urlsExpanded ? 'Collapse URLs' : 'Expand URLs'}
|
||||
>
|
||||
<FontAwesomeIcon icon={urlsExpanded ? faChevronUp : faChevronDown} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{bookmark.parsedContent ? (
|
||||
<div className="bookmark-content">
|
||||
{shouldTruncate && bookmark.content
|
||||
? <ContentWithResolvedProfiles content={`${bookmark.content.slice(0, 210).trimEnd()}…`} />
|
||||
: renderParsedContent(bookmark.parsedContent)}
|
||||
</div>
|
||||
) : bookmark.content && (
|
||||
<div className="bookmark-content">
|
||||
<ContentWithResolvedProfiles content={shouldTruncate ? `${bookmark.content.slice(0, 210).trimEnd()}…` : bookmark.content} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{contentLength > 210 && (
|
||||
<button
|
||||
className="expand-toggle"
|
||||
onClick={() => setExpanded(v => !v)}
|
||||
aria-label={expanded ? 'Collapse' : 'Expand'}
|
||||
title={expanded ? 'Collapse' : 'Expand'}
|
||||
>
|
||||
<FontAwesomeIcon icon={expanded ? faChevronUp : faChevronDown} />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="bookmark-meta">
|
||||
<span>Kind: {bookmark.kind}</span>
|
||||
{eventNevent ? (
|
||||
<a
|
||||
href={`https://search.dergigi.com/e/${eventNevent}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="kind-icon-link"
|
||||
title="Open event in search"
|
||||
>
|
||||
<span className="kind-icon">
|
||||
<FontAwesomeIcon icon={getKindIcon(bookmark.kind)} />
|
||||
</span>
|
||||
</a>
|
||||
) : (
|
||||
<span className="kind-icon">
|
||||
<FontAwesomeIcon icon={getKindIcon(bookmark.kind)} />
|
||||
</span>
|
||||
)}
|
||||
<span>
|
||||
Author: {short(bookmark.pubkey)}
|
||||
<button className="copy-btn" onClick={() => copy(bookmark.pubkey)} title="Copy author pubkey">
|
||||
<FontAwesomeIcon icon={faCopy} />
|
||||
</button>
|
||||
<a
|
||||
href={`https://search.dergigi.com/p/${authorNpub}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="author-link"
|
||||
title="Open author in search"
|
||||
>
|
||||
by: {getAuthorDisplayName()}
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{hasUrls && firstUrlClassification && (
|
||||
<div className="read-now">
|
||||
<button className="read-now-button" onClick={handleReadNow}>
|
||||
{firstUrlClassification.buttonText}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
import React from 'react'
|
||||
import { Bookmark, ActiveAccount } from '../types/bookmarks'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronLeft } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Bookmark } from '../types/bookmarks'
|
||||
import { BookmarkItem } from './BookmarkItem'
|
||||
import { formatDate, renderParsedContent } from '../utils/bookmarkUtils'
|
||||
import SidebarHeader from './SidebarHeader'
|
||||
|
||||
interface BookmarkListProps {
|
||||
bookmarks: Bookmark[]
|
||||
activeAccount: ActiveAccount | null
|
||||
onSelectUrl?: (url: string) => void
|
||||
isCollapsed: boolean
|
||||
onToggleCollapse: () => void
|
||||
onLogout: () => void
|
||||
formatUserDisplay: () => string
|
||||
}
|
||||
|
||||
export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
bookmarks,
|
||||
activeAccount,
|
||||
onLogout,
|
||||
formatUserDisplay
|
||||
onSelectUrl,
|
||||
isCollapsed,
|
||||
onToggleCollapse,
|
||||
onLogout
|
||||
}) => {
|
||||
return (
|
||||
<div className="bookmarks-container">
|
||||
<div className="bookmarks-header">
|
||||
<div>
|
||||
<h2>Your Bookmarks ({bookmarks.length})</h2>
|
||||
{activeAccount && (
|
||||
<p className="user-info">Logged in as: {formatUserDisplay()}</p>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={onLogout} className="logout-button">
|
||||
Logout
|
||||
if (isCollapsed) {
|
||||
return (
|
||||
<div className="bookmarks-container collapsed">
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="toggle-sidebar-btn"
|
||||
title="Expand bookmarks sidebar"
|
||||
aria-label="Expand bookmarks sidebar"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronLeft} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bookmarks-container">
|
||||
<SidebarHeader onToggleCollapse={onToggleCollapse} onLogout={onLogout} />
|
||||
|
||||
{bookmarks.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
@@ -39,10 +49,18 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
<div className="bookmarks-list">
|
||||
{bookmarks.map((bookmark, index) => (
|
||||
<div key={`${bookmark.id}-${index}`} className="bookmark-item">
|
||||
<h3>{bookmark.title}</h3>
|
||||
{bookmark.bookmarkCount && (
|
||||
<p className="bookmark-count">
|
||||
{bookmark.bookmarkCount} bookmarks in this list
|
||||
{bookmark.bookmarkCount} bookmarks in{' '}
|
||||
<a
|
||||
href={`https://search.dergigi.com/e/${bookmark.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="event-link"
|
||||
>
|
||||
this list
|
||||
</a>
|
||||
:
|
||||
</p>
|
||||
)}
|
||||
{bookmark.urlReferences && bookmark.urlReferences.length > 0 && (
|
||||
@@ -57,10 +75,9 @@ export const BookmarkList: React.FC<BookmarkListProps> = ({
|
||||
)}
|
||||
{bookmark.individualBookmarks && bookmark.individualBookmarks.length > 0 && (
|
||||
<div className="individual-bookmarks">
|
||||
<h4>Individual Bookmarks ({bookmark.individualBookmarks.length}):</h4>
|
||||
<div className="bookmarks-grid">
|
||||
{bookmark.individualBookmarks.map((individualBookmark, index) =>
|
||||
<BookmarkItem key={index} bookmark={individualBookmark} index={index} />
|
||||
<BookmarkItem key={index} bookmark={individualBookmark} index={index} onSelectUrl={onSelectUrl} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import { RelayPool } from 'applesauce-relay'
|
||||
import { Bookmark } from '../types/bookmarks'
|
||||
import { BookmarkList } from './BookmarkList'
|
||||
import { fetchBookmarks } from '../services/bookmarkService'
|
||||
import ContentPanel from './ContentPanel'
|
||||
import { fetchReadableContent, ReadableContent } from '../services/readerService'
|
||||
|
||||
interface BookmarksProps {
|
||||
relayPool: RelayPool | null
|
||||
@@ -15,11 +15,12 @@ interface BookmarksProps {
|
||||
const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
const [bookmarks, setBookmarks] = useState<Bookmark[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [selectedUrl, setSelectedUrl] = useState<string | undefined>(undefined)
|
||||
const [readerLoading, setReaderLoading] = useState(false)
|
||||
const [readerContent, setReaderContent] = useState<ReadableContent | undefined>(undefined)
|
||||
const [isCollapsed, setIsCollapsed] = useState(false)
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const accountManager = Hooks.useAccountManager()
|
||||
|
||||
// Use ProfileModel to get user profile information
|
||||
const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Bookmarks useEffect triggered')
|
||||
@@ -51,52 +52,51 @@ const Bookmarks: React.FC<BookmarksProps> = ({ relayPool, onLogout }) => {
|
||||
await fetchBookmarks(relayPool, fullAccount || activeAccount, setBookmarks, setLoading, timeoutId)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const formatUserDisplay = () => {
|
||||
if (!activeAccount) return 'Unknown User'
|
||||
|
||||
// Use profile data from ProfileModel if available
|
||||
if (profile?.name) {
|
||||
return profile.name
|
||||
const handleSelectUrl = async (url: string) => {
|
||||
setSelectedUrl(url)
|
||||
setReaderLoading(true)
|
||||
setReaderContent(undefined)
|
||||
try {
|
||||
const content = await fetchReadableContent(url)
|
||||
setReaderContent(content)
|
||||
} catch (err) {
|
||||
console.warn('Failed to fetch readable content:', err)
|
||||
} finally {
|
||||
setReaderLoading(false)
|
||||
}
|
||||
if (profile?.display_name) {
|
||||
return profile.display_name
|
||||
}
|
||||
if (profile?.nip05) {
|
||||
return profile.nip05
|
||||
}
|
||||
|
||||
// Fallback to formatted public key
|
||||
return `${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}`
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="bookmarks-container">
|
||||
<div className="bookmarks-header">
|
||||
<div>
|
||||
<h2>Your Bookmarks</h2>
|
||||
{activeAccount && (
|
||||
<p className="user-info">Logged in as: {formatUserDisplay()}</p>
|
||||
)}
|
||||
</div>
|
||||
<button onClick={onLogout} className="logout-button">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
<div className="loading">Loading bookmarks...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<BookmarkList
|
||||
bookmarks={bookmarks}
|
||||
activeAccount={activeAccount || null}
|
||||
onLogout={onLogout}
|
||||
formatUserDisplay={formatUserDisplay}
|
||||
/>
|
||||
<div className={`two-pane ${isCollapsed ? 'sidebar-collapsed' : ''}`}>
|
||||
<div className="pane sidebar">
|
||||
<BookmarkList
|
||||
bookmarks={bookmarks}
|
||||
onSelectUrl={handleSelectUrl}
|
||||
isCollapsed={isCollapsed}
|
||||
onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
|
||||
onLogout={onLogout}
|
||||
/>
|
||||
</div>
|
||||
<div className="pane main">
|
||||
<ContentPanel
|
||||
loading={readerLoading}
|
||||
title={readerContent?.title}
|
||||
html={readerContent?.html}
|
||||
markdown={readerContent?.markdown}
|
||||
selectedUrl={selectedUrl}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
57
src/components/ContentPanel.tsx
Normal file
57
src/components/ContentPanel.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
import React from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import remarkGfm from 'remark-gfm'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faSpinner } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
interface ContentPanelProps {
|
||||
loading: boolean
|
||||
title?: string
|
||||
html?: string
|
||||
markdown?: string
|
||||
selectedUrl?: string
|
||||
}
|
||||
|
||||
const ContentPanel: React.FC<ContentPanelProps> = ({ loading, title, html, markdown, selectedUrl }) => {
|
||||
if (!selectedUrl) {
|
||||
return (
|
||||
<div className="reader empty">
|
||||
<p>Select a bookmark to read its content.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="reader loading">
|
||||
<div className="loading-spinner">
|
||||
<FontAwesomeIcon icon={faSpinner} spin />
|
||||
<span>Loading content…</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="reader">
|
||||
{title && <h2 className="reader-title">{title}</h2>}
|
||||
{markdown ? (
|
||||
<div className="reader-markdown">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
||||
{markdown}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
) : html ? (
|
||||
<div className="reader-html" dangerouslySetInnerHTML={{ __html: html }} />
|
||||
) : (
|
||||
<div className="reader empty">
|
||||
<p>No readable content found for this URL.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContentPanel
|
||||
|
||||
|
||||
37
src/components/ContentWithResolvedProfiles.tsx
Normal file
37
src/components/ContentWithResolvedProfiles.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import { decode } from 'nostr-tools/nip19'
|
||||
import { getPubkeyFromDecodeResult } from 'applesauce-core/helpers'
|
||||
import { extractNprofilePubkeys } from '../utils/helpers'
|
||||
|
||||
interface Props { content: string }
|
||||
|
||||
const ContentWithResolvedProfiles: React.FC<Props> = ({ content }) => {
|
||||
const matches = extractNprofilePubkeys(content)
|
||||
const decoded = matches
|
||||
.map((m) => {
|
||||
try { return decode(m) } catch { return undefined as undefined }
|
||||
})
|
||||
.filter((v): v is ReturnType<typeof decode> => Boolean(v))
|
||||
|
||||
const lookups = decoded
|
||||
.map((res) => getPubkeyFromDecodeResult(res))
|
||||
.filter((v): v is string => typeof v === 'string')
|
||||
|
||||
const profiles = lookups.map((pubkey) => ({ pubkey, profile: useEventModel(Models.ProfileModel, [pubkey]) }))
|
||||
|
||||
let rendered = content
|
||||
matches.forEach((m, i) => {
|
||||
const pk = getPubkeyFromDecodeResult(decoded[i])
|
||||
const found = profiles.find((p) => p.pubkey === pk)
|
||||
const name = found?.profile?.name || found?.profile?.display_name || found?.profile?.nip05 || `${pk?.slice(0,8)}...`
|
||||
if (name) rendered = rendered.replace(m, `@${name}`)
|
||||
})
|
||||
|
||||
return <div className="bookmark-content">{rendered}</div>
|
||||
}
|
||||
|
||||
export default ContentWithResolvedProfiles
|
||||
|
||||
|
||||
37
src/components/IconButton.tsx
Normal file
37
src/components/IconButton.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
interface IconButtonProps {
|
||||
icon: IconDefinition
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void
|
||||
title?: string
|
||||
ariaLabel?: string
|
||||
variant?: 'primary' | 'success' | 'ghost'
|
||||
size?: number
|
||||
}
|
||||
|
||||
const IconButton: React.FC<IconButtonProps> = ({
|
||||
icon,
|
||||
onClick,
|
||||
title,
|
||||
ariaLabel,
|
||||
variant = 'ghost',
|
||||
size = 33
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
className={`icon-button ${variant}`}
|
||||
onClick={onClick}
|
||||
title={title}
|
||||
aria-label={ariaLabel || title}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<FontAwesomeIcon icon={icon} />
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export default IconButton
|
||||
|
||||
|
||||
42
src/components/ResolvedMention.tsx
Normal file
42
src/components/ResolvedMention.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import { decode, npubEncode } from 'nostr-tools/nip19'
|
||||
import { getPubkeyFromDecodeResult } from 'applesauce-core/helpers'
|
||||
|
||||
interface ResolvedMentionProps {
|
||||
encoded?: string
|
||||
}
|
||||
|
||||
const ResolvedMention: React.FC<ResolvedMentionProps> = ({ encoded }) => {
|
||||
if (!encoded) return null
|
||||
let pubkey: string | undefined
|
||||
try {
|
||||
pubkey = getPubkeyFromDecodeResult(decode(encoded))
|
||||
} catch {
|
||||
// ignore; will fallback to showing the encoded value
|
||||
}
|
||||
|
||||
const profile = pubkey ? useEventModel(Models.ProfileModel, [pubkey]) : undefined
|
||||
const display = profile?.name || profile?.display_name || profile?.nip05 || (pubkey ? `${pubkey.slice(0, 8)}...` : encoded)
|
||||
const npub = pubkey ? npubEncode(pubkey) : undefined
|
||||
|
||||
if (npub) {
|
||||
return (
|
||||
<a
|
||||
href={`https://search.dergigi.com/p/${npub}`}
|
||||
className="nostr-mention"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
@{display}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return <span className="nostr-mention">{encoded}</span>
|
||||
}
|
||||
|
||||
export default ResolvedMention
|
||||
|
||||
|
||||
61
src/components/SidebarHeader.tsx
Normal file
61
src/components/SidebarHeader.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import React from 'react'
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faChevronRight, faRightFromBracket, faUser } from '@fortawesome/free-solid-svg-icons'
|
||||
import { Hooks } from 'applesauce-react'
|
||||
import { useEventModel } from 'applesauce-react/hooks'
|
||||
import { Models } from 'applesauce-core'
|
||||
import IconButton from './IconButton'
|
||||
|
||||
interface SidebarHeaderProps {
|
||||
onToggleCollapse: () => void
|
||||
onLogout: () => void
|
||||
}
|
||||
|
||||
const SidebarHeader: React.FC<SidebarHeaderProps> = ({ onToggleCollapse, onLogout }) => {
|
||||
const activeAccount = Hooks.useActiveAccount()
|
||||
const profile = useEventModel(Models.ProfileModel, activeAccount ? [activeAccount.pubkey] : null)
|
||||
|
||||
const getProfileImage = () => {
|
||||
return profile?.picture || null
|
||||
}
|
||||
|
||||
const getUserDisplayName = () => {
|
||||
if (!activeAccount) return 'Unknown User'
|
||||
if (profile?.name) return profile.name
|
||||
if (profile?.display_name) return profile.display_name
|
||||
if (profile?.nip05) return profile.nip05
|
||||
return `${activeAccount.pubkey.slice(0, 8)}...${activeAccount.pubkey.slice(-8)}`
|
||||
}
|
||||
|
||||
const profileImage = getProfileImage()
|
||||
|
||||
return (
|
||||
<div className="sidebar-header-bar">
|
||||
<button
|
||||
onClick={onToggleCollapse}
|
||||
className="toggle-sidebar-btn"
|
||||
title="Collapse bookmarks sidebar"
|
||||
aria-label="Collapse bookmarks sidebar"
|
||||
>
|
||||
<FontAwesomeIcon icon={faChevronRight} />
|
||||
</button>
|
||||
<div className="profile-avatar" title={getUserDisplayName()}>
|
||||
{profileImage ? (
|
||||
<img src={profileImage} alt={getUserDisplayName()} />
|
||||
) : (
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
)}
|
||||
</div>
|
||||
<IconButton
|
||||
icon={faRightFromBracket}
|
||||
onClick={onLogout}
|
||||
title="Logout"
|
||||
ariaLabel="Logout"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SidebarHeader
|
||||
|
||||
49
src/components/kindIcon.ts
Normal file
49
src/components/kindIcon.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { IconDefinition } from '@fortawesome/fontawesome-svg-core'
|
||||
import {
|
||||
faCircleUser,
|
||||
faFeather,
|
||||
faRetweet,
|
||||
faHeart,
|
||||
faImage,
|
||||
faVideo,
|
||||
faFile,
|
||||
faLaptopCode,
|
||||
faCodePullRequest,
|
||||
faBug,
|
||||
faExclamationTriangle,
|
||||
faBolt,
|
||||
faCloudBolt,
|
||||
faHighlighter,
|
||||
faNewspaper,
|
||||
faEyeSlash,
|
||||
faThumbtack,
|
||||
faBookmark
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
const iconMap: Record<number, IconDefinition> = {
|
||||
0: faCircleUser,
|
||||
1: faFeather,
|
||||
6: faRetweet,
|
||||
7: faHeart,
|
||||
20: faImage,
|
||||
21: faVideo,
|
||||
22: faVideo,
|
||||
1063: faFile,
|
||||
1337: faLaptopCode,
|
||||
1617: faCodePullRequest,
|
||||
1621: faBug,
|
||||
1984: faExclamationTriangle,
|
||||
9735: faBolt,
|
||||
9321: faCloudBolt,
|
||||
9802: faHighlighter,
|
||||
30023: faNewspaper,
|
||||
10000: faEyeSlash,
|
||||
10001: faThumbtack,
|
||||
10003: faBookmark
|
||||
}
|
||||
|
||||
export function getKindIcon(kind: number): IconDefinition {
|
||||
return iconMap[kind] || faFile
|
||||
}
|
||||
|
||||
|
||||
390
src/index.css
390
src/index.css
@@ -28,6 +28,7 @@ body {
|
||||
|
||||
.app {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.app header {
|
||||
@@ -97,24 +98,101 @@ body {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.bookmarks-header {
|
||||
.sidebar-header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #333;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.bookmarks-header > div {
|
||||
flex: 1;
|
||||
.profile-avatar {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #2a2a2a;
|
||||
border: 1px solid #444;
|
||||
flex-shrink: 0;
|
||||
color: #ddd;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bookmarks-header h2 {
|
||||
margin: 0;
|
||||
.profile-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-avatar svg {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.sidebar-header-bar .toggle-sidebar-btn {
|
||||
background: transparent;
|
||||
color: #ddd;
|
||||
border: 1px solid #444;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
flex-shrink: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.sidebar-header-bar .toggle-sidebar-btn:hover {
|
||||
background: #2a2a2a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.sidebar-header-bar .toggle-sidebar-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.bookmarks-container.collapsed {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn {
|
||||
background: #2a2a2a;
|
||||
color: #ddd;
|
||||
border: 1px solid #444;
|
||||
padding: 0;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn:hover {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.bookmarks-container.collapsed .toggle-sidebar-btn:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.user-info {
|
||||
margin: 0.5rem 0 0 0;
|
||||
color: #888;
|
||||
@@ -128,6 +206,16 @@ body {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.event-link {
|
||||
color: #8ab4f8;
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.event-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.bookmark-urls {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
@@ -144,12 +232,65 @@ body {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bookmark-url:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.url-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.read-inline-btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.read-inline-btn:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
/* Generic IconButton styling */
|
||||
.icon-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
background: #2a2a2a;
|
||||
color: #ddd;
|
||||
cursor: pointer;
|
||||
min-width: 33px;
|
||||
min-height: 33px;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.icon-button:hover { background: #333; }
|
||||
.icon-button:active { transform: translateY(1px); }
|
||||
|
||||
.icon-button.primary { background: #646cff; color: white; border-color: #646cff; }
|
||||
.icon-button.primary:hover { filter: brightness(1.05); }
|
||||
|
||||
.icon-button.success { background: #28a745; color: white; border-color: #28a745; }
|
||||
.icon-button.success:hover { filter: brightness(1.05); }
|
||||
|
||||
.icon-button.ghost { background: #2a2a2a; }
|
||||
|
||||
.bookmark-events {
|
||||
margin: 1rem 0;
|
||||
}
|
||||
@@ -184,6 +325,9 @@ body {
|
||||
.parsed-content {
|
||||
margin: 1rem 0;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.nostr-mention {
|
||||
@@ -194,6 +338,9 @@ body {
|
||||
padding: 0.2rem 0.4rem;
|
||||
border-radius: 3px;
|
||||
font-size: 0.9rem;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.nostr-mention:hover {
|
||||
@@ -204,7 +351,9 @@ body {
|
||||
.nostr-link {
|
||||
color: #007bff;
|
||||
text-decoration: none;
|
||||
word-break: break-all;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.nostr-link:hover {
|
||||
@@ -249,17 +398,135 @@ body {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Two-pane layout */
|
||||
.two-pane {
|
||||
display: grid;
|
||||
grid-template-columns: 360px 1fr;
|
||||
gap: 1rem;
|
||||
height: calc(100vh - 4rem);
|
||||
transition: grid-template-columns 0.3s ease;
|
||||
}
|
||||
|
||||
.two-pane.sidebar-collapsed {
|
||||
grid-template-columns: 60px 1fr;
|
||||
}
|
||||
|
||||
.pane.sidebar {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pane.main {
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.reader {
|
||||
background: #1a1a1a;
|
||||
border: 1px solid #333;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.reader.empty {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.loading-spinner svg {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.reader-title {
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.reader-html {
|
||||
color: #ddd;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.reader-markdown {
|
||||
color: #ddd;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
/* Ensure content is left-aligned even if source markup uses center */
|
||||
.reader .reader-html *,
|
||||
.reader .reader-markdown * {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.reader center,
|
||||
.reader [align="center"] {
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
/* Tame images from external content */
|
||||
.reader .reader-html img,
|
||||
.reader .reader-markdown img {
|
||||
max-width: 100%;
|
||||
max-height: 70vh;
|
||||
height: auto;
|
||||
width: auto;
|
||||
display: block;
|
||||
margin: 0.75rem 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.reader-markdown h1,
|
||||
.reader-markdown h2,
|
||||
.reader-markdown h3,
|
||||
.reader-markdown h4 {
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
.reader-markdown p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
.reader-markdown a {
|
||||
color: #8ab4f8;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.reader-markdown a:hover { text-decoration: underline; }
|
||||
|
||||
.reader-markdown pre,
|
||||
.reader-markdown code {
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.reader-markdown pre {
|
||||
padding: 0.75rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.reader-markdown code {
|
||||
padding: 0.1rem 0.3rem;
|
||||
}
|
||||
|
||||
.bookmark-item {
|
||||
background: #1a1a1a;
|
||||
padding: 1.5rem;
|
||||
border-radius: 12px;
|
||||
border: 1px solid #333;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.bookmark-item:hover {
|
||||
border-color: #646cff;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
@@ -276,6 +543,13 @@ body {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
word-break: break-all;
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.bookmark-url:hover {
|
||||
@@ -286,6 +560,9 @@ body {
|
||||
color: #ccc;
|
||||
margin: 0.5rem 0;
|
||||
line-height: 1.4;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.bookmark-meta {
|
||||
@@ -315,13 +592,15 @@ body {
|
||||
background: #2a2a2a;
|
||||
padding: 1.25rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #444;
|
||||
transition: all 0.2s ease;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.individual-bookmark:hover {
|
||||
border-color: #646cff;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
@@ -343,6 +622,9 @@ body {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.bookmark-id {
|
||||
@@ -363,6 +645,26 @@ body {
|
||||
margin: 0.75rem 0;
|
||||
color: #ccc;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.expand-toggle {
|
||||
margin: 0.25rem 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #888;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 22px; /* half of default icon button */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.expand-toggle:hover {
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.individual-bookmark .bookmark-meta {
|
||||
@@ -381,15 +683,67 @@ body {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.author-link {
|
||||
color: #8ab4f8;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.author-link:hover { text-decoration: underline; }
|
||||
|
||||
.kind-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 6px;
|
||||
background: #2a2a2a;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.kind-icon svg {
|
||||
font-size: 0.9rem;
|
||||
color: #646cff;
|
||||
}
|
||||
|
||||
.kind-icon-link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.read-now {
|
||||
margin-top: 0.75rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.read-now-button {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.6rem 1rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
transition: background-color 0.2s ease, transform 0.1s ease;
|
||||
}
|
||||
|
||||
.read-now-button:hover {
|
||||
background: #218838;
|
||||
}
|
||||
|
||||
.read-now-button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
/* Private Bookmark Styles */
|
||||
.private-bookmark {
|
||||
border-left: 4px solid #ff6b6b;
|
||||
background: linear-gradient(135deg, #2a2a2a 0%, #1f1f1f 100%);
|
||||
background: #2a2a2a;
|
||||
}
|
||||
|
||||
.private-bookmark:hover {
|
||||
border-color: #ff6b6b;
|
||||
box-shadow: 0 2px 8px rgba(255, 107, 107, 0.2);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.private-indicator {
|
||||
|
||||
39
src/services/bookmarkEvents.ts
Normal file
39
src/services/bookmarkEvents.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
export interface NostrEvent {
|
||||
id: string
|
||||
kind: number
|
||||
created_at: number
|
||||
tags: string[][]
|
||||
content: string
|
||||
pubkey: string
|
||||
sig: string
|
||||
}
|
||||
|
||||
export function dedupeNip51Events(events: NostrEvent[]): NostrEvent[] {
|
||||
const byId = new Map<string, NostrEvent>()
|
||||
for (const e of events) {
|
||||
if (e?.id && !byId.has(e.id)) byId.set(e.id, e)
|
||||
}
|
||||
const unique = Array.from(byId.values())
|
||||
|
||||
const bookmarkLists = unique
|
||||
.filter(e => e.kind === 10003 || e.kind === 30001)
|
||||
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
|
||||
const latestBookmarkList = bookmarkLists.find(list => !list.tags?.some((t: string[]) => t[0] === 'd'))
|
||||
|
||||
const byD = new Map<string, NostrEvent>()
|
||||
for (const e of unique) {
|
||||
if (e.kind === 10003 || e.kind === 30003 || e.kind === 30001) {
|
||||
const d = (e.tags || []).find((t: string[]) => t[0] === 'd')?.[1] || ''
|
||||
const prev = byD.get(d)
|
||||
if (!prev || (e.created_at || 0) > (prev.created_at || 0)) byD.set(d, e)
|
||||
}
|
||||
}
|
||||
|
||||
const setsAndNamedLists = Array.from(byD.values())
|
||||
const out: NostrEvent[] = []
|
||||
if (latestBookmarkList) out.push(latestBookmarkList)
|
||||
out.push(...setsAndNamedLists)
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
147
src/services/bookmarkHelpers.ts
Normal file
147
src/services/bookmarkHelpers.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { getParsedContent } from 'applesauce-content/text'
|
||||
import { ActiveAccount, IndividualBookmark, ParsedContent } from '../types/bookmarks'
|
||||
import type { NostrEvent } from './bookmarkEvents'
|
||||
|
||||
// Global symbol for caching hidden bookmark content on events
|
||||
export const BookmarkHiddenSymbol = Symbol.for('bookmark-hidden')
|
||||
|
||||
export interface BookmarkData {
|
||||
id?: string
|
||||
content?: string
|
||||
created_at?: number
|
||||
kind?: number
|
||||
tags?: string[][]
|
||||
}
|
||||
|
||||
export interface ApplesauceBookmarks {
|
||||
notes?: BookmarkData[]
|
||||
articles?: BookmarkData[]
|
||||
hashtags?: BookmarkData[]
|
||||
urls?: BookmarkData[]
|
||||
}
|
||||
|
||||
export interface AccountWithExtension {
|
||||
pubkey: string
|
||||
signer?: unknown
|
||||
nip04?: unknown
|
||||
nip44?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export function isAccountWithExtension(account: unknown): account is AccountWithExtension {
|
||||
return (
|
||||
typeof account === 'object' &&
|
||||
account !== null &&
|
||||
'pubkey' in account &&
|
||||
typeof (account as { pubkey?: unknown }).pubkey === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
export function isHexId(id: unknown): id is string {
|
||||
return typeof id === 'string' && /^[0-9a-f]{64}$/i.test(id)
|
||||
}
|
||||
export type { NostrEvent } from './bookmarkEvents'
|
||||
export { dedupeNip51Events } from './bookmarkEvents'
|
||||
|
||||
export const processApplesauceBookmarks = (
|
||||
bookmarks: unknown,
|
||||
activeAccount: ActiveAccount,
|
||||
isPrivate: boolean
|
||||
): IndividualBookmark[] => {
|
||||
if (!bookmarks) return []
|
||||
|
||||
if (typeof bookmarks === 'object' && bookmarks !== null && !Array.isArray(bookmarks)) {
|
||||
const applesauceBookmarks = bookmarks as ApplesauceBookmarks
|
||||
const allItems: BookmarkData[] = []
|
||||
if (applesauceBookmarks.notes) allItems.push(...applesauceBookmarks.notes)
|
||||
if (applesauceBookmarks.articles) allItems.push(...applesauceBookmarks.articles)
|
||||
if (applesauceBookmarks.hashtags) allItems.push(...applesauceBookmarks.hashtags)
|
||||
if (applesauceBookmarks.urls) allItems.push(...applesauceBookmarks.urls)
|
||||
return allItems.map((bookmark: BookmarkData) => ({
|
||||
id: bookmark.id || `${isPrivate ? 'private' : 'public'}-${Date.now()}`,
|
||||
content: bookmark.content || '',
|
||||
created_at: bookmark.created_at || Math.floor(Date.now() / 1000),
|
||||
pubkey: activeAccount.pubkey,
|
||||
kind: bookmark.kind || 30001,
|
||||
tags: bookmark.tags || [],
|
||||
parsedContent: bookmark.content ? (getParsedContent(bookmark.content) as ParsedContent) : undefined,
|
||||
type: 'event' as const,
|
||||
isPrivate,
|
||||
added_at: Math.floor(Date.now() / 1000)
|
||||
}))
|
||||
}
|
||||
|
||||
const bookmarkArray = Array.isArray(bookmarks) ? bookmarks : [bookmarks]
|
||||
return bookmarkArray.map((bookmark: BookmarkData) => ({
|
||||
id: bookmark.id || `${isPrivate ? 'private' : 'public'}-${Date.now()}`,
|
||||
content: bookmark.content || '',
|
||||
created_at: bookmark.created_at || Math.floor(Date.now() / 1000),
|
||||
pubkey: activeAccount.pubkey,
|
||||
kind: bookmark.kind || 30001,
|
||||
tags: bookmark.tags || [],
|
||||
parsedContent: bookmark.content ? (getParsedContent(bookmark.content) as ParsedContent) : undefined,
|
||||
type: 'event' as const,
|
||||
isPrivate,
|
||||
added_at: Math.floor(Date.now() / 1000)
|
||||
}))
|
||||
}
|
||||
|
||||
// Types and guards around signer/decryption APIs
|
||||
export function hydrateItems(
|
||||
items: IndividualBookmark[],
|
||||
idToEvent: Map<string, NostrEvent>
|
||||
): IndividualBookmark[] {
|
||||
return items.map(item => {
|
||||
const ev = idToEvent.get(item.id)
|
||||
if (!ev) return item
|
||||
return {
|
||||
...item,
|
||||
pubkey: ev.pubkey || item.pubkey,
|
||||
content: ev.content || item.content || '',
|
||||
created_at: ev.created_at || item.created_at,
|
||||
kind: ev.kind || item.kind,
|
||||
tags: ev.tags || item.tags,
|
||||
parsedContent: ev.content ? (getParsedContent(ev.content) as ParsedContent) : item.parsedContent
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Note: event decryption/collection lives in `bookmarkProcessing.ts`
|
||||
|
||||
export type DecryptFn = (pubkey: string, content: string) => Promise<string>
|
||||
export type UnlockSigner = unknown
|
||||
export type UnlockMode = unknown
|
||||
|
||||
export function hasNip44Decrypt(obj: unknown): obj is { nip44: { decrypt: DecryptFn } } {
|
||||
const nip44 = (obj as { nip44?: unknown })?.nip44 as { decrypt?: unknown } | undefined
|
||||
return typeof nip44?.decrypt === 'function'
|
||||
}
|
||||
|
||||
export function hasNip04Decrypt(obj: unknown): obj is { nip04: { decrypt: DecryptFn } } {
|
||||
const nip04 = (obj as { nip04?: unknown })?.nip04 as { decrypt?: unknown } | undefined
|
||||
return typeof nip04?.decrypt === 'function'
|
||||
}
|
||||
|
||||
export function dedupeBookmarksById(bookmarks: IndividualBookmark[]): IndividualBookmark[] {
|
||||
const seen = new Set<string>()
|
||||
const result: IndividualBookmark[] = []
|
||||
for (const b of bookmarks) {
|
||||
if (!seen.has(b.id)) {
|
||||
seen.add(b.id)
|
||||
result.push(b)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function extractUrlsFromContent(content: string): string[] {
|
||||
if (!content) return []
|
||||
// Basic URL regex covering http(s) schemes
|
||||
const urlRegex = /https?:\/\/[\w.-]+(?:\/[\w\-._~:/?#[\]@!$&'()*+,;=%]*)?/gi
|
||||
const matches = content.match(urlRegex)
|
||||
if (!matches) return []
|
||||
// Normalize by trimming trailing punctuation
|
||||
return Array.from(new Set(matches.map(u => u.replace(/[),.;]+$/, ''))))
|
||||
}
|
||||
|
||||
|
||||
104
src/services/bookmarkProcessing.ts
Normal file
104
src/services/bookmarkProcessing.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Helpers } from 'applesauce-core'
|
||||
import {
|
||||
ActiveAccount,
|
||||
IndividualBookmark
|
||||
} from '../types/bookmarks'
|
||||
import { BookmarkHiddenSymbol, hasNip04Decrypt, hasNip44Decrypt, processApplesauceBookmarks } from './bookmarkHelpers'
|
||||
import type { NostrEvent } from './bookmarkHelpers'
|
||||
|
||||
type DecryptFn = (pubkey: string, content: string) => Promise<string>
|
||||
type UnlockHiddenTagsFn = typeof Helpers.unlockHiddenTags
|
||||
type HiddenContentSigner = Parameters<UnlockHiddenTagsFn>[1]
|
||||
type UnlockMode = Parameters<UnlockHiddenTagsFn>[2]
|
||||
|
||||
export async function collectBookmarksFromEvents(
|
||||
bookmarkListEvents: NostrEvent[],
|
||||
activeAccount: ActiveAccount,
|
||||
signerCandidate?: unknown
|
||||
): Promise<{
|
||||
publicItemsAll: IndividualBookmark[]
|
||||
privateItemsAll: IndividualBookmark[]
|
||||
newestCreatedAt: number
|
||||
latestContent: string
|
||||
allTags: string[][]
|
||||
}> {
|
||||
const publicItemsAll: IndividualBookmark[] = []
|
||||
const privateItemsAll: IndividualBookmark[] = []
|
||||
let newestCreatedAt = 0
|
||||
let latestContent = ''
|
||||
let allTags: string[][] = []
|
||||
|
||||
for (const evt of bookmarkListEvents) {
|
||||
newestCreatedAt = Math.max(newestCreatedAt, evt.created_at || 0)
|
||||
if (!latestContent && evt.content && !Helpers.hasHiddenContent(evt)) latestContent = evt.content
|
||||
if (Array.isArray(evt.tags)) allTags = allTags.concat(evt.tags)
|
||||
|
||||
const pub = Helpers.getPublicBookmarks(evt)
|
||||
publicItemsAll.push(...processApplesauceBookmarks(pub, activeAccount, false))
|
||||
|
||||
try {
|
||||
if (Helpers.hasHiddenTags(evt) && Helpers.isHiddenTagsLocked(evt) && signerCandidate) {
|
||||
try {
|
||||
await Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner)
|
||||
} catch {
|
||||
try {
|
||||
await Helpers.unlockHiddenTags(evt, signerCandidate as HiddenContentSigner, 'nip44' as UnlockMode)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} else if (evt.content && evt.content.length > 0 && signerCandidate) {
|
||||
let decryptedContent: string | undefined
|
||||
try {
|
||||
if (hasNip44Decrypt(signerCandidate)) {
|
||||
decryptedContent = await (signerCandidate as { nip44: { decrypt: DecryptFn } }).nip44.decrypt(
|
||||
evt.pubkey,
|
||||
evt.content
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (!decryptedContent) {
|
||||
try {
|
||||
if (hasNip04Decrypt(signerCandidate)) {
|
||||
decryptedContent = await (signerCandidate as { nip04: { decrypt: DecryptFn } }).nip04.decrypt(
|
||||
evt.pubkey,
|
||||
evt.content
|
||||
)
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
if (decryptedContent) {
|
||||
try {
|
||||
const hiddenTags = JSON.parse(decryptedContent) as string[][]
|
||||
const manualPrivate = Helpers.parseBookmarkTags(hiddenTags)
|
||||
privateItemsAll.push(...processApplesauceBookmarks(manualPrivate, activeAccount, true))
|
||||
Reflect.set(evt, BookmarkHiddenSymbol, manualPrivate)
|
||||
Reflect.set(evt, 'EncryptedContentSymbol', decryptedContent)
|
||||
if (!latestContent) {
|
||||
latestContent = decryptedContent
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const priv = Helpers.getHiddenBookmarks(evt)
|
||||
if (priv) {
|
||||
privateItemsAll.push(...processApplesauceBookmarks(priv, activeAccount, true))
|
||||
}
|
||||
} catch {
|
||||
// ignore individual event failures
|
||||
}
|
||||
}
|
||||
|
||||
return { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,124 +1,19 @@
|
||||
import { RelayPool, completeOnEose } from 'applesauce-relay'
|
||||
import { getParsedContent } from 'applesauce-content/text'
|
||||
import { Helpers } from 'applesauce-core'
|
||||
import { lastValueFrom, takeUntil, timer, toArray } from 'rxjs'
|
||||
// Import the bookmark hidden symbol for caching
|
||||
const BookmarkHiddenSymbol = Symbol.for("bookmark-hidden")
|
||||
import { Bookmark, IndividualBookmark, ParsedContent, ActiveAccount } from '../types/bookmarks'
|
||||
|
||||
interface BookmarkData {
|
||||
id?: string
|
||||
content?: string
|
||||
created_at?: number
|
||||
kind?: number
|
||||
tags?: string[][]
|
||||
}
|
||||
|
||||
interface ApplesauceBookmarks {
|
||||
notes?: BookmarkData[]
|
||||
articles?: BookmarkData[]
|
||||
hashtags?: BookmarkData[]
|
||||
urls?: BookmarkData[]
|
||||
}
|
||||
|
||||
interface AccountWithExtension { pubkey: string; signer?: unknown; nip04?: unknown; nip44?: unknown; [key: string]: unknown }
|
||||
|
||||
function isAccountWithExtension(account: unknown): account is AccountWithExtension {
|
||||
return typeof account === 'object' && account !== null && 'pubkey' in account && typeof (account as any).pubkey === 'string'
|
||||
}
|
||||
|
||||
// Note: Using applesauce's built-in hidden content detection instead of custom logic
|
||||
// Encrypted content detection is handled by applesauce's hasHiddenContent() function
|
||||
|
||||
function isHexId(id: unknown): id is string {
|
||||
return typeof id === 'string' && /^[0-9a-f]{64}$/i.test(id)
|
||||
}
|
||||
|
||||
interface NostrEvent {
|
||||
id: string
|
||||
kind: number
|
||||
created_at: number
|
||||
tags: string[][]
|
||||
content: string
|
||||
pubkey: string
|
||||
sig: string
|
||||
}
|
||||
|
||||
function dedupeNip51Events(events: NostrEvent[]): NostrEvent[] {
|
||||
const byId = new Map<string, NostrEvent>()
|
||||
for (const e of events) { if (e?.id && !byId.has(e.id)) byId.set(e.id, e) }
|
||||
const unique = Array.from(byId.values())
|
||||
|
||||
// Get the latest bookmark list (10003/30001) - default bookmark list without 'd' tag
|
||||
const bookmarkLists = unique
|
||||
.filter(e => e.kind === 10003 || e.kind === 30001)
|
||||
.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
|
||||
const latestBookmarkList = bookmarkLists.find(list =>
|
||||
!list.tags?.some((t: string[]) => t[0] === 'd')
|
||||
)
|
||||
|
||||
// Group bookmark sets (30003) and named bookmark lists (10003/30001 with 'd' tag) by their 'd' identifier
|
||||
const byD = new Map<string, NostrEvent>()
|
||||
for (const e of unique) {
|
||||
if (e.kind === 10003 || e.kind === 30003 || e.kind === 30001) {
|
||||
const d = (e.tags || []).find((t: string[]) => t[0] === 'd')?.[1] || ''
|
||||
const prev = byD.get(d)
|
||||
if (!prev || (e.created_at || 0) > (prev.created_at || 0)) byD.set(d, e)
|
||||
}
|
||||
}
|
||||
|
||||
const setsAndNamedLists = Array.from(byD.values())
|
||||
const out: NostrEvent[] = []
|
||||
|
||||
// Add the default bookmark list if it exists
|
||||
if (latestBookmarkList) out.push(latestBookmarkList)
|
||||
|
||||
// Add all bookmark sets and named bookmark lists
|
||||
out.push(...setsAndNamedLists)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
const processApplesauceBookmarks = (
|
||||
bookmarks: unknown,
|
||||
activeAccount: ActiveAccount,
|
||||
isPrivate: boolean
|
||||
): IndividualBookmark[] => {
|
||||
if (!bookmarks) return []
|
||||
|
||||
if (typeof bookmarks === 'object' && bookmarks !== null && !Array.isArray(bookmarks)) {
|
||||
const applesauceBookmarks = bookmarks as ApplesauceBookmarks
|
||||
const allItems: BookmarkData[] = []
|
||||
if (applesauceBookmarks.notes) allItems.push(...applesauceBookmarks.notes)
|
||||
if (applesauceBookmarks.articles) allItems.push(...applesauceBookmarks.articles)
|
||||
if (applesauceBookmarks.hashtags) allItems.push(...applesauceBookmarks.hashtags)
|
||||
if (applesauceBookmarks.urls) allItems.push(...applesauceBookmarks.urls)
|
||||
return allItems.map((bookmark: BookmarkData) => ({
|
||||
id: bookmark.id || `${isPrivate ? 'private' : 'public'}-${Date.now()}`,
|
||||
content: bookmark.content || '',
|
||||
created_at: bookmark.created_at || Date.now(),
|
||||
pubkey: activeAccount.pubkey,
|
||||
kind: bookmark.kind || 30001,
|
||||
tags: bookmark.tags || [],
|
||||
parsedContent: bookmark.content ? getParsedContent(bookmark.content) as ParsedContent : undefined,
|
||||
type: 'event' as const,
|
||||
isPrivate
|
||||
}))
|
||||
}
|
||||
// Fallback: map array-like bookmarks
|
||||
const bookmarkArray = Array.isArray(bookmarks) ? bookmarks : [bookmarks]
|
||||
return bookmarkArray.map((bookmark: BookmarkData) => ({
|
||||
id: bookmark.id || `${isPrivate ? 'private' : 'public'}-${Date.now()}`,
|
||||
content: bookmark.content || '',
|
||||
created_at: bookmark.created_at || Date.now(),
|
||||
pubkey: activeAccount.pubkey,
|
||||
kind: bookmark.kind || 30001,
|
||||
tags: bookmark.tags || [],
|
||||
parsedContent: bookmark.content ? getParsedContent(bookmark.content) as ParsedContent : undefined,
|
||||
type: 'event' as const,
|
||||
isPrivate
|
||||
}))
|
||||
}
|
||||
import {
|
||||
AccountWithExtension,
|
||||
NostrEvent,
|
||||
dedupeNip51Events,
|
||||
hydrateItems,
|
||||
isAccountWithExtension,
|
||||
isHexId,
|
||||
hasNip04Decrypt,
|
||||
hasNip44Decrypt,
|
||||
dedupeBookmarksById,
|
||||
extractUrlsFromContent
|
||||
} from './bookmarkHelpers'
|
||||
import { Bookmark } from '../types/bookmarks'
|
||||
import { collectBookmarksFromEvents } from './bookmarkProcessing.ts'
|
||||
|
||||
|
||||
|
||||
@@ -181,126 +76,24 @@ export const fetchBookmarks = async (
|
||||
|
||||
// For ExtensionAccount, we need a signer with nip04/nip44 for decrypting hidden content
|
||||
// The ExtensionAccount itself has nip04/nip44 getters that proxy to the signer
|
||||
let signerCandidate: any = maybeAccount
|
||||
if (signerCandidate && !(signerCandidate as any).nip04 && !(signerCandidate as any).nip44 && maybeAccount?.signer) {
|
||||
let signerCandidate: unknown = maybeAccount
|
||||
const hasNip04Prop = (signerCandidate as { nip04?: unknown })?.nip04 !== undefined
|
||||
const hasNip44Prop = (signerCandidate as { nip44?: unknown })?.nip44 !== undefined
|
||||
if (signerCandidate && !hasNip04Prop && !hasNip44Prop && maybeAccount?.signer) {
|
||||
// Fallback to the raw signer if account doesn't have nip04/nip44
|
||||
signerCandidate = maybeAccount.signer
|
||||
}
|
||||
|
||||
console.log('🔑 Signer candidate:', !!signerCandidate, typeof signerCandidate)
|
||||
if (signerCandidate) {
|
||||
console.log('🔑 Signer has nip04:', !!(signerCandidate as any).nip04)
|
||||
console.log('🔑 Signer has nip44:', !!(signerCandidate as any).nip44)
|
||||
}
|
||||
const publicItemsAll: IndividualBookmark[] = []
|
||||
const privateItemsAll: IndividualBookmark[] = []
|
||||
let newestCreatedAt = 0
|
||||
let latestContent = ''
|
||||
let allTags: string[][] = []
|
||||
for (const evt of bookmarkListEvents) {
|
||||
const dTag = evt.tags?.find((t: string[]) => t[0] === 'd')?.[1] || 'none'
|
||||
const firstFewTags = evt.tags?.slice(0, 3).map((t: string[]) => `${t[0]}:${t[1]?.slice(0, 8)}`).join(', ') || 'none'
|
||||
|
||||
console.log('📋 Processing bookmark event:', {
|
||||
id: evt.id?.slice(0, 8),
|
||||
kind: evt.kind,
|
||||
contentLength: evt.content?.length || 0,
|
||||
contentPreview: evt.content?.slice(0, 50) + (evt.content?.length > 50 ? '...' : ''),
|
||||
tagsCount: evt.tags?.length || 0,
|
||||
hasHiddenContent: Helpers.hasHiddenContent(evt),
|
||||
canHaveHiddenTags: Helpers.canHaveHiddenTags(evt.kind),
|
||||
dTag: dTag,
|
||||
firstFewTags: firstFewTags
|
||||
})
|
||||
|
||||
newestCreatedAt = Math.max(newestCreatedAt, evt.created_at || 0)
|
||||
if (!latestContent && evt.content && !Helpers.hasHiddenContent(evt)) latestContent = evt.content
|
||||
if (Array.isArray(evt.tags)) allTags = allTags.concat(evt.tags)
|
||||
// public
|
||||
const pub = Helpers.getPublicBookmarks(evt)
|
||||
publicItemsAll.push(...processApplesauceBookmarks(pub, activeAccount, false))
|
||||
// hidden
|
||||
try {
|
||||
console.log('🔒 Event has hidden tags:', Helpers.hasHiddenTags(evt))
|
||||
console.log('🔒 Hidden tags locked:', Helpers.isHiddenTagsLocked(evt))
|
||||
console.log('🔒 Signer candidate available:', !!signerCandidate)
|
||||
console.log('🔒 Signer candidate type:', typeof signerCandidate)
|
||||
console.log('🔒 Event kind supports hidden tags:', Helpers.canHaveHiddenTags(evt.kind))
|
||||
|
||||
// Try to unlock hidden content using applesauce's standard approach first
|
||||
if (Helpers.hasHiddenTags(evt) && Helpers.isHiddenTagsLocked(evt) && signerCandidate) {
|
||||
try {
|
||||
console.log('🔓 Attempting to unlock hidden tags with signer...')
|
||||
await Helpers.unlockHiddenTags(evt, signerCandidate as any)
|
||||
console.log('✅ Successfully unlocked hidden tags')
|
||||
} catch (error) {
|
||||
console.warn('❌ Failed to unlock with default method, trying NIP-44:', error)
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
await Helpers.unlockHiddenTags(evt, signerCandidate as any, 'nip44' as any)
|
||||
console.log('✅ Successfully unlocked hidden tags with NIP-44')
|
||||
} catch (nip44Error) {
|
||||
console.error('❌ Failed to unlock with NIP-44:', nip44Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
// For events that have content but aren't recognized as supporting hidden tags (like kind 30001)
|
||||
else if (evt.content && evt.content.length > 0 && signerCandidate) {
|
||||
console.log('🔓 Attempting manual decryption for event with unrecognized kind...')
|
||||
console.log('📄 Content to decrypt:', evt.content.slice(0, 100) + '...')
|
||||
|
||||
// Try NIP-44 first (common for bookmark lists), then fall back to NIP-04
|
||||
let decryptedContent: string | undefined
|
||||
try {
|
||||
if ((signerCandidate as any).nip44?.decrypt) {
|
||||
console.log('🧪 Trying NIP-44 decryption...')
|
||||
decryptedContent = await (signerCandidate as any).nip44.decrypt(evt.pubkey, evt.content)
|
||||
}
|
||||
} catch (nip44Err) {
|
||||
console.warn('❌ NIP-44 manual decryption failed, will try NIP-04:', nip44Err)
|
||||
}
|
||||
|
||||
if (!decryptedContent) {
|
||||
try {
|
||||
if ((signerCandidate as any).nip04?.decrypt) {
|
||||
console.log('🧪 Trying NIP-04 decryption...')
|
||||
decryptedContent = await (signerCandidate as any).nip04.decrypt(evt.pubkey, evt.content)
|
||||
}
|
||||
} catch (nip04Err) {
|
||||
console.warn('❌ NIP-04 manual decryption failed:', nip04Err)
|
||||
}
|
||||
}
|
||||
|
||||
if (decryptedContent) {
|
||||
console.log('✅ Successfully decrypted content manually')
|
||||
// Parse the decrypted content as JSON (should be array of tags)
|
||||
try {
|
||||
const hiddenTags = JSON.parse(decryptedContent) as string[][]
|
||||
console.log('📋 Decrypted hidden tags:', hiddenTags.length, 'tags')
|
||||
|
||||
// Turn tags into Bookmarks using applesauce helper, then add to private list immediately
|
||||
const manualPrivate = Helpers.parseBookmarkTags(hiddenTags as any)
|
||||
privateItemsAll.push(...processApplesauceBookmarks(manualPrivate, activeAccount, true))
|
||||
|
||||
// Cache on event for any downstream consumers/debugging
|
||||
Reflect.set(evt, BookmarkHiddenSymbol, manualPrivate)
|
||||
Reflect.set(evt, 'EncryptedContentSymbol', decryptedContent)
|
||||
if (!latestContent) { latestContent = decryptedContent }
|
||||
} catch (parseError) {
|
||||
console.warn('❌ Failed to parse decrypted content as JSON:', parseError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const priv = Helpers.getHiddenBookmarks(evt)
|
||||
console.log('🔍 Hidden bookmarks found:', priv ? Object.keys(priv).map(k => `${k}: ${priv[k as keyof typeof priv]?.length || 0}`).join(', ') : 'none')
|
||||
if (priv) {
|
||||
privateItemsAll.push(...processApplesauceBookmarks(priv, activeAccount, true))
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('❌ Failed to process hidden bookmarks for event:', evt.id, error)
|
||||
}
|
||||
console.log('🔑 Signer has nip04:', hasNip04Decrypt(signerCandidate))
|
||||
console.log('🔑 Signer has nip44:', hasNip44Decrypt(signerCandidate))
|
||||
}
|
||||
const { publicItemsAll, privateItemsAll, newestCreatedAt, latestContent, allTags } = await collectBookmarksFromEvents(
|
||||
bookmarkListEvents,
|
||||
activeAccount,
|
||||
signerCandidate
|
||||
)
|
||||
|
||||
const allItems = [...publicItemsAll, ...privateItemsAll]
|
||||
const noteIds = Array.from(new Set(allItems.map(i => i.id).filter(isHexId)))
|
||||
@@ -315,32 +108,31 @@ export const fetchBookmarks = async (
|
||||
console.warn('Failed to fetch events for hydration:', error)
|
||||
}
|
||||
}
|
||||
const hydrateItems = (items: IndividualBookmark[]): IndividualBookmark[] => items.map(item => {
|
||||
const ev = idToEvent.get(item.id)
|
||||
if (!ev) return item
|
||||
return {
|
||||
...item,
|
||||
content: ev.content || item.content || '',
|
||||
created_at: ev.created_at || item.created_at,
|
||||
kind: ev.kind || item.kind,
|
||||
tags: ev.tags || item.tags,
|
||||
parsedContent: ev.content ? getParsedContent(ev.content) as ParsedContent : item.parsedContent
|
||||
}
|
||||
})
|
||||
const allBookmarks = [...hydrateItems(publicItemsAll), ...hydrateItems(privateItemsAll)]
|
||||
const allBookmarks = dedupeBookmarksById([
|
||||
...hydrateItems(publicItemsAll, idToEvent),
|
||||
...hydrateItems(privateItemsAll, idToEvent)
|
||||
])
|
||||
|
||||
// Sort individual bookmarks by timestamp (newest first)
|
||||
const sortedBookmarks = allBookmarks.sort((a, b) => (b.created_at || 0) - (a.created_at || 0))
|
||||
// Sort individual bookmarks by "added" timestamp first (most recently added first),
|
||||
// falling back to event created_at when unknown.
|
||||
const enriched = allBookmarks.map(b => ({
|
||||
...b,
|
||||
tags: b.tags || [],
|
||||
content: b.content || ''
|
||||
}))
|
||||
const sortedBookmarks = enriched
|
||||
.map(b => ({ ...b, urlReferences: extractUrlsFromContent(b.content) }))
|
||||
.sort((a, b) => ((b.added_at || 0) - (a.added_at || 0)) || ((b.created_at || 0) - (a.created_at || 0)))
|
||||
|
||||
const bookmark: Bookmark = {
|
||||
id: `${activeAccount.pubkey}-bookmarks`,
|
||||
title: `Bookmarks (${sortedBookmarks.length})`,
|
||||
url: '',
|
||||
content: latestContent,
|
||||
created_at: newestCreatedAt || Date.now(),
|
||||
created_at: newestCreatedAt || Math.floor(Date.now() / 1000),
|
||||
tags: allTags,
|
||||
bookmarkCount: sortedBookmarks.length,
|
||||
eventReferences: allTags.filter(tag => tag[0] === 'e').map(tag => tag[1]),
|
||||
eventReferences: allTags.filter((tag: string[]) => tag[0] === 'e').map((tag: string[]) => tag[1]),
|
||||
individualBookmarks: sortedBookmarks,
|
||||
isPrivate: privateItemsAll.length > 0,
|
||||
encryptedContent: undefined
|
||||
|
||||
49
src/services/readerService.ts
Normal file
49
src/services/readerService.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// Lightweight readability-style fetcher using r.jina.ai proxy
|
||||
// Returns simplified HTML for a given URL. This avoids CORS and heavy deps.
|
||||
|
||||
export interface ReadableContent {
|
||||
url: string
|
||||
title?: string
|
||||
html?: string
|
||||
markdown?: string
|
||||
}
|
||||
|
||||
function toProxyUrl(url: string): string {
|
||||
// Ensure the target URL has a protocol and build the proxy URL
|
||||
const normalized = /^https?:\/\//i.test(url) ? url : `https://${url}`
|
||||
return `https://r.jina.ai/http://${normalized.replace(/^https?:\/\//, '')}`
|
||||
}
|
||||
|
||||
export async function fetchReadableContent(targetUrl: string): Promise<ReadableContent> {
|
||||
const proxyUrl = toProxyUrl(targetUrl)
|
||||
const res = await fetch(proxyUrl)
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch readable content (${res.status})`)
|
||||
}
|
||||
const text = await res.text()
|
||||
// Detect if the proxy delivered Markdown or HTML. r.jina.ai often returns a
|
||||
// block starting with "Title:" and "Markdown Content:". We handle both.
|
||||
const hasMarkdownBlock = /Markdown Content:\s/i.test(text)
|
||||
|
||||
if (hasMarkdownBlock) {
|
||||
// Try to split out Title and the Markdown payload
|
||||
const titleMatch = text.match(/Title:\s*(.*?)(?:\s+URL Source:|\s+Markdown Content:)/i)
|
||||
const mdMatch = text.match(/Markdown Content:\s*([\s\S]*)$/i)
|
||||
return {
|
||||
url: targetUrl,
|
||||
title: titleMatch?.[1]?.trim(),
|
||||
markdown: mdMatch?.[1]?.trim()
|
||||
}
|
||||
}
|
||||
|
||||
const html = text
|
||||
// Best-effort title extraction from HTML
|
||||
const match = html.match(/<title[^>]*>(.*?)<\/title>/i)
|
||||
return {
|
||||
url: targetUrl,
|
||||
title: match?.[1],
|
||||
html
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface IndividualBookmark {
|
||||
type: 'event' | 'article'
|
||||
isPrivate?: boolean
|
||||
encryptedContent?: string
|
||||
// When the item was added to the bookmark list (synthetic, for sorting)
|
||||
added_at?: number
|
||||
}
|
||||
|
||||
export interface ActiveAccount {
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
import React from 'react'
|
||||
import { formatDistanceToNow } from 'date-fns'
|
||||
import { ParsedContent, ParsedNode } from '../types/bookmarks'
|
||||
import ResolvedMention from '../components/ResolvedMention'
|
||||
// Note: ContentWithResolvedProfiles is imported by components directly to keep this file component-only for fast refresh
|
||||
|
||||
export const formatDate = (timestamp: number) => {
|
||||
return new Date(timestamp * 1000).toLocaleDateString()
|
||||
const date = new Date(timestamp * 1000)
|
||||
return formatDistanceToNow(date, { addSuffix: true })
|
||||
}
|
||||
|
||||
// Component to render content with resolved nprofile names
|
||||
// Intentionally no exports except components and render helpers
|
||||
|
||||
// Component to render parsed content using applesauce-content
|
||||
export const renderParsedContent = (parsedContent: ParsedContent) => {
|
||||
if (!parsedContent || !parsedContent.children) {
|
||||
@@ -17,17 +24,7 @@ export const renderParsedContent = (parsedContent: ParsedContent) => {
|
||||
}
|
||||
|
||||
if (node.type === 'mention') {
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={`nostr:${node.encoded}`}
|
||||
className="nostr-mention"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{node.encoded}
|
||||
</a>
|
||||
)
|
||||
return <ResolvedMention key={index} encoded={node.encoded} />
|
||||
}
|
||||
|
||||
if (node.type === 'link') {
|
||||
|
||||
39
src/utils/helpers.ts
Normal file
39
src/utils/helpers.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
// Extract pubkeys from nprofile strings in content
|
||||
export const extractNprofilePubkeys = (content: string): string[] => {
|
||||
const nprofileRegex = /nprofile1[a-z0-9]+/gi
|
||||
const matches = content.match(nprofileRegex) || []
|
||||
const unique = new Set<string>(matches)
|
||||
return Array.from(unique)
|
||||
}
|
||||
|
||||
export type UrlType = 'video' | 'image' | 'youtube' | 'article'
|
||||
|
||||
export interface UrlClassification {
|
||||
type: UrlType
|
||||
buttonText: string
|
||||
}
|
||||
|
||||
export const classifyUrl = (url: string): UrlClassification => {
|
||||
const urlLower = url.toLowerCase()
|
||||
|
||||
// Check for YouTube
|
||||
if (urlLower.includes('youtube.com') || urlLower.includes('youtu.be')) {
|
||||
return { type: 'youtube', buttonText: 'WATCH NOW' }
|
||||
}
|
||||
|
||||
// Check for video extensions
|
||||
const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv', '.m4v']
|
||||
if (videoExtensions.some(ext => urlLower.includes(ext))) {
|
||||
return { type: 'video', buttonText: 'WATCH NOW' }
|
||||
}
|
||||
|
||||
// Check for image extensions
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.bmp', '.ico']
|
||||
if (imageExtensions.some(ext => urlLower.includes(ext))) {
|
||||
return { type: 'image', buttonText: 'VIEW NOW' }
|
||||
}
|
||||
|
||||
// Default to article
|
||||
return { type: 'article', buttonText: 'READ NOW' }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user