From 0896623253c53ef9566e5042693f35436555d108 Mon Sep 17 00:00:00 2001 From: Jonathan Staab Date: Thu, 30 Mar 2023 08:49:12 -0500 Subject: [PATCH] Clean up and improve content parsing --- src/util/html.ts | 11 ++-- src/views/notes/Note.svelte | 16 ++---- src/views/notes/NoteContent.svelte | 87 ++++++++++++++++++++++++++++++ 3 files changed, 95 insertions(+), 19 deletions(-) create mode 100644 src/views/notes/NoteContent.svelte diff --git a/src/util/html.ts b/src/util/html.ts index 2ccfc041..12cc4153 100644 --- a/src/util/html.ts +++ b/src/util/html.ts @@ -114,14 +114,11 @@ export const fromParentOffset = (element, offset): [HTMLElement, number] => { } export const extractUrls = content => { - const regex = /(https?:\/\/)?[-a-z0-9@:%._\+~#=\.]+\.[a-z]{1,6}[-a-z0-9@:%_\+.~#?!&//=;]*/gi - const urls = content.match(regex) + const regex = /((http|ws)s?:\/\/)?[-a-z0-9@:%_\+~#=\.]+\.[a-z]{1,6}[-a-z0-9:%_\+~#\?!&\/=;\.]*/gi + const urls = content.match(regex) || [] - return ( - (urls || []) - // Skip decimals like 3.5 and ellipses which have more than one dot in a row - .filter(url => !url.match(/^[\d\.]+$/) && !url.match(/\.{2}/)) - ) + // Skip stuff like 3.5 or U.S. and ellipses which have more than one dot in a row + return urls.filter(url => !url.match(/^[.\.]+$/) && !url.match(/\.{2}/)) } export const renderContent = content => { diff --git a/src/views/notes/Note.svelte b/src/views/notes/Note.svelte index c8aa2aca..e459eed3 100644 --- a/src/views/notes/Note.svelte +++ b/src/views/notes/Note.svelte @@ -8,7 +8,7 @@ import {quantify} from "hurdak/lib/hurdak" import {Tags, findRootId, findReplyId, displayPerson, isLike} from "src/util/nostr" import {formatTimestamp, now, tryJson, formatSats, fetchJson} from "src/util/misc" - import {extractUrls, isMobile} from "src/util/html" + import {isMobile} from "src/util/html" import {invoiceAmount} from "src/util/lightning" import QRCode from "src/partials/QRCode.svelte" import ImageInput from "src/partials/ImageInput.svelte" @@ -17,12 +17,12 @@ import Content from "src/partials/Content.svelte" import PersonSummary from "src/views/person/PersonSummary.svelte" import Popover from "src/partials/Popover.svelte" + import PersonCircle from "src/partials/PersonCircle.svelte" import RelayCard from "src/views/relays/RelayCard.svelte" import Modal from "src/partials/Modal.svelte" import Preview from "src/partials/Preview.svelte" import Anchor from "src/partials/Anchor.svelte" import {toast, modal} from "src/app/ui" - import {renderNote} from "src/app" import Compose from "src/partials/Compose.svelte" import Card from "src/partials/Card.svelte" import user from "src/agent/user" @@ -35,7 +35,7 @@ import cmd from "src/agent/cmd" import {routes} from "src/app/ui" import {publishWithToast} from "src/app" - import PersonCircle from "src/partials/PersonCircle.svelte" + import NoteContent from "src/views/notes/NoteContent.svelte" export let note export let depth = 0 @@ -58,7 +58,6 @@ const {profile, canPublish, mutes} = user const timestamp = formatTimestamp(note.created_at) const borderColor = invertColors ? "gray-6" : "gray-7" - const links = extractUrls(note.content) const showEntire = anchorId === note.id const interactive = !anchorId || !showEntire const person = watch("people", () => getPersonWithFallback(note.pubkey)) @@ -402,14 +401,7 @@ You have muted this note.

{:else} -
-

{@html renderNote(note, {showEntire})}

- {#if user.getSetting("showMedia") && links.length > 0} - - {/if} -
+ {/if}
+ import {last, uniq, trim} from "ramda" + import {doPipe, ellipsize} from "hurdak/lib/hurdak" + import {extractUrls, escapeHtml} from "src/util/html" + import {displayPerson} from "src/util/nostr" + import Preview from "src/partials/Preview.svelte" + import user from "src/agent/user" + import {getPersonWithFallback} from "src/agent/tables" + import {routes} from "src/app/ui" + + const canPreview = url => url.match("\.(jpg|jpeg|png|gif|mov|mp4)") + + export let note + export let showEntire + + const links = uniq(extractUrls(note.content)) + const content = doPipe(note.content, [ + trim, + escapeHtml, + c => (showEntire || c.length < 800 ? c : ellipsize(c, 400)), + c => { + // Pad content with whitespace to simplify our regular expressions + c = `
${c}
` + + for (let url of links) { + // It's common for punctuation to end a url, trim it off + if (url.match(/[\.\?,:]$/)) { + url = url.slice(0, -1) + } + + const href = url.includes("://") ? url : "https://" + url + const display = url.replace(/(http|ws)s?:\/\/(www\.)?/, "").replace(/[\.\/?;,:]$/, "") + const escaped = url.replace(/([.*+?^${}()|[\]\\])/g, "\\$1") + const wsRegex = new RegExp(`
${escaped}
`, "g") + const slashRegex = new RegExp(`\/${escaped}`, "g") + + // Skip stuff that's just at the end of a filepath + if (c.match(slashRegex)) { + continue + } + + // If the url is on its own line, remove it entirely + if (c.match(wsRegex) && canPreview(url)) { + c = c.replace(wsRegex, '') + continue + } + + // Avoid matching urls inside quotes to avoid double-replacing + const quoteRegex = new RegExp(`([^"]*)(${escaped})([^"]*)`, "g") + + const $a = document.createElement("a") + + $a.href = href + $a.target = "_blank" + $a.className = "underline" + $a.innerText = ellipsize(display, 50) + + c = c.replace(quoteRegex, `$1${$a.outerHTML}$3`) + } + + return c.trim() + }, + // Mentions + c => + c.replace(/#\[(\d+)\]/g, (tag, i) => { + if (!note.tags[parseInt(i)]) { + return tag + } + + const pubkey = note.tags[parseInt(i)][1] + const person = getPersonWithFallback(pubkey) + const name = displayPerson(person) + const path = routes.person(pubkey) + + return `@${name}` + }), + ]) + + +
+

{@html content}

+ {#if user.getSetting("showMedia") && links.length > 0} + + {/if} +