From f8cd3264403b15e63a9ada42c16c75209e188076 Mon Sep 17 00:00:00 2001 From: Alessio Date: Sun, 27 Aug 2023 15:09:35 -0300 Subject: [PATCH] Add clickable entities to tweet text --- internal/webserver/helpers_test.go | 46 +++++++++++++++++++ internal/webserver/response_helpers.go | 38 +++++++++++++++ internal/webserver/server_test.go | 18 ++++++++ .../tpl/tweet_page_includes/single_tweet.tpl | 17 +++++-- 4 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 internal/webserver/helpers_test.go diff --git a/internal/webserver/helpers_test.go b/internal/webserver/helpers_test.go new file mode 100644 index 0000000..37ef41f --- /dev/null +++ b/internal/webserver/helpers_test.go @@ -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") +} diff --git a/internal/webserver/response_helpers.go b/internal/webserver/response_helpers.go index b189e69..12994c9 100644 --- a/internal/webserver/response_helpers.go +++ b/internal/webserver/response_helpers.go @@ -9,6 +9,7 @@ import ( "net/http" "path" "path/filepath" + "regexp" "runtime" "runtime/debug" @@ -91,6 +92,7 @@ func (app *Application) buffered_render_tweet_page(w http.ResponseWriter, tpl_fi "space": data.Space, "active_user": app.get_active_user, "focused_tweet_id": data.FocusedTweetID, + "get_entities": get_entities, }), Filenames: append(partials, get_filepath(tpl_file)), TplName: "base", @@ -124,6 +126,7 @@ func (app *Application) buffered_render_tweet_htmx(w http.ResponseWriter, tpl_na "space": data.Space, "active_user": app.get_active_user, "focused_tweet_id": data.FocusedTweetID, + "get_entities": get_entities, }), Filenames: partials, TplName: tpl_name, @@ -148,6 +151,41 @@ func (app *Application) get_active_user() scraper.User { 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 { ret := sprig.FuncMap() for i := range extras { diff --git a/internal/webserver/server_test.go b/internal/webserver/server_test.go index f6a7544..5eb7709 100644 --- a/internal/webserver/server_test.go +++ b/internal/webserver/server_test.go @@ -318,6 +318,24 @@ func TestTweetsWithContent(t *testing.T) { 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) { assert := assert.New(t) require := require.New(t) diff --git a/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl b/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl index d3c6e19..c645940 100644 --- a/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl +++ b/internal/webserver/tpl/tweet_page_includes/single_tweet.tpl @@ -51,8 +51,19 @@
{{range (splitList "\n" $main_tweet.Text)}} -

- {{.}} +

+ {{range (get_entities .)}} + {{if (eq .EntityType 1)}} + + @{{.Contents}} + {{else if (eq .EntityType 2)}} + + #{{.Contents}} + {{else}} + + {{.Contents}} + {{end}} + {{end}}

{{end}} @@ -114,7 +125,7 @@