goldsmith/plugins/syntax/syntax.go
2024-02-16 22:35:49 -08:00

156 lines
3.7 KiB
Go

// Package syntax generates syntax highlighting for preformatted code blocks
// using the "chroma" processor. All of the themes and styles from the
// processor are directly exposed through the plugin interface.
package syntax
import (
"bytes"
"fmt"
"git.foosoft.net/alex/goldsmith"
"git.foosoft.net/alex/goldsmith/filters/wildcard"
"github.com/PuerkitoBio/goquery"
"github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/lexers"
"github.com/alecthomas/chroma/styles"
)
type Placement int
const (
PlaceInside Placement = iota
PlaceInline
)
// Syntax chainable context.
type Syntax struct {
style string
numbers bool
prefix string
placement Placement
}
// New creates a new instance of the Syntax plugin.
func New() *Syntax {
return &Syntax{
style: "github",
numbers: false,
prefix: "language-",
placement: PlaceInside,
}
}
// Style sets the color scheme used for syntax highlighting (default: "github").
// Additional styles can be found at: https://github.com/alecthomas/chroma/tree/master/styles.
func (self *Syntax) Style(style string) *Syntax {
self.style = style
return self
}
// LineNumbers sets the visibility of a line number gutter next to the code (default: false).
func (self *Syntax) LineNumbers(numbers bool) *Syntax {
self.numbers = numbers
return self
}
// Prefix sets the CSS class name prefix for code language identification (default: "language-").
func (self *Syntax) Prefix(prefix string) *Syntax {
self.prefix = prefix
return self
}
// Placement determines if code should replace the containing block or be placed inside of it (default: "PlaceInside").
func (self *Syntax) Placement(placement Placement) *Syntax {
self.placement = placement
return self
}
func (*Syntax) Name() string {
return "syntax"
}
func (*Syntax) Initialize(context *goldsmith.Context) error {
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
return nil
}
func (self *Syntax) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
if outputFile := context.RetrieveCachedFile(inputFile.Path(), inputFile); outputFile != nil {
outputFile.CopyProps(inputFile)
context.DispatchFile(outputFile)
return nil
}
doc, err := goquery.NewDocumentFromReader(inputFile)
if err != nil {
return err
}
var errs []error
doc.Find(fmt.Sprintf("[class*=%s]", self.prefix)).Each(func(i int, sel *goquery.Selection) {
class := sel.AttrOr("class", "")
language := class[len(self.prefix):]
lexer := lexers.Get(language)
if lexer == nil {
lexer = lexers.Fallback
}
iterator, err := lexer.Tokenise(nil, sel.Text())
if err != nil {
errs = append(errs, err)
return
}
style := styles.Get(self.style)
if style == nil {
style = styles.Fallback
}
var options []html.Option
if self.numbers {
options = append(options, html.WithLineNumbers(true))
}
formatter := html.New(options...)
var buff bytes.Buffer
if err := formatter.Format(&buff, style, iterator); err != nil {
errs = append(errs, err)
return
}
switch self.placement {
case PlaceInside:
sel.SetHtml(string(buff.Bytes()))
case PlaceInline:
if docCode, err := goquery.NewDocumentFromReader(&buff); err == nil {
selPre := docCode.Find("pre")
if style, exists := selPre.Attr("style"); exists {
sel.SetAttr("style", style)
}
if htmlPre, err := selPre.Html(); err == nil {
sel.SetHtml(htmlPre)
}
}
}
})
if len(errs) > 0 {
return errs[0]
}
html, err := doc.Html()
if err != nil {
return err
}
outputFile, err := context.CreateFileFromReader(inputFile.Path(), bytes.NewReader([]byte(html)))
if err != nil {
return err
}
outputFile.CopyProps(inputFile)
context.DispatchAndCacheFile(outputFile, inputFile)
return nil
}