Add clickable entities to tweet text
This commit is contained in:
parent
11c94511d5
commit
f8cd326440
46
internal/webserver/helpers_test.go
Normal file
46
internal/webserver/helpers_test.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package webserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
// "fmt"
|
||||||
|
// "net/http"
|
||||||
|
// "net/http/httptest"
|
||||||
|
// "net/url"
|
||||||
|
// "strings"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetEntitiesNone(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
s := "This is just a simple string"
|
||||||
|
entities := get_entities(s)
|
||||||
|
|
||||||
|
require.Len(entities, 1)
|
||||||
|
assert.Equal(ENTITY_TYPE_TEXT, entities[0].EntityType)
|
||||||
|
assert.Equal(s, entities[0].Contents)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetEntitiesHashtagAndMention(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
s := "A string with a #hashtag and a @mention in it"
|
||||||
|
entities := get_entities(s)
|
||||||
|
|
||||||
|
require.Len(entities, 5)
|
||||||
|
assert.Equal(entities[0].EntityType, ENTITY_TYPE_TEXT)
|
||||||
|
assert.Equal(entities[0].Contents, "A string with a ")
|
||||||
|
assert.Equal(entities[1].EntityType, ENTITY_TYPE_HASHTAG)
|
||||||
|
assert.Equal(entities[1].Contents, "hashtag")
|
||||||
|
assert.Equal(entities[2].EntityType, ENTITY_TYPE_TEXT)
|
||||||
|
assert.Equal(entities[2].Contents, " and a ")
|
||||||
|
assert.Equal(entities[3].EntityType, ENTITY_TYPE_MENTION)
|
||||||
|
assert.Equal(entities[3].Contents, "mention")
|
||||||
|
assert.Equal(entities[4].EntityType, ENTITY_TYPE_TEXT)
|
||||||
|
assert.Equal(entities[4].Contents, " in it")
|
||||||
|
}
|
@ -9,6 +9,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
@ -91,6 +92,7 @@ func (app *Application) buffered_render_tweet_page(w http.ResponseWriter, tpl_fi
|
|||||||
"space": data.Space,
|
"space": data.Space,
|
||||||
"active_user": app.get_active_user,
|
"active_user": app.get_active_user,
|
||||||
"focused_tweet_id": data.FocusedTweetID,
|
"focused_tweet_id": data.FocusedTweetID,
|
||||||
|
"get_entities": get_entities,
|
||||||
}),
|
}),
|
||||||
Filenames: append(partials, get_filepath(tpl_file)),
|
Filenames: append(partials, get_filepath(tpl_file)),
|
||||||
TplName: "base",
|
TplName: "base",
|
||||||
@ -124,6 +126,7 @@ func (app *Application) buffered_render_tweet_htmx(w http.ResponseWriter, tpl_na
|
|||||||
"space": data.Space,
|
"space": data.Space,
|
||||||
"active_user": app.get_active_user,
|
"active_user": app.get_active_user,
|
||||||
"focused_tweet_id": data.FocusedTweetID,
|
"focused_tweet_id": data.FocusedTweetID,
|
||||||
|
"get_entities": get_entities,
|
||||||
}),
|
}),
|
||||||
Filenames: partials,
|
Filenames: partials,
|
||||||
TplName: tpl_name,
|
TplName: tpl_name,
|
||||||
@ -148,6 +151,41 @@ func (app *Application) get_active_user() scraper.User {
|
|||||||
return app.ActiveUser
|
return app.ActiveUser
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EntityType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
ENTITY_TYPE_TEXT EntityType = iota
|
||||||
|
ENTITY_TYPE_MENTION
|
||||||
|
ENTITY_TYPE_HASHTAG
|
||||||
|
)
|
||||||
|
|
||||||
|
type Entity struct {
|
||||||
|
EntityType
|
||||||
|
Contents string
|
||||||
|
}
|
||||||
|
|
||||||
|
func get_entities(text string) []Entity {
|
||||||
|
ret := []Entity{}
|
||||||
|
start := 0
|
||||||
|
for _, idxs := range regexp.MustCompile(`[@#]\w+`).FindAllStringIndex(text, -1) {
|
||||||
|
if start != idxs[0] {
|
||||||
|
ret = append(ret, Entity{ENTITY_TYPE_TEXT, text[start:idxs[0]]})
|
||||||
|
}
|
||||||
|
piece := text[idxs[0]+1 : idxs[1]] // Chop off the "#" or "@"
|
||||||
|
if text[idxs[0]] == '@' {
|
||||||
|
ret = append(ret, Entity{ENTITY_TYPE_MENTION, piece})
|
||||||
|
} else {
|
||||||
|
ret = append(ret, Entity{ENTITY_TYPE_HASHTAG, piece})
|
||||||
|
}
|
||||||
|
start = idxs[1]
|
||||||
|
}
|
||||||
|
if start < len(text) {
|
||||||
|
ret = append(ret, Entity{ENTITY_TYPE_TEXT, text[start:]})
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
func func_map(extras template.FuncMap) template.FuncMap {
|
func func_map(extras template.FuncMap) template.FuncMap {
|
||||||
ret := sprig.FuncMap()
|
ret := sprig.FuncMap()
|
||||||
for i := range extras {
|
for i := range extras {
|
||||||
|
@ -318,6 +318,24 @@ func TestTweetsWithContent(t *testing.T) {
|
|||||||
assert.Len(cascadia.QueryAll(root, selector("ul.space-participants-list li")), 9)
|
assert.Len(cascadia.QueryAll(root, selector("ul.space-participants-list li")), 9)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTweetWithEntities(t *testing.T) {
|
||||||
|
assert := assert.New(t)
|
||||||
|
require := require.New(t)
|
||||||
|
|
||||||
|
resp := do_request(httptest.NewRequest("GET", "/tweet/1489944024278523906", nil))
|
||||||
|
require.Equal(resp.StatusCode, 200)
|
||||||
|
root, err := html.Parse(resp.Body)
|
||||||
|
require.NoError(err)
|
||||||
|
entities := cascadia.QueryAll(root, selector(".entity"))
|
||||||
|
assert.Len(entities, 2)
|
||||||
|
assert.Equal(entities[0].Data, "a")
|
||||||
|
assert.Equal(entities[0].FirstChild.Data, "@gofundme")
|
||||||
|
assert.Contains(entities[0].Attr, html.Attribute{Key: "href", Val: "/gofundme"})
|
||||||
|
assert.Equal(entities[1].Data, "a")
|
||||||
|
assert.Equal(entities[1].FirstChild.Data, "#BankruptGoFundMe")
|
||||||
|
assert.Contains(entities[1].Attr, html.Attribute{Key: "href", Val: "/search/%23BankruptGoFundMe"})
|
||||||
|
}
|
||||||
|
|
||||||
func TestLongTweet(t *testing.T) {
|
func TestLongTweet(t *testing.T) {
|
||||||
assert := assert.New(t)
|
assert := assert.New(t)
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
@ -51,8 +51,19 @@
|
|||||||
<span class="vertical-container-1">
|
<span class="vertical-container-1">
|
||||||
<div class="tweet-content">
|
<div class="tweet-content">
|
||||||
{{range (splitList "\n" $main_tweet.Text)}}
|
{{range (splitList "\n" $main_tweet.Text)}}
|
||||||
<p class="tweet-text">
|
<p class="tweet-text" hx-trigger="click consume">
|
||||||
{{.}}
|
{{range (get_entities .)}}
|
||||||
|
{{if (eq .EntityType 1)}}
|
||||||
|
<!-- Mention -->
|
||||||
|
<a class="entity" href="/{{.Contents}}">@{{.Contents}}</a>
|
||||||
|
{{else if (eq .EntityType 2)}}
|
||||||
|
<!-- Hashtag -->
|
||||||
|
<a class="entity" href="/search/%23{{.Contents}}">#{{.Contents}}</a>
|
||||||
|
{{else}}
|
||||||
|
<!-- Just text -->
|
||||||
|
{{.Contents}}
|
||||||
|
{{end}}
|
||||||
|
{{end}}
|
||||||
</p>
|
</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
@ -114,7 +125,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="dummy"></div>
|
<div class="dummy"></div>
|
||||||
<div class="dropdown" hx-trigger="click consume">
|
<div class="dropdown" hx-trigger="click consume">
|
||||||
<button class="dropdown-button">
|
<button class="dropdown-button" title="Options">
|
||||||
<img class="svg-icon" src="/static/icons/more.svg" />
|
<img class="svg-icon" src="/static/icons/more.svg" />
|
||||||
</button>
|
</button>
|
||||||
<ul class="dropdown-items">
|
<ul class="dropdown-items">
|
||||||
|
Loading…
x
Reference in New Issue
Block a user