Add vitepress docs
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
# @welshman/content
|
||||
|
||||
`@welshman/content` is a comprehensive content processing library designed specifically for Nostr applications.
|
||||
It provides a robust system for parsing, processing, and rendering Nostr content while handling various special formats and entities common in the Nostr ecosystem.
|
||||
|
||||
|
||||
## Core Concepts
|
||||
|
||||
The package is built around two main components:
|
||||
|
||||
1. **Parser**: Transforms raw content into structured elements
|
||||
```typescript
|
||||
const parsed = parse({
|
||||
content: "Hello #nostr, check nostr:npub1...",
|
||||
tags: [["p", "pubkey123"]]
|
||||
})
|
||||
```
|
||||
|
||||
2. **Renderer**: Converts parsed content into desired output format
|
||||
```typescript
|
||||
const html = renderAsHtml(parsed).toString()
|
||||
const text = renderAsText(parsed).toString()
|
||||
```
|
||||
|
||||
## Common Use Cases
|
||||
|
||||
- Rendering Nostr notes with proper entity linking
|
||||
- Processing and displaying user content safely
|
||||
- Handling rich text content in Nostr clients
|
||||
- Converting between different content formats
|
||||
- Creating customized content displays
|
||||
|
||||
## Quick Example
|
||||
|
||||
```typescript
|
||||
import { parse, renderAsHtml, truncate } from '@welshman/content'
|
||||
|
||||
// Parse and process content
|
||||
const parsed = parse({
|
||||
content: `
|
||||
Hello #nostr!
|
||||
Check out this note: nostr:note1...
|
||||
https://example.com/image.jpg
|
||||
`,
|
||||
tags: [["p", "pubkey123"]]
|
||||
})
|
||||
|
||||
// Truncate if needed
|
||||
const truncated = truncate(parsed, {
|
||||
maxLength: 500,
|
||||
mediaLength: 150
|
||||
})
|
||||
|
||||
// Render as HTML
|
||||
const html = renderAsHtml(truncated, {
|
||||
entityBase: "https://your-app.com/"
|
||||
}).toString()
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @welshman/content
|
||||
```
|
||||
|
||||
This package is essential for applications that need to handle Nostr content in a structured and safe way, providing all the necessary tools for parsing, processing, and rendering Nostr-specific content formats.
|
||||
@@ -0,0 +1,181 @@
|
||||
# Content Parser
|
||||
|
||||
The content parser system in `@welshman/content` provides a powerful way to parse Nostr content into structured elements.
|
||||
It handles various types of content including Nostr entities, links, code blocks, and special formats.
|
||||
|
||||
## Content Types
|
||||
|
||||
### Basic Types
|
||||
```typescript
|
||||
enum ParsedType {
|
||||
Text = "text", // Plain text
|
||||
Newline = "newline", // Line breaks
|
||||
Topic = "topic", // Hashtags (#nostr)
|
||||
Code = "code", // Code blocks (inline and multi-line)
|
||||
Link = "link", // URLs
|
||||
LinkGrid = "link-grid" // Grid of media links
|
||||
}
|
||||
```
|
||||
|
||||
### Nostr-specific Types
|
||||
```typescript
|
||||
enum ParsedType {
|
||||
Event = "event", // Nostr events (note1/nevent1)
|
||||
Profile = "profile", // Profiles (npub1/nprofile1)
|
||||
Address = "address", // Addresses (naddr1)
|
||||
}
|
||||
```
|
||||
|
||||
### Special Format Types
|
||||
```typescript
|
||||
enum ParsedType {
|
||||
Cashu = "cashu", // Cashu tokens
|
||||
Invoice = "invoice", // Lightning invoices
|
||||
Ellipsis = "ellipsis" // Truncation marker
|
||||
}
|
||||
```
|
||||
|
||||
## Parsing Content
|
||||
|
||||
### Main Parser
|
||||
```typescript
|
||||
const parse = ({
|
||||
content = "",
|
||||
tags = []
|
||||
}: {
|
||||
content?: string
|
||||
tags?: string[][]
|
||||
}) => Parsed[]
|
||||
|
||||
// Example
|
||||
const parsed = parse({
|
||||
content: "Hello #nostr, check nostr:npub1...",
|
||||
tags: [["p", "pubkey123"]]
|
||||
})
|
||||
```
|
||||
|
||||
### Available Parsers
|
||||
|
||||
The system includes specialized parsers for each content type:
|
||||
|
||||
```typescript
|
||||
// Nostr Entities
|
||||
parseAddress(text: string, context: ParseContext): ParsedAddress | void
|
||||
parseEvent(text: string, context: ParseContext): ParsedEvent | void
|
||||
parseProfile(text: string, context: ParseContext): ParsedProfile | void
|
||||
|
||||
// Code Blocks
|
||||
parseCodeBlock(text: string, context: ParseContext): ParsedCode | void
|
||||
parseCodeInline(text: string, context: ParseContext): ParsedCode | void
|
||||
|
||||
// Special Formats
|
||||
parseCashu(text: string, context: ParseContext): ParsedCashu | void
|
||||
parseInvoice(text: string, context: ParseContext): ParsedInvoice | void
|
||||
|
||||
// Basic Content
|
||||
parseLink(text: string, context: ParseContext): ParsedLink | void
|
||||
parseNewline(text: string, context: ParseContext): ParsedNewline | void
|
||||
parseTopic(text: string, context: ParseContext): ParsedTopic | void
|
||||
```
|
||||
|
||||
## Content Processing
|
||||
|
||||
### Truncation
|
||||
```typescript
|
||||
type TruncateOpts = {
|
||||
minLength?: number // Minimum content length (default: 500)
|
||||
maxLength?: number // Maximum content length (default: 700)
|
||||
mediaLength?: number // Length value for media items (default: 200)
|
||||
entityLength?: number // Length value for entities (default: 30)
|
||||
}
|
||||
|
||||
const truncate = (
|
||||
content: Parsed[],
|
||||
options?: TruncateOpts
|
||||
) => Parsed[]
|
||||
|
||||
// Example
|
||||
const truncated = truncate(parsed, {
|
||||
maxLength: 1000,
|
||||
mediaLength: 150
|
||||
})
|
||||
```
|
||||
|
||||
### Link Processing
|
||||
```typescript
|
||||
// Consolidate consecutive image links into grids
|
||||
const reduceLinks = (content: Parsed[]) => Parsed[]
|
||||
|
||||
// Example
|
||||
const processed = reduceLinks(parsed)
|
||||
```
|
||||
|
||||
## Type Guards
|
||||
|
||||
```typescript
|
||||
// Basic content
|
||||
isText(parsed: Parsed): parsed is ParsedText
|
||||
isNewline(parsed: Parsed): parsed is ParsedNewline
|
||||
isCode(parsed: Parsed): parsed is ParsedCode
|
||||
isTopic(parsed: Parsed): parsed is ParsedTopic
|
||||
|
||||
// Links and media
|
||||
isLink(parsed: Parsed): parsed is ParsedLink
|
||||
isImage(parsed: Parsed): parsed is ParsedLink
|
||||
isLinkGrid(parsed: Parsed): parsed is ParsedLinkGrid
|
||||
|
||||
// Nostr entities
|
||||
isEvent(parsed: Parsed): parsed is ParsedEvent
|
||||
isProfile(parsed: Parsed): parsed is ParsedProfile
|
||||
isAddress(parsed: Parsed): parsed is ParsedAddress
|
||||
|
||||
// Special formats
|
||||
isCashu(parsed: Parsed): parsed is ParsedCashu
|
||||
isInvoice(parsed: Parsed): parsed is ParsedInvoice
|
||||
isEllipsis(parsed: Parsed): parsed is ParsedEllipsis
|
||||
```
|
||||
|
||||
## Complete Example
|
||||
|
||||
```typescript
|
||||
// Parse content with tags
|
||||
const parsed = parse({
|
||||
content: `
|
||||
Hello #nostr!
|
||||
|
||||
Check out this note: nostr:note1...
|
||||
And this profile: nostr:npub1...
|
||||
|
||||
Some code: \`console.log("hello")\`
|
||||
|
||||
https://example.com/image.jpg
|
||||
https://example.com/image2.jpg
|
||||
`,
|
||||
tags: [
|
||||
["p", "pubkey123"],
|
||||
["e", "event456"]
|
||||
]
|
||||
})
|
||||
|
||||
// Process the content
|
||||
const processed = reduceLinks(parsed)
|
||||
|
||||
// Truncate if needed
|
||||
const final = truncate(processed, {
|
||||
maxLength: 500,
|
||||
mediaLength: 150
|
||||
})
|
||||
|
||||
// Check types and handle accordingly
|
||||
final.forEach(item => {
|
||||
if (isImage(item)) {
|
||||
// Handle image
|
||||
} else if (isProfile(item)) {
|
||||
// Handle profile reference
|
||||
} else if (isCode(item)) {
|
||||
// Handle code block
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
This parser system provides a robust foundation for handling Nostr content, with support for various content types and processing needs. The type-safe approach ensures reliable content handling while maintaining flexibility for different use cases.
|
||||
@@ -0,0 +1,195 @@
|
||||
# Content Renderer
|
||||
|
||||
The renderer system in `@welshman/content` provides flexible ways to convert parsed content into text or HTML output. It includes customizable rendering options and specialized handlers for each content type.
|
||||
|
||||
## Renderer Class
|
||||
|
||||
```typescript
|
||||
class Renderer {
|
||||
constructor(readonly options: RenderOptions)
|
||||
|
||||
// Core methods
|
||||
toString(): string
|
||||
addText(value: string): void
|
||||
addNewlines(count: number): void
|
||||
addLink(href: string, display: string): void
|
||||
addEntityLink(entity: string): void
|
||||
}
|
||||
```
|
||||
|
||||
## Render Options
|
||||
|
||||
```typescript
|
||||
type RenderOptions = {
|
||||
// String to use for newlines
|
||||
newline: string
|
||||
|
||||
// Base URL for Nostr entities
|
||||
entityBase: string
|
||||
|
||||
// Custom link rendering
|
||||
renderLink: (href: string, display: string) => string
|
||||
|
||||
// Custom entity rendering
|
||||
renderEntity: (entity: string) => string
|
||||
}
|
||||
```
|
||||
|
||||
## Built-in Renderers
|
||||
|
||||
### Text Renderer
|
||||
```typescript
|
||||
const textRenderOptions = {
|
||||
newline: "\n",
|
||||
entityBase: "",
|
||||
renderLink: (href, display) => href,
|
||||
renderEntity: (entity) => entity.slice(0, 16) + "…"
|
||||
}
|
||||
|
||||
const textRenderer = makeTextRenderer({
|
||||
// Override default options if needed
|
||||
})
|
||||
```
|
||||
|
||||
### HTML Renderer
|
||||
```typescript
|
||||
const htmlRenderOptions = {
|
||||
newline: "\n",
|
||||
entityBase: "https://njump.me/",
|
||||
renderLink: (href, display) => {
|
||||
const element = document.createElement("a")
|
||||
element.href = sanitizeUrl(href)
|
||||
element.target = "_blank"
|
||||
element.innerText = display
|
||||
return element.outerHTML
|
||||
},
|
||||
renderEntity: (entity) => entity.slice(0, 16) + "…"
|
||||
}
|
||||
|
||||
const htmlRenderer = makeHtmlRenderer({
|
||||
// Override default options if needed
|
||||
})
|
||||
```
|
||||
|
||||
## Content Type Renderers
|
||||
|
||||
```typescript
|
||||
// Basic content
|
||||
renderText(p: ParsedText, r: Renderer): void
|
||||
renderNewline(p: ParsedNewline, r: Renderer): void
|
||||
renderCode(p: ParsedCode, r: Renderer): void
|
||||
renderTopic(p: ParsedTopic, r: Renderer): void
|
||||
|
||||
// Links
|
||||
renderLink(p: ParsedLink, r: Renderer): void
|
||||
|
||||
// Nostr entities
|
||||
renderEvent(p: ParsedEvent, r: Renderer): void
|
||||
renderProfile(p: ParsedProfile, r: Renderer): void
|
||||
renderAddress(p: ParsedAddress, r: Renderer): void
|
||||
|
||||
// Special formats
|
||||
renderCashu(p: ParsedCashu, r: Renderer): void
|
||||
renderInvoice(p: ParsedInvoice, r: Renderer): void
|
||||
renderEllipsis(p: ParsedEllipsis, r: Renderer): void
|
||||
```
|
||||
|
||||
## Usage Examples
|
||||
|
||||
### Basic Text Rendering
|
||||
```typescript
|
||||
const parsed = parse({
|
||||
content: "Hello #nostr, check nostr:npub1...",
|
||||
tags: []
|
||||
})
|
||||
|
||||
// Render as plain text
|
||||
const text = renderAsText(parsed).toString()
|
||||
|
||||
// Render as HTML
|
||||
const html = renderAsHtml(parsed).toString()
|
||||
```
|
||||
|
||||
### Custom Rendering Options
|
||||
```typescript
|
||||
// Custom text renderer
|
||||
const customText = renderAsText(parsed, {
|
||||
entityBase: "nostr:",
|
||||
renderEntity: (entity) => entity.slice(0, 8)
|
||||
}).toString()
|
||||
|
||||
// Custom HTML renderer
|
||||
const customHtml = renderAsHtml(parsed, {
|
||||
entityBase: "https://example.com/",
|
||||
renderLink: (href, display) => `<a class="custom-link" href="${href}">${display}</a>`,
|
||||
renderEntity: (entity) => `<span class="entity">${entity}</span>`
|
||||
}).toString()
|
||||
```
|
||||
|
||||
### Rendering Individual Elements
|
||||
```typescript
|
||||
const renderer = makeHtmlRenderer()
|
||||
|
||||
// Render single element
|
||||
renderOne({
|
||||
type: ParsedType.Link,
|
||||
value: {
|
||||
url: new URL("https://example.com"),
|
||||
meta: {},
|
||||
isMedia: false
|
||||
},
|
||||
raw: "https://example.com"
|
||||
}, renderer)
|
||||
|
||||
// Render multiple elements
|
||||
renderMany([
|
||||
{
|
||||
type: ParsedType.Text,
|
||||
value: "Hello ",
|
||||
raw: "Hello "
|
||||
},
|
||||
{
|
||||
type: ParsedType.Topic,
|
||||
value: "nostr",
|
||||
raw: "#nostr"
|
||||
}
|
||||
], renderer)
|
||||
```
|
||||
|
||||
### Complete Example
|
||||
```typescript
|
||||
// Parse and process content
|
||||
const parsed = parse({
|
||||
content: `
|
||||
Check out this profile: nostr:npub1...
|
||||
|
||||
Code example:
|
||||
\`console.log("hello")\`
|
||||
|
||||
#nostr #bitcoin
|
||||
|
||||
https://example.com/image.jpg
|
||||
`,
|
||||
tags: []
|
||||
})
|
||||
|
||||
// Create custom renderer
|
||||
const renderer = makeHtmlRenderer({
|
||||
entityBase: "https://example.com/",
|
||||
renderLink: (href, display) => {
|
||||
if (href.endsWith('.jpg')) {
|
||||
return `<img src="${href}" alt="${display}">`
|
||||
}
|
||||
return `<a href="${href}">${display}</a>`
|
||||
},
|
||||
renderEntity: (entity) => {
|
||||
return `<span class="entity">${entity.slice(0, 8)}</span>`
|
||||
}
|
||||
})
|
||||
|
||||
// Render content
|
||||
const html = render(parsed, renderer).toString()
|
||||
```
|
||||
|
||||
|
||||
The renderer system provides a flexible way to output parsed content in various formats while maintaining control over the rendering process. Its modular design allows for easy customization and extension for specific application needs.
|
||||
Reference in New Issue
Block a user