diff --git a/go.mod b/go.mod index f75347c..458ea53 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,9 @@ require ( github.com/nbd-wtf/go-nostr v0.20.0 github.com/pelletier/go-toml v1.9.5 github.com/rs/zerolog v1.29.1 + golang.org/x/exp v0.0.0-20221106115401-f9659909a136 golang.org/x/image v0.0.0-20190802002840-cff245a6509b + mvdan.cc/xurls/v2 v2.5.0 ) require ( @@ -42,7 +44,6 @@ require ( github.com/tidwall/gjson v1.14.4 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect - golang.org/x/exp v0.0.0-20221106115401-f9659909a136 // indirect golang.org/x/net v0.10.0 // indirect golang.org/x/sys v0.8.0 // indirect google.golang.org/protobuf v1.23.0 // indirect diff --git a/go.sum b/go.sum index 373e822..5de54f2 100644 --- a/go.sum +++ b/go.sum @@ -222,3 +222,5 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8= +mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE= diff --git a/main.go b/main.go index 3b3fefc..dc51771 100644 --- a/main.go +++ b/main.go @@ -76,7 +76,7 @@ func main() { } funcMap := template.FuncMap{ - "basicFormatting": basicFormatting, + "basicFormatting": func(input string) string { return basicFormatting(input, false, false) }, "previewNotesFormatting": previewNotesFormatting, "escapeString": html.EscapeString, "sanitizeXSS": sanitizeXSS, diff --git a/render.go b/render.go index 2e100cd..2eade30 100644 --- a/render.go +++ b/render.go @@ -308,7 +308,11 @@ func render(w http.ResponseWriter, r *http.Request) { if event.Kind == 30023 || event.Kind == 30024 { content = mdToHTML(content, typ == "telegram_instant_view") } else { - content = basicFormatting(renderQuotesAsHTML(r.Context(), html.EscapeString(content))) + // first we run basicFormatting, which turns URLs into their appropriate HTML tags + content = basicFormatting(html.EscapeString(content), true, false) + // then we render quotes as HTML, which will also apply basicFormatting to all the internal quotes + content = renderQuotesAsHTML(r.Context(), content, typ == "telegram_instant_view") + // we must do this because inside we must treat s different when telegram_instant_view } // pretty JSON diff --git a/utils.go b/utils.go index 777d01e..a9c849c 100644 --- a/utils.go +++ b/utils.go @@ -16,6 +16,7 @@ import ( mdhtml "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" "github.com/microcosm-cc/bluemonday" + "mvdan.cc/xurls/v2" "github.com/nbd-wtf/go-nostr" "github.com/nbd-wtf/go-nostr/nip10" @@ -25,12 +26,19 @@ import ( var ( urlSuffixMatcher = regexp.MustCompile(`[\w-_.]+\.[\w-_.]+(\/[\/\w]*)?$`) - nostrEveryMatcher = regexp.MustCompile(`\S*(nostr:)?((npub|note|nevent|nprofile|naddr)1[a-z0-9]+)\b`) - nostrNoteNeventMatcher = regexp.MustCompile(`\S*(nostr:)?((note|nevent)1[a-z0-9]+)\b`) - nostrNpubNprofileMatcher = regexp.MustCompile(`\S*(nostr:)?((npub|nprofile)1[a-z0-9]+)\b`) - hrefMatcher = regexp.MustCompile(`\S*(https?://\S+)\S*`) - imgsMatcher = regexp.MustCompile(`\S*(\()?(https?://\S+(\.jpg|\.jpeg|\.png|\.webp|\.gif))\S*`) - videoMatcher = regexp.MustCompile(`\S*(https?://\S+(\.mp4|\.ogg|\.webm|.mov))\S*`) + nostrEveryMatcher = regexp.MustCompile(`nostr:((npub|note|nevent|nprofile|naddr)1[a-z0-9]+)\b`) + nostrNoteNeventMatcher = regexp.MustCompile(`nostr:((note|nevent)1[a-z0-9]+)\b`) + nostrNpubNprofileMatcher = regexp.MustCompile(`nostr:((npub|nprofile)1[a-z0-9]+)\b`) + + urlMatcher = func() *regexp.Regexp { + // hack to only allow these schemes while still using this library + xurls.Schemes = []string{"https"} + xurls.SchemesNoAuthority = []string{"blob"} + xurls.SchemesUnofficial = []string{"http"} + return xurls.Strict() + }() + imageExtensionMatcher = regexp.MustCompile(`.*\.(png|jpg|jpeg|gif|webp)(\?.*)?$`) + videoExtensionMatcher = regexp.MustCompile(`.*\.(mp4|ogg|webm|mov)(\?.*)?$`) ) var kindNames = map[int]string{ @@ -212,47 +220,29 @@ func getParentNevent(event *nostr.Event) string { // Rendering functions // ### ### ### ### ### ### ### ### ### ### ### -func replaceImageURLsWithTags(input string, replacement string) string { - // Match and replace image URLs with a custom replacement - // Usually is html => ` ` - // or markdown !()[...] tags for further processing => `![](%s)` - input = imgsMatcher.ReplaceAllStringFunc(input, func(match string) string { - submatch := imgsMatcher.FindStringSubmatch(match) - if len(submatch) < 2 || - strings.Contains(submatch[0], "](") { // Markdown ![](...) image - return match +func replaceURLsWithTags(input string, imageReplacementTemplate, videoReplacementTemplate string) string { + return urlMatcher.ReplaceAllStringFunc(input, func(match string) string { + switch { + case imageExtensionMatcher.MatchString(match): + // Match and replace image URLs with a custom replacement + // Usually is html => ` ` + // or markdown !()[...] tags for further processing => `![](%s)` + return fmt.Sprintf(imageReplacementTemplate, match) + case videoExtensionMatcher.MatchString(match): + // Match and replace video URLs with a custom replacement + // Usually is html