feat: highlight code blocks within answers
This commit is contained in:
parent
da57f98fec
commit
631a700c9f
2
go.mod
2
go.mod
@ -4,7 +4,9 @@ go 1.19
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
github.com/PuerkitoBio/goquery v1.8.0 // indirect
|
||||||
|
github.com/alecthomas/chroma v0.10.0 // indirect
|
||||||
github.com/andybalholm/cascadia v1.3.1 // indirect
|
github.com/andybalholm/cascadia v1.3.1 // indirect
|
||||||
|
github.com/dlclark/regexp2 v1.7.0 // indirect
|
||||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
github.com/gin-gonic/gin v1.8.2 // indirect
|
github.com/gin-gonic/gin v1.8.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.0 // indirect
|
github.com/go-playground/locales v0.14.0 // indirect
|
||||||
|
5
go.sum
5
go.sum
@ -1,10 +1,15 @@
|
|||||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||||
|
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||||
|
github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
|
||||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||||
|
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
|
||||||
|
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
|
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=
|
||||||
|
60
public/syntax.css
Normal file
60
public/syntax.css
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
/* Background */ .bg { color: #f8f8f2; background-color: #272822; }
|
||||||
|
/* Error */ .err { color: #960050; background-color: #1e0010 }
|
||||||
|
/* LineTableTD */ .lntd { vertical-align: top; padding: 0; margin: 0; border: 0; }
|
||||||
|
/* LineTable */ .lntable { border-spacing: 0; padding: 0; margin: 0; border: 0; }
|
||||||
|
/* LineHighlight */ .hl { background-color: #3c3d38 }
|
||||||
|
/* LineNumbersTable */ .lnt { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||||
|
/* LineNumbers */ .ln { white-space: pre; user-select: none; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;color: #7f7f7f }
|
||||||
|
/* Line */ .line { display: flex; }
|
||||||
|
/* Keyword */ .k { color: #66d9ef }
|
||||||
|
/* KeywordConstant */ .kc { color: #66d9ef }
|
||||||
|
/* KeywordDeclaration */ .kd { color: #66d9ef }
|
||||||
|
/* KeywordNamespace */ .kn { color: #f92672 }
|
||||||
|
/* KeywordPseudo */ .kp { color: #66d9ef }
|
||||||
|
/* KeywordReserved */ .kr { color: #66d9ef }
|
||||||
|
/* KeywordType */ .kt { color: #66d9ef }
|
||||||
|
/* NameAttribute */ .na { color: #a6e22e }
|
||||||
|
/* NameClass */ .nc { color: #a6e22e }
|
||||||
|
/* NameConstant */ .no { color: #66d9ef }
|
||||||
|
/* NameDecorator */ .nd { color: #a6e22e }
|
||||||
|
/* NameException */ .ne { color: #a6e22e }
|
||||||
|
/* NameFunction */ .nf { color: #a6e22e }
|
||||||
|
/* NameOther */ .nx { color: #a6e22e }
|
||||||
|
/* NameTag */ .nt { color: #f92672 }
|
||||||
|
/* Literal */ .l { color: #ae81ff }
|
||||||
|
/* LiteralDate */ .ld { color: #e6db74 }
|
||||||
|
/* LiteralString */ .s { color: #e6db74 }
|
||||||
|
/* LiteralStringAffix */ .sa { color: #e6db74 }
|
||||||
|
/* LiteralStringBacktick */ .sb { color: #e6db74 }
|
||||||
|
/* LiteralStringChar */ .sc { color: #e6db74 }
|
||||||
|
/* LiteralStringDelimiter */ .dl { color: #e6db74 }
|
||||||
|
/* LiteralStringDoc */ .sd { color: #e6db74 }
|
||||||
|
/* LiteralStringDouble */ .s2 { color: #e6db74 }
|
||||||
|
/* LiteralStringEscape */ .se { color: #ae81ff }
|
||||||
|
/* LiteralStringHeredoc */ .sh { color: #e6db74 }
|
||||||
|
/* LiteralStringInterpol */ .si { color: #e6db74 }
|
||||||
|
/* LiteralStringOther */ .sx { color: #e6db74 }
|
||||||
|
/* LiteralStringRegex */ .sr { color: #e6db74 }
|
||||||
|
/* LiteralStringSingle */ .s1 { color: #e6db74 }
|
||||||
|
/* LiteralStringSymbol */ .ss { color: #e6db74 }
|
||||||
|
/* LiteralNumber */ .m { color: #ae81ff }
|
||||||
|
/* LiteralNumberBin */ .mb { color: #ae81ff }
|
||||||
|
/* LiteralNumberFloat */ .mf { color: #ae81ff }
|
||||||
|
/* LiteralNumberHex */ .mh { color: #ae81ff }
|
||||||
|
/* LiteralNumberInteger */ .mi { color: #ae81ff }
|
||||||
|
/* LiteralNumberIntegerLong */ .il { color: #ae81ff }
|
||||||
|
/* LiteralNumberOct */ .mo { color: #ae81ff }
|
||||||
|
/* Operator */ .o { color: #f92672 }
|
||||||
|
/* OperatorWord */ .ow { color: #f92672 }
|
||||||
|
/* Comment */ .c { color: #75715e }
|
||||||
|
/* CommentHashbang */ .ch { color: #75715e }
|
||||||
|
/* CommentMultiline */ .cm { color: #75715e }
|
||||||
|
/* CommentSingle */ .c1 { color: #75715e }
|
||||||
|
/* CommentSpecial */ .cs { color: #75715e }
|
||||||
|
/* CommentPreproc */ .cp { color: #75715e }
|
||||||
|
/* CommentPreprocFile */ .cpf { color: #75715e }
|
||||||
|
/* GenericDeleted */ .gd { color: #f92672 }
|
||||||
|
/* GenericEmph */ .ge { font-style: italic }
|
||||||
|
/* GenericInserted */ .gi { color: #a6e22e }
|
||||||
|
/* GenericStrong */ .gs { font-weight: bold }
|
||||||
|
/* GenericSubheading */ .gu { color: #75715e }
|
@ -1,9 +1,11 @@
|
|||||||
package routes
|
package routes
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"anonymousoverflow/src/utils"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/PuerkitoBio/goquery"
|
"github.com/PuerkitoBio/goquery"
|
||||||
@ -11,6 +13,8 @@ import (
|
|||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var codeBlockRegex = regexp.MustCompile(`(?s)<pre><code>(.+?)<\/code><\/pre>`)
|
||||||
|
|
||||||
func ViewQuestion(c *gin.Context) {
|
func ViewQuestion(c *gin.Context) {
|
||||||
client := resty.New()
|
client := resty.New()
|
||||||
|
|
||||||
@ -137,7 +141,17 @@ func ViewQuestion(c *gin.Context) {
|
|||||||
// append <div class="answer-author">Answered %s by %s</div> to the bottom of the answer
|
// append <div class="answer-author">Answered %s by %s</div> to the bottom of the answer
|
||||||
answerBodyHTML += fmt.Sprintf(`<div class="answer-author-parent"><div class="answer-author">Answered at %s by <a href="https://stackoverflow.com/%s" target="_blank" rel="noopener noreferrer">%s</a></div></div>`, answerTimestamp, answerAuthorURL, answerAuthorName)
|
answerBodyHTML += fmt.Sprintf(`<div class="answer-author-parent"><div class="answer-author">Answered at %s by <a href="https://stackoverflow.com/%s" target="_blank" rel="noopener noreferrer">%s</a></div></div>`, answerTimestamp, answerAuthorURL, answerAuthorName)
|
||||||
|
|
||||||
// get the timestamp and author
|
// parse any code blocks and highlight them
|
||||||
|
answerCodeBlocks := codeBlockRegex.FindAllString(answerBodyHTML, -1)
|
||||||
|
for _, codeBlock := range answerCodeBlocks {
|
||||||
|
codeBlock = utils.StripBlockTags(codeBlock)
|
||||||
|
|
||||||
|
// syntax highlight
|
||||||
|
highlightedCodeBlock := utils.HighlightSyntaxViaContent(codeBlock)
|
||||||
|
|
||||||
|
// replace the code block with the highlighted code block
|
||||||
|
answerBodyHTML = strings.Replace(answerBodyHTML, codeBlock, highlightedCodeBlock, 1)
|
||||||
|
}
|
||||||
|
|
||||||
answers = append(answers, template.HTML(answerBodyHTML))
|
answers = append(answers, template.HTML(answerBodyHTML))
|
||||||
})
|
})
|
||||||
|
78
src/utils/syntax.go
Normal file
78
src/utils/syntax.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alecthomas/chroma/formatters"
|
||||||
|
"github.com/alecthomas/chroma/lexers"
|
||||||
|
"github.com/alecthomas/chroma/styles"
|
||||||
|
)
|
||||||
|
|
||||||
|
var plainFormattedCodeRegex = regexp.MustCompile(`(?s)<pre tabindex="0" class="chroma"><code>(.+?)</code></pre>`)
|
||||||
|
|
||||||
|
func HighlightSyntaxViaContent(content string) (htmlOut string) {
|
||||||
|
content = html.UnescapeString(content)
|
||||||
|
|
||||||
|
fallbackOut := content
|
||||||
|
|
||||||
|
// identify the language
|
||||||
|
lexer := lexers.Analyse(content)
|
||||||
|
if lexer == nil {
|
||||||
|
// unable to identify, so just return the wrapped content
|
||||||
|
htmlOut = fallbackOut
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
style := styles.Get("xcode")
|
||||||
|
if style == nil {
|
||||||
|
style = styles.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
formatter := formatters.Get("html")
|
||||||
|
if formatter == nil {
|
||||||
|
formatter = formatters.Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
iterator, err := lexer.Tokenise(nil, content)
|
||||||
|
if err != nil {
|
||||||
|
htmlOut = fallbackOut
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b := bytes.NewBufferString("")
|
||||||
|
w := io.Writer(b)
|
||||||
|
|
||||||
|
err = formatter.Format(w, style, iterator)
|
||||||
|
if err != nil {
|
||||||
|
htmlOut = fallbackOut
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse only the <pre><code>...</code></pre> part
|
||||||
|
htmlOut = b.String()
|
||||||
|
htmlOut = plainFormattedCodeRegex.FindString(htmlOut)
|
||||||
|
|
||||||
|
htmlOut = StripBlockTags(htmlOut)
|
||||||
|
|
||||||
|
// remove <pre tabindex="0" class="chroma">
|
||||||
|
htmlOut = strings.Replace(htmlOut, "<pre tabindex=\"0\" class=\"chroma\">", "", -1)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func StripBlockTags(content string) (result string) {
|
||||||
|
// strip all "<code>" tags
|
||||||
|
content = strings.Replace(content, "<code>", "", -1)
|
||||||
|
content = strings.Replace(content, "</code>", "", -1)
|
||||||
|
// and the <pre>
|
||||||
|
content = strings.Replace(content, "<pre>", "", -1)
|
||||||
|
content = strings.Replace(content, "</pre>", "", -1)
|
||||||
|
|
||||||
|
result = content
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
@ -4,6 +4,7 @@
|
|||||||
<title>{{ .title }}</title>
|
<title>{{ .title }}</title>
|
||||||
<link rel="stylesheet" href="/static/question.css" />
|
<link rel="stylesheet" href="/static/question.css" />
|
||||||
<link rel="stylesheet" href="/static/globals.css" />
|
<link rel="stylesheet" href="/static/globals.css" />
|
||||||
|
<link rel="stylesheet" href="/static/syntax.css" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta
|
<meta
|
||||||
http-equiv="Content-Security-Policy"
|
http-equiv="Content-Security-Policy"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user