feat: highlight code blocks within answers

This commit is contained in:
httpjamesm 2022-12-28 18:51:14 -05:00
parent da57f98fec
commit 631a700c9f
6 changed files with 161 additions and 1 deletions

2
go.mod
View File

@ -4,7 +4,9 @@ go 1.19
require (
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/dlclark/regexp2 v1.7.0 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.8.2 // indirect
github.com/go-playground/locales v0.14.0 // indirect

5
go.sum
View File

@ -1,10 +1,15 @@
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/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/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
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.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/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.2 h1:UzKToD9/PoFj/V4rvlKqTRKnQYyz8Sc1MJlv4JHPtvY=

60
public/syntax.css Normal file
View 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 }

View File

@ -1,9 +1,11 @@
package routes
import (
"anonymousoverflow/src/utils"
"fmt"
"html/template"
"os"
"regexp"
"strings"
"github.com/PuerkitoBio/goquery"
@ -11,6 +13,8 @@ import (
"github.com/go-resty/resty/v2"
)
var codeBlockRegex = regexp.MustCompile(`(?s)<pre><code>(.+?)<\/code><\/pre>`)
func ViewQuestion(c *gin.Context) {
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
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))
})

78
src/utils/syntax.go Normal file
View 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
}

View File

@ -4,6 +4,7 @@
<title>{{ .title }}</title>
<link rel="stylesheet" href="/static/question.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
http-equiv="Content-Security-Policy"