2024-02-17 06:35:49 +00:00
|
|
|
// 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"
|
|
|
|
}
|
|
|
|
|
2024-03-04 01:46:38 +00:00
|
|
|
func (*Syntax) Initialize(context *goldsmith.Context) error {
|
2024-02-17 06:35:49 +00:00
|
|
|
context.Filter(wildcard.New("**/*.html", "**/*.htm"))
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2024-03-04 02:09:21 +00:00
|
|
|
func (self *Syntax) Process(context *goldsmith.Context, inputFile *goldsmith.File) error {
|
2024-02-17 06:35:49 +00:00
|
|
|
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
|
|
|
|
}
|