mirror of
https://github.com/gohugoio/hugo.git
synced 2026-08-24 15:28:54 +00:00
98ee69bce2
This commit adds the interface ContentReWriter in the tranformer chain. This is backed by two pooled byte buffers, alternating between being the reader or the writer. This keeps the performance characteristic of the old implementation, but in a thread safe way. Fixes #911 Benchmark old vs new: benchmark old ns/op new ns/op delta BenchmarkAbsURL 17614 17384 -1.31% BenchmarkXMLAbsURL 9431 9248 -1.94% benchmark old allocs new allocs delta BenchmarkAbsURL 24 28 +16.67% BenchmarkXMLAbsURL 12 14 +16.67% benchmark old bytes new bytes delta BenchmarkAbsURL 3295 3424 +3.92% BenchmarkXMLAbsURL 1954 1987 +1.69%
236 lines
4.5 KiB
Go
236 lines
4.5 KiB
Go
package transform
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/url"
|
|
"strings"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type matchState int
|
|
|
|
const (
|
|
matchStateNone matchState = iota
|
|
matchStateWhitespace
|
|
matchStatePartial
|
|
matchStateFull
|
|
)
|
|
|
|
const (
|
|
matchPrefixSrc int = iota
|
|
matchPrefixHref
|
|
)
|
|
|
|
type contentlexer struct {
|
|
content []byte
|
|
|
|
pos int // input position
|
|
start int // item start position
|
|
width int // width of last element
|
|
|
|
matchers []absURLMatcher
|
|
state stateFunc
|
|
prefixLookup *prefixes
|
|
|
|
w io.Writer
|
|
}
|
|
|
|
type stateFunc func(*contentlexer) stateFunc
|
|
|
|
type prefixRunes []rune
|
|
|
|
type prefixes struct {
|
|
pr []prefixRunes
|
|
curr prefixRunes // current prefix lookup table
|
|
i int // current index
|
|
|
|
// first rune in potential match
|
|
first rune
|
|
|
|
// match-state:
|
|
// none, whitespace, partial, full
|
|
ms matchState
|
|
}
|
|
|
|
// match returns partial and full match for the prefix in play
|
|
// - it's a full match if all prefix runes has checked out in row
|
|
// - it's a partial match if it's on its way towards a full match
|
|
func (l *contentlexer) match(r rune) {
|
|
p := l.prefixLookup
|
|
if p.curr == nil {
|
|
// assumes prefixes all start off on a different rune
|
|
// works in this special case: href, src
|
|
p.i = 0
|
|
for _, pr := range p.pr {
|
|
if pr[p.i] == r {
|
|
fullMatch := len(p.pr) == 1
|
|
p.first = r
|
|
if !fullMatch {
|
|
p.curr = pr
|
|
l.prefixLookup.ms = matchStatePartial
|
|
} else {
|
|
l.prefixLookup.ms = matchStateFull
|
|
}
|
|
return
|
|
}
|
|
}
|
|
} else {
|
|
p.i++
|
|
if p.curr[p.i] == r {
|
|
fullMatch := len(p.curr) == p.i+1
|
|
if fullMatch {
|
|
p.curr = nil
|
|
l.prefixLookup.ms = matchStateFull
|
|
} else {
|
|
l.prefixLookup.ms = matchStatePartial
|
|
}
|
|
return
|
|
}
|
|
|
|
p.curr = nil
|
|
}
|
|
|
|
l.prefixLookup.ms = matchStateNone
|
|
}
|
|
|
|
func (l *contentlexer) emit() {
|
|
l.w.Write(l.content[l.start:l.pos])
|
|
l.start = l.pos
|
|
}
|
|
|
|
var mainPrefixRunes = []prefixRunes{{'s', 'r', 'c', '='}, {'h', 'r', 'e', 'f', '='}}
|
|
|
|
type absURLMatcher struct {
|
|
prefix int
|
|
match []byte
|
|
replacement []byte
|
|
}
|
|
|
|
func (a absURLMatcher) isSourceType() bool {
|
|
return a.prefix == matchPrefixSrc
|
|
}
|
|
|
|
func checkCandidate(l *contentlexer) {
|
|
isSource := l.prefixLookup.first == 's'
|
|
for _, m := range l.matchers {
|
|
|
|
if isSource && !m.isSourceType() || !isSource && m.isSourceType() {
|
|
continue
|
|
}
|
|
|
|
if bytes.HasPrefix(l.content[l.pos:], m.match) {
|
|
// check for schemaless URLs
|
|
posAfter := l.pos + len(m.match)
|
|
if int(posAfter) >= len(l.content) {
|
|
return
|
|
}
|
|
r, _ := utf8.DecodeRune(l.content[posAfter:])
|
|
if r == '/' {
|
|
// schemaless: skip
|
|
return
|
|
}
|
|
if l.pos > l.start {
|
|
l.emit()
|
|
}
|
|
l.pos += len(m.match)
|
|
l.w.Write(m.replacement)
|
|
l.start = l.pos
|
|
return
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
func (l *contentlexer) replace() {
|
|
contentLength := len(l.content)
|
|
var r rune
|
|
|
|
for {
|
|
if int(l.pos) >= contentLength {
|
|
l.width = 0
|
|
break
|
|
}
|
|
|
|
var width int = 1
|
|
r = rune(l.content[l.pos])
|
|
if r >= utf8.RuneSelf {
|
|
r, width = utf8.DecodeRune(l.content[l.pos:])
|
|
}
|
|
l.width = width
|
|
l.pos += l.width
|
|
if r == ' ' {
|
|
l.prefixLookup.ms = matchStateWhitespace
|
|
} else if l.prefixLookup.ms != matchStateNone {
|
|
l.match(r)
|
|
if l.prefixLookup.ms == matchStateFull {
|
|
checkCandidate(l)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// Done!
|
|
if l.pos > l.start {
|
|
l.emit()
|
|
}
|
|
}
|
|
|
|
func doReplace(rw ContentReWriter, matchers []absURLMatcher) {
|
|
|
|
lexer := &contentlexer{
|
|
content: rw.Content(),
|
|
w: rw,
|
|
prefixLookup: &prefixes{pr: mainPrefixRunes},
|
|
matchers: matchers}
|
|
|
|
lexer.replace()
|
|
|
|
}
|
|
|
|
type absURLReplacer struct {
|
|
htmlMatchers []absURLMatcher
|
|
xmlMatchers []absURLMatcher
|
|
}
|
|
|
|
func newAbsURLReplacer(baseURL string) *absURLReplacer {
|
|
u, _ := url.Parse(baseURL)
|
|
base := strings.TrimRight(u.String(), "/")
|
|
|
|
// HTML
|
|
dqHTMLMatch := []byte("\"/")
|
|
sqHTMLMatch := []byte("'/")
|
|
|
|
// XML
|
|
dqXMLMatch := []byte(""/")
|
|
sqXMLMatch := []byte("'/")
|
|
|
|
dqHTML := []byte("\"" + base + "/")
|
|
sqHTML := []byte("'" + base + "/")
|
|
|
|
dqXML := []byte(""" + base + "/")
|
|
sqXML := []byte("'" + base + "/")
|
|
|
|
return &absURLReplacer{
|
|
htmlMatchers: []absURLMatcher{
|
|
{matchPrefixSrc, dqHTMLMatch, dqHTML},
|
|
{matchPrefixSrc, sqHTMLMatch, sqHTML},
|
|
{matchPrefixHref, dqHTMLMatch, dqHTML},
|
|
{matchPrefixHref, sqHTMLMatch, sqHTML}},
|
|
xmlMatchers: []absURLMatcher{
|
|
{matchPrefixSrc, dqXMLMatch, dqXML},
|
|
{matchPrefixSrc, sqXMLMatch, sqXML},
|
|
{matchPrefixHref, dqXMLMatch, dqXML},
|
|
{matchPrefixHref, sqXMLMatch, sqXML},
|
|
}}
|
|
|
|
}
|
|
|
|
func (au *absURLReplacer) replaceInHTML(rw ContentReWriter) {
|
|
doReplace(rw, au.htmlMatchers)
|
|
}
|
|
|
|
func (au *absURLReplacer) replaceInXML(rw ContentReWriter) {
|
|
doReplace(rw, au.xmlMatchers)
|
|
}
|