From 7982bebb35654be99dd50b58ff3bb5a51ce498c1 Mon Sep 17 00:00:00 2001 From: Jon Staab Date: Thu, 5 Dec 2024 15:30:39 -0800 Subject: [PATCH] Fix some tag utilities, re-work content rendering --- package-lock.json | 1 + packages/content/package.json | 1 + packages/content/src/index.ts | 150 +++++++++++++++++++++------------- packages/util/src/List.ts | 8 +- packages/util/src/Tags.ts | 4 +- 5 files changed, 99 insertions(+), 65 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88d833b..9d64c0c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3664,6 +3664,7 @@ "license": "MIT", "dependencies": { "@braintree/sanitize-url": "^7.0.2", + "@welshman/lib": "^0.0.28", "nostr-tools": "^2.7.2" }, "devDependencies": { diff --git a/packages/content/package.json b/packages/content/package.json index 06fc0e2..db6c76a 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -32,6 +32,7 @@ }, "dependencies": { "@braintree/sanitize-url": "^7.0.2", + "@welshman/lib": "^0.0.28", "nostr-tools": "^2.7.2" } } diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index d67dd19..1edf01a 100644 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -419,97 +419,129 @@ export const truncate = ( return content } -// Renderers +// Renderer -export type RenderOptions = { - entityBaseUrl?: string -} +export class Renderer { + private value = "" -export const defaultRenderOptions = { - entityBaseUrl: 'https://njump.me/' -} - -export class HTML { - constructor(readonly value: string) { - this.value = value - } + constructor(readonly options: RenderOptions) {} toString = () => this.value - static useDangerously = (value: string) => new HTML(value) - - static useSafely = (value: string) => { + addText = (value: string) => { const element = document.createElement('div') element.innerText = value - return new HTML(element.innerHTML) + this.value += element.innerHTML } - static buildLink = (href: string, display: string) => { + addNewlines = (count: number) => { + for (let i = 0; i < count; i++) { + this.value += this.options.newline + } + } + + addLink = (href: string, display: string) => { + this.value += this.options.renderLink(href, display) + } + + addEntityLink = (entity: string) => { + this.addLink(this.options.entityBase + entity, this.options.renderEntity(entity)) + } +} + +export type RenderOptions = { + newline: string + entityBase: string + renderLink: (href: string, display: string) => string + renderEntity: (entity: string) => string +} + +export const textRenderOptions = { + newline: '\n', + entityBase: '', + renderLink: (href: string, display: string) => href, + renderEntity: (entity: string) => entity.slice(0, 16) + '…', +} + +export const htmlRenderOptions = { + newline: '\n', + entityBase: 'https://njump.me/', + renderLink: (href: string, display: string) => { const element = document.createElement('a') element.href = sanitizeUrl(href) element.target = "_blank" element.innerText = display - return HTML.useDangerously(element.outerHTML) - } - - static buildEntityLink = (entity: string, options: RenderOptions) => - HTML.buildLink(options.entityBaseUrl + entity, entity.slice(0, 16) + '…') + return element.outerHTML + }, + renderEntity: (entity: string) => entity.slice(0, 16) + '…', } -export const renderCashu = (parsed: ParsedCashu, options: RenderOptions) => - HTML.useSafely(parsed.value) +export const makeTextRenderer = (options: Partial = {}) => + new Renderer({...textRenderOptions, ...options}) -export const renderCode = (parsed: ParsedCode, options: RenderOptions) => - HTML.useSafely(parsed.value) +export const makeHtmlRenderer = (options: Partial = {}) => + new Renderer({...htmlRenderOptions, ...options}) -export const renderEllipsis = (parsed: ParsedEllipsis, options: RenderOptions) => "…" +// Top level render methods -export const renderInvoice = (parsed: ParsedInvoice, options: RenderOptions) => - HTML.useSafely(parsed.value) +export const renderCashu = (p: ParsedCashu, r: Renderer) => r.addText(p.value) -export const renderLink = (parsed: ParsedLink, options: RenderOptions) => { - const href = parsed.value.url.toString() - const display = parsed.value.url.host + parsed.value.url.pathname +export const renderCode = (p: ParsedCode, r: Renderer) => r.addText(p.value) - return HTML.buildLink(href, display) -} +export const renderEllipsis = (p: ParsedEllipsis, r: Renderer) => "…" -export const renderNewline = (parsed: ParsedNewline, options: RenderOptions) => - HTML.useSafely(Array.from(parsed.value).map(() => '
').join('')) +export const renderInvoice = (p: ParsedInvoice, r: Renderer) => r.addText(p.value) -export const renderText = (parsed: ParsedText, options: RenderOptions) => - HTML.useSafely(parsed.value) +export const renderLink = (p: ParsedLink, r: Renderer) => + r.addLink(p.value.url.toString(), p.value.url.host + p.value.url.pathname) -export const renderTopic = (parsed: ParsedTopic, options: RenderOptions) => - HTML.useSafely(parsed.value) +export const renderNewline = (p: ParsedNewline, r: Renderer) => r.addNewlines(Array.from(p.value).length) -export const renderEvent = (parsed: ParsedEvent, options: RenderOptions) => - HTML.buildEntityLink(nip19.neventEncode(parsed.value), options) +export const renderText = (p: ParsedText, r: Renderer) => r.addText(p.value) -export const renderProfile = (parsed: ParsedProfile, options: RenderOptions) => - HTML.buildEntityLink(nip19.nprofileEncode(parsed.value), options) +export const renderTopic = (p: ParsedTopic, r: Renderer) => r.addText(p.value) -export const renderAddress = (parsed: ParsedAddress, options: RenderOptions) => - HTML.buildEntityLink(nip19.naddrEncode(parsed.value), options) +export const renderEvent = (p: ParsedEvent, r: Renderer) => r.addEntityLink(nip19.neventEncode(p.value)) -export const render = (parsed: Parsed, options: RenderOptions = {}) => { - options = {...defaultRenderOptions, ...options} +export const renderProfile = (p: ParsedProfile, r: Renderer) => r.addEntityLink(nip19.nprofileEncode(p.value)) +export const renderAddress = (p: ParsedAddress, r: Renderer) => r.addEntityLink(nip19.naddrEncode(p.value)) + +export const renderOne = (parsed: Parsed, renderer: Renderer) => { switch (parsed.type) { - case ParsedType.Address: return renderAddress(parsed as ParsedAddress, options) - case ParsedType.Cashu: return renderCashu(parsed as ParsedCashu, options) - case ParsedType.Code: return renderCode(parsed as ParsedCode, options) - case ParsedType.Ellipsis: return renderEllipsis(parsed as ParsedEllipsis, options) - case ParsedType.Event: return renderEvent(parsed as ParsedEvent, options) - case ParsedType.Invoice: return renderInvoice(parsed as ParsedInvoice, options) - case ParsedType.Link: return renderLink(parsed as ParsedLink, options) - case ParsedType.Newline: return renderNewline(parsed as ParsedNewline, options) - case ParsedType.Profile: return renderProfile(parsed as ParsedProfile, options) - case ParsedType.Text: return renderText(parsed as ParsedText, options) - case ParsedType.Topic: return renderTopic(parsed as ParsedTopic, options) + case ParsedType.Address: renderAddress(parsed as ParsedAddress, renderer); break + case ParsedType.Cashu: renderCashu(parsed as ParsedCashu, renderer); break + case ParsedType.Code: renderCode(parsed as ParsedCode, renderer); break + case ParsedType.Ellipsis: renderEllipsis(parsed as ParsedEllipsis, renderer); break + case ParsedType.Event: renderEvent(parsed as ParsedEvent, renderer); break + case ParsedType.Invoice: renderInvoice(parsed as ParsedInvoice, renderer); break + case ParsedType.Link: renderLink(parsed as ParsedLink, renderer); break + case ParsedType.Newline: renderNewline(parsed as ParsedNewline, renderer); break + case ParsedType.Profile: renderProfile(parsed as ParsedProfile, renderer); break + case ParsedType.Text: renderText(parsed as ParsedText, renderer); break + case ParsedType.Topic: renderTopic(parsed as ParsedTopic, renderer); break } + + return renderer } + +export const renderMany = (parsed: Parsed[], renderer: Renderer) => { + for (const p of parsed) { + renderOne(p, renderer) + } + + return renderer +} + +export const render = (parsed: Parsed | Parsed[], renderer: Renderer) => + Array.isArray(parsed) ? renderMany(parsed, renderer) : renderOne(parsed, renderer) + +export const renderAsText = (parsed: Parsed | Parsed[], options: Partial = {}) => + render(parsed, makeTextRenderer(options)) + +export const renderAsHtml = (parsed: Parsed | Parsed[], options: Partial = {}) => + render(parsed, makeHtmlRenderer(options)) diff --git a/packages/util/src/List.ts b/packages/util/src/List.ts index f65f512..f6dbf32 100644 --- a/packages/util/src/List.ts +++ b/packages/util/src/List.ts @@ -63,23 +63,23 @@ export const removeFromListByPredicate = (list: List, pred: (t: string[]) => boo export const removeFromList = (list: List, value: string) => removeFromListByPredicate(list, nthEq(1, value)) -export const addToListPublicly = (list: List, tag: string[]) => { +export const addToListPublicly = (list: List, ...tags: string[][]) => { const template = { kind: list.kind, content: list.event?.content || "", - tags: uniqTags(append(tag, list.publicTags)), + tags: uniqTags([...list.publicTags, ...tags]), } return new Encryptable(template, {}) } -export const addToListPrivately = (list: List, tag: string[]) => { +export const addToListPrivately = (list: List, ...tags: string[][]) => { const template = { kind: list.kind, tags: list.publicTags, } return new Encryptable(template, { - content: JSON.stringify(uniqTags(append(tag, list.privateTags))), + content: JSON.stringify(uniqTags([...list.privateTags, ...tags])), }) } diff --git a/packages/util/src/Tags.ts b/packages/util/src/Tags.ts index 5b350e1..3d6364e 100644 --- a/packages/util/src/Tags.ts +++ b/packages/util/src/Tags.ts @@ -194,12 +194,12 @@ export const getTopicTags = (tags: string[][]) => tags.filter(t => ["t"].include export const getTopicTagValues = (tags: string[][]) => getTopicTags(tags).map(nth(1)) export const getRelayTags = (tags: string[][]) => - tags.filter(t => ["r", "relay"].includes(t[0]) && isRelayUrl(t[1])) + tags.filter(t => ["r", "relay"].includes(t[0]) && isRelayUrl(t[1] || "")) export const getRelayTagValues = (tags: string[][]) => getRelayTags(tags).map(nth(1)) export const getGroupTags = (tags: string[][]) => - tags.filter(t => ["h", "group"].includes(t[0]) && t[1] && isRelayUrl(t[2])) + tags.filter(t => ["h", "group"].includes(t[0]) && t[1] && isRelayUrl(t[2] || "")) export const getGroupTagValues = (tags: string[][]) => getGroupTags(tags).map(nth(1))