Add feed validation and display
This commit is contained in:
@@ -1,29 +0,0 @@
|
|||||||
# @welshman/content [](https://npmjs.com/package/@welshman/content)
|
|
||||||
|
|
||||||
Utilities for parsing and rendering note content. Customizable via RenderOptions.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {parse, render} from '@welshman/content'
|
|
||||||
|
|
||||||
const content = "Hello<br>from https://coracle.tools! <script>alert('evil')</script>"
|
|
||||||
const parsed = parse({content, tags: []})
|
|
||||||
// [
|
|
||||||
// { type: 'text', value: 'Hello<br>from ', raw: 'Hello<br>from ' },
|
|
||||||
// {
|
|
||||||
// type: 'link',
|
|
||||||
// value: { url: URL, isMedia: false },
|
|
||||||
// raw: 'https://coracle.tools'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'text',
|
|
||||||
// value: "! <script>alert('evil')</script>",
|
|
||||||
// raw: "! <script>alert('evil')</script>"
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
const result = renderAsText(parsed)
|
|
||||||
// => Hello<br>from https://coracle.tools/! <script>alert('evil')</script>
|
|
||||||
|
|
||||||
const result = renderAsHtml(parsed)
|
|
||||||
// => Hello<br>from <a href="https://coracle.tools/" target="_blank">coracle.tools/</a>! <script>alert('evil')</script>
|
|
||||||
```
|
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@welshman/lib": "workspace:*",
|
"@welshman/lib": "workspace:*",
|
||||||
"@welshman/net": "workspace:*",
|
"@welshman/net": "workspace:*",
|
||||||
"@welshman/util": "workspace:*"
|
"@welshman/util": "workspace:*",
|
||||||
|
"trava": "^1.2.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"rimraf": "~6.0.0",
|
"rimraf": "~6.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import {now, uniq, displayList} from "@welshman/lib"
|
||||||
|
import {
|
||||||
|
FeedType,
|
||||||
|
Feed,
|
||||||
|
AddressFeed,
|
||||||
|
AuthorFeed,
|
||||||
|
CreatedAtFeed,
|
||||||
|
DVMFeed,
|
||||||
|
DifferenceFeed,
|
||||||
|
IDFeed,
|
||||||
|
IntersectionFeed,
|
||||||
|
GlobalFeed,
|
||||||
|
KindFeed,
|
||||||
|
ListFeed,
|
||||||
|
LabelFeed,
|
||||||
|
WOTFeed,
|
||||||
|
RelayFeed,
|
||||||
|
ScopeFeed,
|
||||||
|
SearchFeed,
|
||||||
|
TagFeed,
|
||||||
|
UnionFeed,
|
||||||
|
} from "./core.js"
|
||||||
|
import {getFeedArgs} from "./utils.js"
|
||||||
|
|
||||||
|
export const displayAddressFeed = (feed: AddressFeed) => {
|
||||||
|
const n = getFeedArgs(feed).length
|
||||||
|
|
||||||
|
return `${n} replaceable {n === 1 ? 'event' : 'events'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayAuthorFeed = (feed: AuthorFeed) => {
|
||||||
|
const n = getFeedArgs(feed).length
|
||||||
|
|
||||||
|
return `events from ${n} replaceable {n === 1 ? 'person' : 'people'}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayCreatedAtFeed = (feed: CreatedAtFeed) => {
|
||||||
|
const items = getFeedArgs(feed)
|
||||||
|
const descriptions: string[] = []
|
||||||
|
|
||||||
|
for (const {since, until, relative = []} of items) {
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
if (since) {
|
||||||
|
const timestamp = relative.includes("since") ? now() - since : since
|
||||||
|
parts.push(`after ${new Date(timestamp * 1000).toLocaleString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (until) {
|
||||||
|
const timestamp = relative.includes("until") ? now() - until : until
|
||||||
|
parts.push(`before ${new Date(timestamp * 1000).toLocaleString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length > 0) {
|
||||||
|
descriptions.push(parts.join(" and "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (descriptions.length === 0) {
|
||||||
|
return "events from any time"
|
||||||
|
}
|
||||||
|
|
||||||
|
return `events ${displayList(descriptions, "or")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayDVMFeed = (feed: DVMFeed) => {
|
||||||
|
const items = getFeedArgs(feed)
|
||||||
|
const descriptions: string[] = []
|
||||||
|
|
||||||
|
for (const {kind, tags = [], relays = []} of items) {
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
parts.push(`kind ${kind}`)
|
||||||
|
|
||||||
|
if (tags.length > 0) {
|
||||||
|
parts.push(`with ${tags.length} tag${tags.length === 1 ? "" : "s"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relays.length > 0) {
|
||||||
|
parts.push(`from ${displayList(relays)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptions.push(parts.join(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return `events from DVM requests of ${displayList(descriptions)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayDifferenceFeed = (feed: DifferenceFeed) => {
|
||||||
|
const [base, ...excluded] = getFeedArgs(feed)
|
||||||
|
|
||||||
|
return `all ${displayFeed(base)}, excluding ${displayList(excluded.map(displayFeed))}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayIDFeed = (feed: IDFeed) => `${getFeedArgs(feed).length} events`
|
||||||
|
|
||||||
|
export const displayIntersectionFeed = (feed: IntersectionFeed) =>
|
||||||
|
`events matching ${displayList(getFeedArgs(feed).map(displayFeed))}`
|
||||||
|
|
||||||
|
export const displayGlobalFeed = (feed: GlobalFeed) => "anything"
|
||||||
|
|
||||||
|
export const displayKindFeed = (feed: KindFeed) =>
|
||||||
|
`events of kind ${displayList(getFeedArgs(feed))}`
|
||||||
|
|
||||||
|
export const displayListFeed = (feed: ListFeed) => {
|
||||||
|
const addresses = uniq(getFeedArgs(feed).flatMap(({addresses}) => addresses))
|
||||||
|
|
||||||
|
return `events from ${addresses.length} list${addresses.length === 1 ? "" : "s"}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayLabelFeed = (feed: LabelFeed) => {
|
||||||
|
const items = getFeedArgs(feed)
|
||||||
|
const descriptions: string[] = []
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
const parts: string[] = []
|
||||||
|
|
||||||
|
if (item.authors?.length) {
|
||||||
|
parts.push(`by ${item.authors.length} author${item.authors.length === 1 ? "" : "s"}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tags = Object.entries(item)
|
||||||
|
.filter(([key]) => key.startsWith("#"))
|
||||||
|
.map(([key, values]) => `${key}=${displayList(values as string[])}`)
|
||||||
|
|
||||||
|
if (tags.length) {
|
||||||
|
parts.push(`with tags ${displayList(tags)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.relays?.length) {
|
||||||
|
parts.push(`from ${displayList(item.relays)}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
descriptions.push(parts.join(" "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return `events ${displayList(descriptions)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayWOTFeed = (feed: WOTFeed) => {
|
||||||
|
const descriptions = getFeedArgs(feed).map(({min = 0, max = 1}) =>
|
||||||
|
min === max ? `WOT score of ${min}` : `WOT score between ${min} and ${max}`,
|
||||||
|
)
|
||||||
|
|
||||||
|
return `Events from authors with ${displayList(descriptions)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayRelayFeed = (feed: RelayFeed) => `events from ${displayList(getFeedArgs(feed))}`
|
||||||
|
|
||||||
|
export const displayScopeFeed = (feed: ScopeFeed) =>
|
||||||
|
`events from ${displayList(getFeedArgs(feed).map(s => s.toLowerCase()))}`
|
||||||
|
|
||||||
|
export const displaySearchFeed = (feed: SearchFeed) =>
|
||||||
|
`events matching ${displayList(getFeedArgs(feed).map(term => `"${term}"`))}`
|
||||||
|
|
||||||
|
export const displayTagFeed = (feed: TagFeed) => {
|
||||||
|
const [key, ...values] = getFeedArgs(feed)
|
||||||
|
|
||||||
|
return `events with ${key} tag matching ${displayList(values, "or")}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayUnionFeed = (feed: UnionFeed) =>
|
||||||
|
`all ${displayList(getFeedArgs(feed).map(displayFeed))}`
|
||||||
|
|
||||||
|
export const displayFeed = (feed: Feed): string => {
|
||||||
|
switch (feed[0]) {
|
||||||
|
case FeedType.Address:
|
||||||
|
return displayAddressFeed(feed)
|
||||||
|
case FeedType.Author:
|
||||||
|
return displayAuthorFeed(feed)
|
||||||
|
case FeedType.CreatedAt:
|
||||||
|
return displayCreatedAtFeed(feed)
|
||||||
|
case FeedType.DVM:
|
||||||
|
return displayDVMFeed(feed)
|
||||||
|
case FeedType.Difference:
|
||||||
|
return displayDifferenceFeed(feed)
|
||||||
|
case FeedType.ID:
|
||||||
|
return displayIDFeed(feed)
|
||||||
|
case FeedType.Intersection:
|
||||||
|
return displayIntersectionFeed(feed)
|
||||||
|
case FeedType.Global:
|
||||||
|
return displayGlobalFeed(feed)
|
||||||
|
case FeedType.Kind:
|
||||||
|
return displayKindFeed(feed)
|
||||||
|
case FeedType.List:
|
||||||
|
return displayListFeed(feed)
|
||||||
|
case FeedType.Label:
|
||||||
|
return displayLabelFeed(feed)
|
||||||
|
case FeedType.WOT:
|
||||||
|
return displayWOTFeed(feed)
|
||||||
|
case FeedType.Relay:
|
||||||
|
return displayRelayFeed(feed)
|
||||||
|
case FeedType.Scope:
|
||||||
|
return displayScopeFeed(feed)
|
||||||
|
case FeedType.Search:
|
||||||
|
return displaySearchFeed(feed)
|
||||||
|
case FeedType.Tag:
|
||||||
|
return displayTagFeed(feed)
|
||||||
|
case FeedType.Union:
|
||||||
|
return displayUnionFeed(feed)
|
||||||
|
default:
|
||||||
|
return "[unknown feed type]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const displayFeeds = (feeds: Feed[]) => displayList(feeds.map(displayFeed))
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export * from "./core.js"
|
export * from "./core.js"
|
||||||
export * from "./compiler.js"
|
export * from "./compiler.js"
|
||||||
export * from "./controller.js"
|
export * from "./controller.js"
|
||||||
|
export * from "./display.js"
|
||||||
export * from "./utils.js"
|
export * from "./utils.js"
|
||||||
|
export * from "./validate.js"
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import Trava from "trava"
|
||||||
|
import {spec, isPojo} from "@welshman/lib"
|
||||||
|
import {isRelayUrl, Address} from "@welshman/util"
|
||||||
|
import {Scope, FeedType} from "./core.js"
|
||||||
|
import {getFeedArgs} from "./utils.js"
|
||||||
|
|
||||||
|
const {ValidationError, Compose, Check, Each, Optional, Keys} = Trava
|
||||||
|
|
||||||
|
const validateNumber = Check((x: any) => typeof x === "number", "Value must be a number")
|
||||||
|
|
||||||
|
const validateString = Check((x: any) => typeof x === "string", "Value must be a string")
|
||||||
|
|
||||||
|
const validateRelay = Check((x: any) => typeof x === "string" && isRelayUrl(x), "Invalid relay url")
|
||||||
|
|
||||||
|
const validateScope = Check((x: any) => Object.values(Scope).includes(x), "Invalid scope")
|
||||||
|
|
||||||
|
const validateHex = Check((x: any) => typeof x === "string" && x.length === 64, "Invalid hex value")
|
||||||
|
|
||||||
|
const validateAddress = Check(
|
||||||
|
(x: any) => typeof x === "string" && Address.isAddress(x),
|
||||||
|
"Invalid address",
|
||||||
|
)
|
||||||
|
|
||||||
|
const validateArray = Check((x: any) => Array.isArray(x), "Value must be an array")
|
||||||
|
|
||||||
|
const validateObject = Check((x: any) => isPojo(x), "Value must be an object")
|
||||||
|
|
||||||
|
export {ValidationError}
|
||||||
|
|
||||||
|
export const validateTagFeedMapping = Compose([
|
||||||
|
validateArray,
|
||||||
|
Check(spec({length: 2}), "Tag feed mappings must have two entries"),
|
||||||
|
(a: any[]) => validateString(a[0]),
|
||||||
|
(a: any[]) => validateFeed(a[1]),
|
||||||
|
])
|
||||||
|
|
||||||
|
export const validateFeedArgs = (validateArgument: Trava.Validator) => (feed: any) => {
|
||||||
|
let error = validateArray(feed)
|
||||||
|
if (error instanceof ValidationError) return error
|
||||||
|
|
||||||
|
for (const argument of getFeedArgs(feed)) {
|
||||||
|
error = validateArgument(argument)
|
||||||
|
if (error instanceof ValidationError) return error
|
||||||
|
}
|
||||||
|
|
||||||
|
return feed
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateAddressFeed = validateFeedArgs(validateAddress)
|
||||||
|
|
||||||
|
export const validateAuthorFeed = validateFeedArgs(validateHex)
|
||||||
|
|
||||||
|
export const validateCreatedAtFeed = validateFeedArgs(
|
||||||
|
Keys({
|
||||||
|
since: Optional(validateNumber),
|
||||||
|
until: Optional(validateNumber),
|
||||||
|
relative: Optional(Each(validateString)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const validateDVMFeed = validateFeedArgs(
|
||||||
|
Keys({
|
||||||
|
kind: validateNumber,
|
||||||
|
tags: Optional(Each(validateString)),
|
||||||
|
relays: Optional(Each(validateString)),
|
||||||
|
mappings: Optional(Each(validateTagFeedMapping)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const validateDifferenceFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||||
|
|
||||||
|
export const validateIDFeed = validateFeedArgs(validateHex)
|
||||||
|
|
||||||
|
export const validateIntersectionFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||||
|
|
||||||
|
export const validateGlobalFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||||
|
|
||||||
|
export const validateKindFeed = validateFeedArgs(validateNumber)
|
||||||
|
|
||||||
|
export const validateListFeed = validateFeedArgs(
|
||||||
|
Keys({
|
||||||
|
addresses: Optional(Each(validateString)),
|
||||||
|
mappings: Optional(Each(validateTagFeedMapping)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const validateLabelFeed = validateFeedArgs(
|
||||||
|
Compose([
|
||||||
|
validateObject,
|
||||||
|
(item: any) => {
|
||||||
|
const validateRelays = Each(validateRelay)
|
||||||
|
const validateAuthors = Each(validateHex)
|
||||||
|
const validateStrings = Each(validateString)
|
||||||
|
const validateMappings = Each(validateTagFeedMapping)
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(item)) {
|
||||||
|
let error
|
||||||
|
if (key === "relays") {
|
||||||
|
error = validateRelays(value)
|
||||||
|
} else if (key === "authors") {
|
||||||
|
error = validateAuthors(value)
|
||||||
|
} else if (key === "mappings") {
|
||||||
|
error = validateMappings(value)
|
||||||
|
} else if (key.match("^#.$")) {
|
||||||
|
error = validateStrings(value)
|
||||||
|
} else {
|
||||||
|
error = new ValidationError("Invalid label item")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ValidationError) return error
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const validateWOTFeed = validateFeedArgs(
|
||||||
|
Keys({
|
||||||
|
min: Optional(validateNumber),
|
||||||
|
max: Optional(validateNumber),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
export const validateRelayFeed = validateFeedArgs(validateRelay)
|
||||||
|
|
||||||
|
export const validateScopeFeed = validateFeedArgs(validateScope)
|
||||||
|
|
||||||
|
export const validateSearchFeed = validateFeedArgs(validateString)
|
||||||
|
|
||||||
|
export const validateTagFeed = validateFeedArgs(validateString)
|
||||||
|
|
||||||
|
export const validateUnionFeed = validateFeedArgs((x: any) => validateFeed(x))
|
||||||
|
|
||||||
|
export const validateFeed = (feed: any) => {
|
||||||
|
const error = validateArray(feed)
|
||||||
|
if (error instanceof ValidationError) return error
|
||||||
|
|
||||||
|
switch (feed[0]) {
|
||||||
|
case FeedType.Address:
|
||||||
|
return validateAddressFeed(feed)
|
||||||
|
case FeedType.Author:
|
||||||
|
return validateAuthorFeed(feed)
|
||||||
|
case FeedType.CreatedAt:
|
||||||
|
return validateCreatedAtFeed(feed)
|
||||||
|
case FeedType.DVM:
|
||||||
|
return validateDVMFeed(feed)
|
||||||
|
case FeedType.Difference:
|
||||||
|
return validateDifferenceFeed(feed)
|
||||||
|
case FeedType.ID:
|
||||||
|
return validateIDFeed(feed)
|
||||||
|
case FeedType.Intersection:
|
||||||
|
return validateIntersectionFeed(feed)
|
||||||
|
case FeedType.Global:
|
||||||
|
return validateGlobalFeed(feed)
|
||||||
|
case FeedType.Kind:
|
||||||
|
return validateKindFeed(feed)
|
||||||
|
case FeedType.List:
|
||||||
|
return validateListFeed(feed)
|
||||||
|
case FeedType.Label:
|
||||||
|
return validateLabelFeed(feed)
|
||||||
|
case FeedType.WOT:
|
||||||
|
return validateWOTFeed(feed)
|
||||||
|
case FeedType.Relay:
|
||||||
|
return validateRelayFeed(feed)
|
||||||
|
case FeedType.Scope:
|
||||||
|
return validateScopeFeed(feed)
|
||||||
|
case FeedType.Search:
|
||||||
|
return validateSearchFeed(feed)
|
||||||
|
case FeedType.Tag:
|
||||||
|
return validateTagFeed(feed)
|
||||||
|
case FeedType.Union:
|
||||||
|
return validateUnionFeed(feed)
|
||||||
|
default:
|
||||||
|
return new ValidationError("Unknown feed type")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -310,6 +310,61 @@ describe("Tools", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("displayList", () => {
|
||||||
|
it("should return an empty string when the list is empty", () => {
|
||||||
|
const list = []
|
||||||
|
const output = T.displayList(list)
|
||||||
|
|
||||||
|
expect(output).toEqual("")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a single entry when list length is one", () => {
|
||||||
|
const list = ["Apple"]
|
||||||
|
const output = T.displayList(list)
|
||||||
|
|
||||||
|
expect(output).toEqual("Apple")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a string of both items when the list length is two", () => {
|
||||||
|
const list = ["Apple", "Banana"]
|
||||||
|
const output = T.displayList(list)
|
||||||
|
|
||||||
|
expect(output).toEqual("Apple and Banana")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a string of all items when the list length is three", () => {
|
||||||
|
const list = ["Apple", "Banana", "Cherry"]
|
||||||
|
const output = T.displayList(list)
|
||||||
|
|
||||||
|
expect(output).toEqual("Apple, Banana, and Cherry")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a truncated string of all items when the list is long", () => {
|
||||||
|
const list = [
|
||||||
|
"Apple",
|
||||||
|
"Banana",
|
||||||
|
"Orange",
|
||||||
|
"Pear",
|
||||||
|
"Grapes",
|
||||||
|
"Mango",
|
||||||
|
"Pineapple",
|
||||||
|
"Kiwi",
|
||||||
|
"Strawberry",
|
||||||
|
"Blueberry",
|
||||||
|
]
|
||||||
|
const output = T.displayList(list)
|
||||||
|
|
||||||
|
expect(output).toEqual("Apple, Banana, Orange, Pear, Grapes, Mango, and 4 others")
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should return a string of both items with list of two numbers", () => {
|
||||||
|
const list = [7, 17]
|
||||||
|
const output = T.displayList(list)
|
||||||
|
|
||||||
|
expect(output).toEqual("7 and 17")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("Collection Operations", () => {
|
describe("Collection Operations", () => {
|
||||||
it("should handle group operations", () => {
|
it("should handle group operations", () => {
|
||||||
const items = [
|
const items = [
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type Nil = null | undefined
|
|||||||
|
|
||||||
export const isNil = <T>(x: T, ...args: unknown[]) => x === undefined || x === null
|
export const isNil = <T>(x: T, ...args: unknown[]) => x === undefined || x === null
|
||||||
|
|
||||||
export const isNotNil = <T>(x: T, ...args: unknown[]) => x !== undefined || x !== null
|
export const isNotNil = <T>(x: T, ...args: unknown[]) => x !== undefined && x !== null
|
||||||
|
|
||||||
export const assertNotNil = <T>(x: T, ...args: unknown[]) => x!
|
export const assertNotNil = <T>(x: T, ...args: unknown[]) => x!
|
||||||
|
|
||||||
@@ -1358,6 +1358,19 @@ export const ellipsize = (s: string, l: number, suffix = "...") => {
|
|||||||
return s + suffix
|
return s + suffix
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Displays a list of items with oxford commas and a chosen conjunction */
|
||||||
|
export const displayList = <T>(xs: T[], conj = "and", n = 6) => {
|
||||||
|
if (xs.length > n + 2) {
|
||||||
|
return `${xs.slice(0, n).join(", ")}, ${conj} ${xs.length - n} others`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (xs.length < 3) {
|
||||||
|
return xs.join(` ${conj} `)
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${xs.slice(0, -1).join(", ")}, ${conj} ${xs.slice(-1).join("")}`
|
||||||
|
}
|
||||||
|
|
||||||
/** Generates a hash string from input string */
|
/** Generates a hash string from input string */
|
||||||
export const hash = (s: string) =>
|
export const hash = (s: string) =>
|
||||||
Math.abs(s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)).toString()
|
Math.abs(s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0)).toString()
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
# @welshman/content [](https://npmjs.com/package/@welshman/content)
|
|
||||||
|
|
||||||
Utilities for parsing and rendering note content. Customizable via RenderOptions.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {parse, render} from '@welshman/content'
|
|
||||||
|
|
||||||
const content = "Hello<br>from https://coracle.tools! <script>alert('evil')</script>"
|
|
||||||
const parsed = parse({content, tags: []})
|
|
||||||
// [
|
|
||||||
// { type: 'text', value: 'Hello<br>from ', raw: 'Hello<br>from ' },
|
|
||||||
// {
|
|
||||||
// type: 'link',
|
|
||||||
// value: { url: URL, isMedia: false },
|
|
||||||
// raw: 'https://coracle.tools'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// type: 'text',
|
|
||||||
// value: "! <script>alert('evil')</script>",
|
|
||||||
// raw: "! <script>alert('evil')</script>"
|
|
||||||
// }
|
|
||||||
// ]
|
|
||||||
|
|
||||||
const result = renderAsText(parsed)
|
|
||||||
// => Hello<br>from https://coracle.tools/! <script>alert('evil')</script>
|
|
||||||
|
|
||||||
const result = renderAsHtml(parsed)
|
|
||||||
// => Hello<br>from <a href="https://coracle.tools/" target="_blank">coracle.tools/</a>! <script>alert('evil')</script>
|
|
||||||
```
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# @welshman/store [](https://npmjs.com/package/@welshman/store)
|
|
||||||
|
|
||||||
Utilities for dealing with svelte stores when using welshman.
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {Repository, NAMED_PEOPLE, NAMED_TOPICS, type TrustedEvent, readUserList, List} from '@welshman/util'
|
|
||||||
import {deriveEventsMapped} from '@welshman/store'
|
|
||||||
|
|
||||||
const repository = new Repository()
|
|
||||||
|
|
||||||
// Create a svelte store that performantly maps matching events in the repository to List objects
|
|
||||||
const lists = deriveEventsMapped<PublishedUserList>(repository, {
|
|
||||||
filters: [{kinds: [NAMED_PEOPLE, NAMED_TOPICS]}],
|
|
||||||
eventToItem: (event: TrustedEvent) => (event.tags.length > 1 ? readUserList(event) : null),
|
|
||||||
itemToEvent: (list: List) => list.event,
|
|
||||||
})
|
|
||||||
```
|
|
||||||
Generated
+18
@@ -222,6 +222,9 @@ importers:
|
|||||||
'@welshman/util':
|
'@welshman/util':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../util
|
version: link:../util
|
||||||
|
trava:
|
||||||
|
specifier: ^1.2.1
|
||||||
|
version: 1.2.1
|
||||||
devDependencies:
|
devDependencies:
|
||||||
rimraf:
|
rimraf:
|
||||||
specifier: ~6.0.0
|
specifier: ~6.0.0
|
||||||
@@ -306,6 +309,15 @@ importers:
|
|||||||
specifier: ~5.8.0
|
specifier: ~5.8.0
|
||||||
version: 5.8.2
|
version: 5.8.2
|
||||||
|
|
||||||
|
packages/schema:
|
||||||
|
devDependencies:
|
||||||
|
rimraf:
|
||||||
|
specifier: ~6.0.0
|
||||||
|
version: 6.0.1
|
||||||
|
typescript:
|
||||||
|
specifier: ~5.8.0
|
||||||
|
version: 5.8.2
|
||||||
|
|
||||||
packages/signer:
|
packages/signer:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@noble/curves':
|
'@noble/curves':
|
||||||
@@ -2258,6 +2270,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
|
||||||
engines: {node: '>=8.0'}
|
engines: {node: '>=8.0'}
|
||||||
|
|
||||||
|
trava@1.2.1:
|
||||||
|
resolution: {integrity: sha512-2dt4qJEVtYIwCoihoO+HfVLN+uid7ibtKnZ4pjLF4YqyDOwGuP0uTGLTUnvNkAp92qh3YmgRv4wojxnj5yBPAQ==}
|
||||||
|
engines: {node: '>=6.0.0', npm: '>=4.0.0'}
|
||||||
|
|
||||||
trim-lines@3.0.1:
|
trim-lines@3.0.1:
|
||||||
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
|
||||||
|
|
||||||
@@ -4400,6 +4416,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
is-number: 7.0.0
|
is-number: 7.0.0
|
||||||
|
|
||||||
|
trava@1.2.1: {}
|
||||||
|
|
||||||
trim-lines@3.0.1: {}
|
trim-lines@3.0.1: {}
|
||||||
|
|
||||||
ts-api-utils@2.1.0(typescript@5.8.2):
|
ts-api-utils@2.1.0(typescript@5.8.2):
|
||||||
|
|||||||
Reference in New Issue
Block a user