Compare commits

...

18 Commits

Author SHA1 Message Date
httpjamesm bca87a89ad docs: new readme screenshots 2024-03-28 20:11:38 -04:00
httpjamesm 0f67fcf66f Merge branch 'main' of https://github.com/httpjamesm/AnonymousOverflow 2024-03-28 20:03:36 -04:00
httpjamesm 4c3996542c docs: version 1.12.0 2024-03-28 20:03:34 -04:00
httpjamesm a701810e11 fix: set font-src to 'self' on questions (#101) 2024-03-28 20:03:17 -04:00
Solomon 23b8ed8899 improvement: use lang-* classes declared in snippet markup (#97)
* improvement: use lang-* classes declared in snippet markup

* fix: don't require snippet lang classes in code blocks

* fix: constrain greedy code block match
2024-03-28 19:59:28 -04:00
httpjamesm bf5300706e docs: #82 add snine 2024-03-25 13:09:36 -04:00
httpjamesm 1e659c550a docs: #75 update r4fo instance info + onion 2024-03-25 13:08:39 -04:00
httpjamesm 2ade374482 docs: #74 update bloatcat instance info 2024-03-25 13:07:29 -04:00
Matt Fellenz 94032f4f90 Implement external exchanges (#99)
* Implement external exchanges

* test: translateUrl test cases

* fix: double slash bug in translateUrl

---------

Co-authored-by: httpjamesm <github@httpjames.space>
2024-03-25 13:05:24 -04:00
Solomon 89126a7377 fix: keep HTML escaped (#96) 2024-03-25 12:47:59 -04:00
Nebula 0d7355bd46 Update whatever.social locations (#94) 2024-03-18 02:34:09 -04:00
httpjamesm f7209802ce Merge branch 'origin/main' 2024-03-09 12:10:29 -05:00
httpjamesm 880f27b786 docs: version 1.11.0 2024-03-09 12:10:18 -05:00
httpjamesm 4c62f9bc4f feat: allow answerId in shorthand overflow URL (#91) 2024-03-09 12:09:17 -05:00
httpjamesm e82646635e Replace StackOverflow Links (#90)
* feat: replace stackoverflow and exchange links

* fix: replace stackoverflow.com links with path

* feat: run stack overflow link replacer on process

* feat: process HTML on comment text
2024-03-09 12:06:41 -05:00
httpjamesm ff66f41f47 fix: derive currentUrl from actual page path (#89) 2024-03-09 11:28:45 -05:00
httpjamesm 01b960cd43 fix: add stackoverflow flex classes (#88) 2024-03-09 11:20:10 -05:00
ngn 0058aea03b add ao.ngn.tf to README (#83) 2024-03-09 11:16:32 -05:00
19 changed files with 251 additions and 77 deletions
+38 -35
View File
@@ -6,49 +6,52 @@ This project is super lightweight by design. The UI is simple and the frontend i
## Screenshots
![Home](https://files.horizon.pics/e2b9275c-1409-4978-801b-de981a8d3ae9?a=1&mime1=image&mime2=png)
![Home](./docs/screenshots/home_dark.webp)
![Question](https://files.horizon.pics/0f6b0036-87f0-4acd-9a0f-936b5c397a73?a=1&mime1=image&mime2=png)
![Question](./docs/screenshots/question_dark.webp)
![Answer](https://files.horizon.pics/861ec510-644b-43f2-9439-0a2cae841422?a=1&mime1=image&mime2=png)
![Answer](./docs/screenshots/answers_light.webp)
## Clearnet Instances
| Instance URL | Region | Notes |
| ----------------------------------------------------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------ |
| [code.whatever.social](https://code.whatever.social) | United States & The Netherlands | Operated by [Whatever Social](https://whatever.social) and [http.james](https://httpjames.space) |
| [ao.vern.cc](https://ao.vern.cc) | United States | Operated by [vern.cc](https://vern.cc) |
| [overflow.smnz.de](https://overflow.smnz.de) | Germany | Operated by [smnz.de](https://smnz.de) |
| [overflow.lunar.icu](https://overflow.lunar.icu) | Germany | Operated by [lunar.icu](https://lunar.icu/) |
| [overflow.adminforge.de](https://overflow.adminforge.de/) | Germany | Operated by [adminForge](https://adminforge.de/) |
| [overflow.hostux.net](https://overflow.hostux.net/) | France | Operated by [Hostux](https://hostux.net/) |
| [overflow.projectsegfau.lt](https://overflow.projectsegfau.lt/) | United States, France, India | Operated by [Project Segfault](https://projectsegfau.lt/) |
| [code.xbdm.fun](https://code.xbdm.fun) | Germany | Operated by [xbdm.fun](https://xbdm.fun) |
| [overflow.fascinated.cc](https://overflow.fascinated.cc/) | Germany | Operated by [fascinated.cc](https://fascinated.cc/) |
| [ao.bloatcat.tk](https://ao.bloatcat.tk) | Iceland | Operated by [bloatcat.tk](https://bloatcat.tk) |
| [anonoverflow.frontendfriendly.xyz](https://anonoverflow.frontendfriendly.xyz/) | United States | Operated by [frontendfriendly.xyz](https://frontendfriendly.xyz/) |
| [ao.owo.si](https://ao.owo.si/) | Germany | Operated by [owo.si](https://owo.si/) |
| [overflow.datura.network](https://overflow.datura.network/) | Germany | Operated by [datura.network](https://datura.network) |
| [overflow.freedit.eu](overflow.freedit.eu) | United States | Operated by [freedit.eu](https://freedit.eu) |
| [ao.ftw.lol](https://ao.ftw.lol) | Germany | Operated by [ftw.lol](https://ftw.lol) |
| [anonoverflow.hyperreal.coffee](https://anonoverflow.hyperreal.coffee) | United States | Operated by [hyperreal.coffee](https://hyperreal.coffee) |
| [a.opnxng.com](a.opnxng.com) | Singapore | Operated by [opnxng.com](https://about.opnxng.com) |
| [overflow.sudovanilla.com](https://overflow.sudovanilla.com) | United States | Operated by [SudoVanilla](https://sudovanilla.com) |
| [anonymousoverflow.privacyfucking.rocks](https://anonymousoverflow.privacyfucking.rocks/) | Germany | Operated by [privacyfucking.rocks](https://privacyfucking.rocks) |
| [exchange.seitan-ayoub.lol](https://exchange.seitan-ayoub.lol) | Germany | Operated by [Seitan Ayoub](https://seitan-ayoub.lol) |
| [overflow.r4fo.com](https://overflow.r4fo.com) | The Netherlands | Operated by [r4fo.com](https://r4fo.com) |
| [overflow.ducks.party](https://overflow.ducks.party) | The Netherlands | Operated by [ducks.party](https://ducks.party) |
| Instance URL | Region | Notes |
| ----------------------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------ |
| [code.whatever.social](https://code.whatever.social) | Germany | Operated by [Whatever Social](https://whatever.social) and [http.james](https://httpjames.space) |
| [ao.vern.cc](https://ao.vern.cc) | United States | Operated by [vern.cc](https://vern.cc) |
| [overflow.smnz.de](https://overflow.smnz.de) | Germany | Operated by [smnz.de](https://smnz.de) |
| [overflow.lunar.icu](https://overflow.lunar.icu) | Germany | Operated by [lunar.icu](https://lunar.icu/) |
| [overflow.adminforge.de](https://overflow.adminforge.de/) | Germany | Operated by [adminForge](https://adminforge.de/) |
| [overflow.hostux.net](https://overflow.hostux.net/) | France | Operated by [Hostux](https://hostux.net/) |
| [overflow.projectsegfau.lt](https://overflow.projectsegfau.lt/) | United States, France, India | Operated by [Project Segfault](https://projectsegfau.lt/) |
| [code.xbdm.fun](https://code.xbdm.fun) | Germany | Operated by [xbdm.fun](https://xbdm.fun) |
| [overflow.fascinated.cc](https://overflow.fascinated.cc/) | Germany | Operated by [fascinated.cc](https://fascinated.cc/) |
| [ao.bloat.cat](https://ao.bloat.cat) | Romania | Operated by [bloatcat.tk](https://bloat.cat) |
| [anonoverflow.frontendfriendly.xyz](https://anonoverflow.frontendfriendly.xyz/) | United States | Operated by [frontendfriendly.xyz](https://frontendfriendly.xyz/) |
| [ao.owo.si](https://ao.owo.si/) | Germany | Operated by [owo.si](https://owo.si/) |
| [overflow.datura.network](https://overflow.datura.network/) | Germany | Operated by [datura.network](https://datura.network) |
| [overflow.freedit.eu](overflow.freedit.eu) | United States | Operated by [freedit.eu](https://freedit.eu) |
| [ao.ftw.lol](https://ao.ftw.lol) | Germany | Operated by [ftw.lol](https://ftw.lol) |
| [anonoverflow.hyperreal.coffee](https://anonoverflow.hyperreal.coffee) | United States | Operated by [hyperreal.coffee](https://hyperreal.coffee) |
| [a.opnxng.com](a.opnxng.com) | Singapore | Operated by [opnxng.com](https://about.opnxng.com) |
| [overflow.sudovanilla.com](https://overflow.sudovanilla.com) | United States | Operated by [SudoVanilla](https://sudovanilla.com) |
| [anonymousoverflow.privacyfucking.rocks](https://anonymousoverflow.privacyfucking.rocks/) | Germany | Operated by [privacyfucking.rocks](https://privacyfucking.rocks) |
| [exchange.seitan-ayoub.lol](https://exchange.seitan-ayoub.lol) | Germany | Operated by [Seitan Ayoub](https://seitan-ayoub.lol) |
| [overflow.r4fo.com](https://overflow.r4fo.com) | The Netherlands | Operated by [r4fo.com](https://r4fo.com) |
| [overflow.ducks.party](https://overflow.ducks.party) | The Netherlands | Operated by [ducks.party](https://ducks.party) |
| [ao.ngn.tf](https://ao.ngn.tf) | Turkey | Operated by [ngn](https://ngn.tf) |
| [overflow.snine.nl](https://overflow.snine.nl) | The Netherlands | Operated by [snine](https://snine.nl) |
## Other Instances
| Instance URL | Region | Notes |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | --------------------------------------------------------- |
| [ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | United States | Operated by [vern.cc](https://vern.cc) |
| [vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p](http://vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p) | United States | Operated by [vern.cc](https://vern.cc) |
| [overflow.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion](http://overflow.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/) | Luxembourg | Operated by [Project Segfault](https://projectsegfau.lt/) |
| [overflow.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion](http://overflow.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion/) | Germany | Operated by [datura.network](https://datura.network) |
| [ao.pk47sgwhncn5cgidm7bofngmh7lc7ukjdpk5bjwfemmyp27ovl25ikyd.onion](http://ao.pk47sgwhncn5cgidm7bofngmh7lc7ukjdpk5bjwfemmyp27ovl25ikyd.onion/) | Germany | Operated by [owo.si](https://owo.si/) |
| [ay7akchgdh76r4lc62hzd52z6xqoh67loototsetvqxo5o7ngo5q.b32.i2p](http://ay7akchgdh76r4lc62hzd52z6xqoh67loototsetvqxo5o7ngo5q.b32.i2p/) | Germany | Operated by [owo.si](https://owo.si/) |
| Instance URL | Region | Notes |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | --------------------------------------------------------- |
| [ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://ao.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion) | United States | Operated by [vern.cc](https://vern.cc) |
| [vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p](http://vernmzgraj6aaoafmehupvtkkynpaa67rxcdj2kinwiy6konn6rq.b32.i2p) | United States | Operated by [vern.cc](https://vern.cc) |
| [overflow.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion](http://overflow.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion/) | Luxembourg | Operated by [Project Segfault](https://projectsegfau.lt/) |
| [overflow.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion](http://overflow.daturab6drmkhyeia4ch5gvfc2f3wgo6bhjrv3pz6n7kxmvoznlkq4yd.onion/) | Germany | Operated by [datura.network](https://datura.network) |
| [ao.pk47sgwhncn5cgidm7bofngmh7lc7ukjdpk5bjwfemmyp27ovl25ikyd.onion](http://ao.pk47sgwhncn5cgidm7bofngmh7lc7ukjdpk5bjwfemmyp27ovl25ikyd.onion/) | Germany | Operated by [owo.si](https://owo.si/) |
| [ay7akchgdh76r4lc62hzd52z6xqoh67loototsetvqxo5o7ngo5q.b32.i2p](http://ay7akchgdh76r4lc62hzd52z6xqoh67loototsetvqxo5o7ngo5q.b32.i2p/) | Germany | Operated by [owo.si](https://owo.si/) |
| [overflow.r4focoma7gu2zdwwcjjad47ysxt634lg73sxmdbkdozanwqslho5ohyd.onion](http://overflow.r4focoma7gu2zdwwcjjad47ysxt634lg73sxmdbkdozanwqslho5ohyd.onion) | The Netherlands | Operated by [r4fo.com](https://r4fo.com) |
## Why use AnonymousOverflow over StackOverflow?
+1 -1
View File
@@ -1,3 +1,3 @@
package config
var Version = "1.10.1"
var Version = "1.12.0"
Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

+4
View File
@@ -13,6 +13,7 @@ require (
require (
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.0 // indirect
@@ -25,6 +26,8 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.0.6 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/ugorji/go/codec v1.2.8 // indirect
golang.org/x/crypto v0.4.0 // indirect
golang.org/x/net v0.7.0 // indirect
@@ -32,4 +35,5 @@ require (
golang.org/x/text v0.7.0 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+2
View File
@@ -72,6 +72,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/ugorji/go/codec v1.2.8 h1:sgBJS6COt0b/P40VouWKdseidkDgHxYGm0SAglUHfP0=
github.com/ugorji/go/codec v1.2.8/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
+1
View File
@@ -52,6 +52,7 @@ func main() {
r.GET("/a/:id", routes.RedirectShortenedOverflowURL)
r.GET("/q/:id", routes.RedirectShortenedOverflowURL)
r.GET("/q/:id/:answerId", routes.RedirectShortenedOverflowURL)
r.GET("/questions/:id", func(c *gin.Context) {
// redirect user to the question with the title
+12
View File
@@ -50,3 +50,15 @@ html {
details {
cursor: pointer;
}
.d-flex {
display: flex;
}
.fd-column {
flex-direction: column;
}
.fw-nowrap {
flex-wrap: nowrap;
}
+38 -20
View File
@@ -20,7 +20,41 @@ type urlConversionRequest struct {
URL string `form:"url" binding:"required"`
}
var stackExchangeRegex = regexp.MustCompile(`https://(.+).stackexchange.com/questions/`)
var coreRegex = regexp.MustCompile(`(?:https?://)?(?:www\.)?([^/]+)(/(?:questions|q|a)/.+)`)
// Will return `nil` if `rawUrl` is invalid.
func translateUrl(rawUrl string) string {
coreMatches := coreRegex.FindStringSubmatch(rawUrl)
if coreMatches == nil {
return ""
}
domain := coreMatches[1]
rest := coreMatches[2]
exchange := ""
if domain == "stackoverflow.com" {
// No exchange parameter needed.
} else if sub, found := strings.CutSuffix(domain, ".stackexchange.com"); found {
if sub == "" {
return ""
} else if strings.Contains(sub, ".") {
// Anything containing dots is interpreted as a full domain, so we use the correct full domain.
exchange = domain
} else {
exchange = sub
}
} else {
exchange = domain
}
// Ensure we properly format the return string to avoid double slashes
if exchange == "" {
return rest
} else {
return fmt.Sprintf("/exchange/%s%s", exchange, rest)
}
}
func PostHome(c *gin.Context) {
body := urlConversionRequest{}
@@ -33,16 +67,9 @@ func PostHome(c *gin.Context) {
return
}
soLink := body.URL
translated := translateUrl(body.URL)
// remove the www.
soLink = strings.ReplaceAll(soLink, "www.", "")
// validate URL
isStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/questions/")
isShortenedStackOverflow := strings.HasPrefix(soLink, "https://stackoverflow.com/a/") || strings.HasPrefix(soLink, "https://stackoverflow.com/q/")
isStackExchange := stackExchangeRegex.MatchString(soLink)
if !isStackExchange && !isStackOverflow && !isShortenedStackOverflow {
if translated == "" {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Invalid stack overflow/exchange URL",
"theme": c.MustGet("theme").(string),
@@ -50,14 +77,5 @@ func PostHome(c *gin.Context) {
return
}
// if stack overflow, trim https://stackoverflow.com
if isStackOverflow || isShortenedStackOverflow {
c.Redirect(302, strings.TrimPrefix(soLink, "https://stackoverflow.com"))
return
}
// if stack exchange, extract the subdomain
sub := stackExchangeRegex.FindStringSubmatch(soLink)[1]
c.Redirect(302, fmt.Sprintf("/exchange/%s/%s", sub, strings.TrimPrefix(soLink, fmt.Sprintf("https://%s.stackexchange.com", sub))))
c.Redirect(302, translated)
}
+32
View File
@@ -0,0 +1,32 @@
package routes
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestTranslateUrl(t *testing.T) {
assert := assert.New(t)
// Test with a Valid StackOverflow URL
assert.Equal("/questions/example-question", translateUrl("https://stackoverflow.com/questions/example-question"), "StackOverflow URL should not be modified")
// Test with Complex Subdomain
assert.Equal("/exchange/meta.math.stackexchange.com/q/example-question", translateUrl("https://meta.math.stackexchange.com/q/example-question"), "Complex StackExchange subdomain should be used as full exchange")
// Test with Non-StackExchange Domain
assert.Equal("/exchange/example.com/questions/example-question", translateUrl("https://example.com/questions/example-question"), "Non-StackExchange domain should be detected as exchange")
// Test with Invalid URL
assert.Equal("", translateUrl("This is not a URL"), "Invalid URL should return an empty string")
// Test with Empty String
assert.Equal("", translateUrl(""), "Empty string should return an empty string")
// Test with Missing Path
assert.Equal("", translateUrl("https://stackoverflow.com"), "URL without path should return an empty string")
// Test with Valid URL but Root Domain for StackExchange
assert.Equal("", translateUrl("https://stackexchange.com"), "Root StackExchange domain without subdomain should return an empty string")
}
+7 -12
View File
@@ -47,7 +47,9 @@ func ViewQuestion(c *gin.Context) {
domain := "stackoverflow.com"
if params.Sub != "" {
if strings.Contains(params.Sub, ".") {
domain = params.Sub
} else if params.Sub != "" {
domain = fmt.Sprintf("%s.stackexchange.com", params.Sub)
}
@@ -109,7 +111,7 @@ func ViewQuestion(c *gin.Context) {
"answers": answers,
"imagePolicy": imagePolicy,
"theme": c.MustGet("theme").(string),
"currentUrl": fmt.Sprintf("%s/questions/%s/%s", os.Getenv("APP_URL"), questionId, params.QuestionTitle),
"currentUrl": fmt.Sprintf("%s%s", os.Getenv("APP_URL"), c.Request.URL.Path),
"sortValue": params.SoSortValue,
"domain": domain,
})
@@ -180,7 +182,7 @@ func extractQuestionData(doc *goquery.Document, domain string) (question types.F
if err != nil {
return question, err
}
question.Body = template.HTML(processHTMLBody(questionBodyParentHTML))
question.Body = template.HTML(utils.ProcessHTMLBody(questionBodyParentHTML))
// Extract the shortened body description.
shortenedBody := strings.TrimSpace(questionBodyParent.Text())
@@ -245,8 +247,8 @@ func extractAnswersData(doc *goquery.Document, domain string) ([]types.FilteredA
answerBodyHTML, _ := answerBody.Html()
// Process code blocks within the answer.
processedAnswerBody := processHTMLBody(answerBodyHTML)
answer.Body = template.HTML(html.UnescapeString(processedAnswerBody))
processedAnswerBody := utils.ProcessHTMLBody(answerBodyHTML)
answer.Body = template.HTML(processedAnswerBody)
// Extract author information and timestamp.
extractAnswerAuthorInfo(s, &answer, domain)
@@ -257,13 +259,6 @@ func extractAnswersData(doc *goquery.Document, domain string) ([]types.FilteredA
return answers, nil
}
// processHTMLBody highlights syntax and replaces images with proxied versions.
func processHTMLBody(bodyHTML string) string {
highlightedBody := utils.HighlightCodeBlocks(bodyHTML)
imageProxiedBody := utils.ReplaceImgTags(highlightedBody)
return imageProxiedBody
}
// extractAnswerAuthorInfo extracts the author name, URL, and timestamp from an answer block.
// It directly mutates the answer.
func extractAnswerAuthorInfo(selection *goquery.Selection, answer *types.FilteredAnswer, domain string) {
+2 -1
View File
@@ -11,6 +11,7 @@ import (
func RedirectShortenedOverflowURL(c *gin.Context) {
id := c.Param("id")
answerId := c.Param("answerId")
// fetch the stack overflow URL
client := resty.New()
@@ -20,7 +21,7 @@ func RedirectShortenedOverflowURL(c *gin.Context) {
}),
)
resp, err := client.R().Get(fmt.Sprintf("https://www.stackoverflow.com/a/%s", id))
resp, err := client.R().Get(fmt.Sprintf("https://www.stackoverflow.com/a/%s/%s", id, answerId))
if err != nil {
c.HTML(400, "home.html", gin.H{
"errorMessage": "Unable to fetch stack overflow URL",
+1 -1
View File
@@ -50,7 +50,7 @@ func FindAndReturnComments(inHtml, domain string, postLayout *goquery.Selection)
commentTimestamp := commentBody.Find("span.relativetime-clean").Text()
newFilteredComment := types.FilteredComment{
Text: template.HTML(commentCopy),
Text: template.HTML(ProcessHTMLBody(commentCopy)),
Timestamp: commentTimestamp,
AuthorName: commentAuthor.Text(),
AuthorURL: commentAuthorURL,
+45
View File
@@ -0,0 +1,45 @@
package utils
import (
"net/url"
"regexp"
"strings"
)
// stackOverflowLinkQualifierRegex matches all anchor elements that meet the following conditions:
// * must be an anchor element
// * the anchor element must have a pathname beginning with /q or /questions
// * if there is a host, it must be stackoverflow.com or a subdomain
var stackOverflowLinkQualifierRegex = regexp.MustCompile(`<a\s[^>]*href="(?:https?://(?:www\.)?(?:\w+\.)*(?:stackoverflow|stackexchange)\.com)?/(?:q|questions)/[^"]*"[^>]*>.*?</a>`)
func ReplaceStackOverflowLinks(html string) string {
return stackOverflowLinkQualifierRegex.ReplaceAllStringFunc(html, func(match string) string {
// Extract the href attribute value from the anchor tag
hrefRegex := regexp.MustCompile(`href="([^"]*)"`)
hrefMatch := hrefRegex.FindStringSubmatch(match)
if len(hrefMatch) < 2 {
return match
}
href := hrefMatch[1]
// Parse the URL
url, err := url.Parse(href)
if err != nil {
return match
}
newUrl := url.String()
// Check if the host is a subdomain
parts := strings.Split(url.Host, ".")
if len(parts) > 2 {
// Prepend the subdomain to the path
url.Path = "/exchange/" + parts[0] + url.Path
}
newUrl = url.Path + url.RawQuery + url.Fragment
// Replace the href attribute value in the anchor tag
return strings.Replace(match, hrefMatch[1], newUrl, 1)
})
}
+49
View File
@@ -0,0 +1,49 @@
package utils
import (
"fmt"
"github.com/stretchr/testify/assert"
"strings"
"testing"
)
var sampleInput = `<div class="d-flex fd-column fw-nowrap">
<div class="d-flex fw-nowrap">
<div class="flex--item wmn0 fl1 lh-lg">
<div class="flex--item fl1 lh-lg">
<div>
<b>This question already has answers here</b>:
</div>
</div>
</div>
</div>
<div class="flex--item mb0 mt4">
<a href="/questions/55083952/is-it-possible-to-populate-a-large-set-at-compile-time" dir="ltr">Is it possible to populate a large set at compile time?</a>
<span class="question-originals-answer-count">
(3 answers)
</span>
</div>
<div class="flex--item mb0 mt4">
<a href="https://stackoverflow.com/questions/27221504/how-can-you-make-a-safe-static-singleton-in-rust" dir="ltr">How can you make a safe static singleton in Rust?</a>
<span class="question-originals-answer-count">
(5 answers)
</span>
</div>
<div class="flex--item mb0 mt4">
<a href="https://security.stackexchange.com/questions/25371/brute-force-an-ssh-login-that-has-only-a-4-letter-password" dir="ltr">Brute-force an SSH-login that has only a 4-letter password</a>
<span class="question-originals-answer-count">
(9 answers)
</span>
</div>
<div class="flex--item mb0 mt8">Closed <span title="2020-01-29 14:28:42Z" class="relativetime">4 years ago</span>.</div>
</div>`
func TestReplaceStackOverflowLinks(t *testing.T) {
replacedLinks := ReplaceStackOverflowLinks(sampleInput)
fmt.Println(replacedLinks)
assert.False(t, strings.Contains(replacedLinks, "stackoverflow.com"))
assert.False(t, strings.Contains(replacedLinks, "stackexchange.com"))
}
+9
View File
@@ -0,0 +1,9 @@
package utils
// ProcessHTMLBody runs HTML through the various preparation functions.
func ProcessHTMLBody(bodyHTML string) string {
highlightedBody := HighlightCodeBlocks(bodyHTML)
imageProxiedBody := ReplaceImgTags(highlightedBody)
stackOverflowLinksReplacedBody := ReplaceStackOverflowLinks(imageProxiedBody)
return stackOverflowLinksReplacedBody
}
+9 -6
View File
@@ -14,13 +14,15 @@ import (
// highlightSyntaxViaContent uses Chroma to lex code content and apply the appropriate tokenizer engine.
// If it can't find one, it defaults to JavaScript syntax highlighting.
func highlightSyntaxViaContent(content string) (htmlOut string) {
func highlightSyntaxViaContent(content string, lang string) (htmlOut string) {
content = html.UnescapeString(content)
fallbackOut := html.EscapeString(content)
// identify the language
lexer := lexers.Analyse(content)
lexer := lexers.Get(lang)
if lexer == nil {
lexer = lexers.Analyse(content)
}
if lexer == nil {
lexer = lexers.Get(".js")
}
@@ -71,19 +73,20 @@ func stripBlockTags(content string) (result string) {
return
}
var codeBlockRegex = regexp.MustCompile(`(?s)<pre><code>(.*?)<\/code><\/pre>`)
var codeBlockRegex = regexp.MustCompile(`(?s)<pre(?:[^>]+?lang-(.+?)[\s"'])?.*?><code>(.*?)<\/code><\/pre>`)
// HighlightCodeBlocks uses both highlightSyntaxViaContent stripCodeBlocks and returns the newly highlighted code HTML.
func HighlightCodeBlocks(html string) string {
// Replace each code block with the highlighted version
highlightedHTML := codeBlockRegex.ReplaceAllStringFunc(html, func(codeBlock string) string {
// Extract the code content from the code block
codeContent := codeBlockRegex.FindStringSubmatch(codeBlock)[1]
matches := codeBlockRegex.FindStringSubmatch(codeBlock)
lang, codeContent := matches[1], matches[2]
codeContent = stripBlockTags(codeContent)
// Highlight the code content
highlightedCode := highlightSyntaxViaContent(codeContent)
highlightedCode := highlightSyntaxViaContent(codeContent, lang)
// Replace the original code block with the highlighted version
highlightedCodeBlock := "<pre>" + highlightedCode + "</pre>"
+1 -1
View File
@@ -7,7 +7,7 @@
<link rel="stylesheet" href="/static/comments.css" />
<meta
http-equiv="Content-Security-Policy"
content="default-src 'none'; style-src 'self'; script-src 'self'; img-src {{ .imagePolicy }};"
content="default-src 'none'; style-src 'self'; script-src 'self'; img-src {{ .imagePolicy }}; font-src 'self';"
/>
<meta name="description" content="{{ .question.ShortenedBody }}..." />
{{ template "sharedHead.html" }}