mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-07 05:12:45 +02:00
255 lines
4.2 KiB
Go
255 lines
4.2 KiB
Go
// Package headertemplate contains functions for rendering header templates.
|
|
package headertemplate
|
|
|
|
import "strings"
|
|
|
|
// Render renders a header template string.
|
|
func Render(src string, fn func(ref []string) string) string {
|
|
p := newParser(src, fn)
|
|
return p.parse()
|
|
}
|
|
|
|
// This is a hand written parser attempting to model this peg grammar:
|
|
//
|
|
// Grammar <- ( Variable / Text )* !.
|
|
// Text <- .
|
|
// Variable <- EscapedVariable / SimpleVariable / ComplexVariable
|
|
// EscapedVariable <- '$' '$'
|
|
// SimpleVariable <- '$' SimpleExpression
|
|
// SimpleExpression <- identifier ( '.' identifier )*
|
|
// ComplexVariable <- '$' '{' _ ComplexExpression _ '}'
|
|
// ComplexExpression <- identifier _ (ComplexSelector / ComplexIndex)*
|
|
// ComplexSelector <- '.' _ ComplexExpression _
|
|
// ComplexIndex <- '[' _ StringLiteral _ ']' _
|
|
// StringLiteral <- '"' (('\\'.) / [^"])* '"'
|
|
// identifier <- [a-zA-Z0-9_] [a-zA-Z0-9_\-]*
|
|
// _ <- ( ' ' / '\t' )*
|
|
|
|
type parser struct {
|
|
buffer []byte
|
|
pos int
|
|
stack []int
|
|
visit func(ref []string) string
|
|
}
|
|
|
|
func newParser(src string, visit func(ref []string) string) *parser {
|
|
return &parser{buffer: []byte(src), visit: visit}
|
|
}
|
|
|
|
func (p *parser) save() {
|
|
p.stack = append(p.stack, p.pos)
|
|
}
|
|
|
|
func (p *parser) restore() {
|
|
p.pos = p.stack[len(p.stack)-1]
|
|
}
|
|
|
|
func (p *parser) pop() {
|
|
p.stack = p.stack[:len(p.stack)-1]
|
|
}
|
|
|
|
func (p *parser) peek() byte {
|
|
if p.pos < len(p.buffer) {
|
|
return p.buffer[p.pos]
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (p *parser) next() byte {
|
|
if p.pos < len(p.buffer) {
|
|
c := p.buffer[p.pos]
|
|
p.pos++
|
|
return c
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (p *parser) parse() string {
|
|
var b strings.Builder
|
|
for p.pos < len(p.buffer) {
|
|
if v, ok := p.parseVariable(); ok {
|
|
b.WriteString(v)
|
|
continue
|
|
}
|
|
b.WriteByte(p.next())
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (p *parser) parseVariable() (string, bool) {
|
|
if p.peek() != '$' {
|
|
return "", false
|
|
}
|
|
|
|
p.save()
|
|
defer p.pop()
|
|
|
|
// $$ becomes $
|
|
p.next()
|
|
if p.peek() == '$' {
|
|
p.next()
|
|
return "$", true
|
|
}
|
|
|
|
if p.peek() == '{' {
|
|
p.next()
|
|
e, ok := p.parseComplexExpression()
|
|
if !ok {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
if p.next() != '}' {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
return e, true
|
|
}
|
|
|
|
e, ok := p.parseSimpleExpression()
|
|
if !ok {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
|
|
return e, true
|
|
}
|
|
|
|
func (p *parser) parseComplexExpression() (string, bool) {
|
|
p.save()
|
|
defer p.pop()
|
|
|
|
p.skipWhitespace()
|
|
|
|
var ref []string
|
|
id, ok := p.parseIdentifier()
|
|
if !ok {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
ref = append(ref, id)
|
|
|
|
for {
|
|
p.skipWhitespace()
|
|
|
|
if p.peek() == '.' {
|
|
p.next()
|
|
p.skipWhitespace()
|
|
|
|
id, ok := p.parseIdentifier()
|
|
if !ok {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
ref = append(ref, id)
|
|
|
|
} else if p.peek() == '[' {
|
|
p.next()
|
|
p.skipWhitespace()
|
|
|
|
s, ok := p.parseString()
|
|
if !ok {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
ref = append(ref, s)
|
|
|
|
p.skipWhitespace()
|
|
if p.next() != ']' {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return p.visit(ref), true
|
|
}
|
|
|
|
func (p *parser) parseString() (string, bool) {
|
|
p.save()
|
|
defer p.pop()
|
|
|
|
if p.next() != '"' {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
|
|
var b strings.Builder
|
|
for {
|
|
c := p.next()
|
|
switch c {
|
|
case '"':
|
|
return b.String(), true
|
|
case 0:
|
|
p.restore()
|
|
return "", false
|
|
case '\\':
|
|
c = p.next()
|
|
if c == 0 {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
b.WriteByte(c)
|
|
default:
|
|
b.WriteByte(c)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *parser) parseSimpleExpression() (string, bool) {
|
|
p.save()
|
|
defer p.pop()
|
|
|
|
var ref []string
|
|
for {
|
|
id, ok := p.parseIdentifier()
|
|
if !ok {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
ref = append(ref, id)
|
|
|
|
if p.peek() != '.' {
|
|
break
|
|
}
|
|
p.next()
|
|
}
|
|
|
|
return p.visit(ref), true
|
|
}
|
|
|
|
func (p *parser) parseIdentifier() (string, bool) {
|
|
p.save()
|
|
defer p.pop()
|
|
|
|
var b strings.Builder
|
|
for isIdentifierCharacter(p.peek()) {
|
|
b.WriteByte(p.next())
|
|
}
|
|
|
|
if b.Len() == 0 {
|
|
p.restore()
|
|
return "", false
|
|
}
|
|
|
|
return b.String(), true
|
|
}
|
|
|
|
func (p *parser) skipWhitespace() {
|
|
for isWhitespaceCharacter(p.peek()) {
|
|
p.next()
|
|
}
|
|
}
|
|
|
|
func isIdentifierCharacter(c byte) bool {
|
|
return (c >= '0' && c <= '9') ||
|
|
(c >= 'a' && c <= 'z') ||
|
|
(c >= 'A' && c <= 'Z') ||
|
|
c == '_' ||
|
|
c == '-'
|
|
}
|
|
|
|
func isWhitespaceCharacter(c byte) bool {
|
|
return c == ' ' || c == '\t'
|
|
}
|