tpls

Extendable, Fast Template Engine for Go
git clone git://git.lair.cx/tpls
Log | Files | Refs | README | LICENSE

builtin_template.go (5402B)


      1 package tplc
      2 
      3 import (
      4 	"bytes"
      5 	"fmt"
      6 	"html"
      7 	"io"
      8 	"regexp"
      9 	"strconv"
     10 	"unicode"
     11 	"unicode/utf8"
     12 
     13 	"github.com/pkg/errors"
     14 	"github.com/valyala/bytebufferpool"
     15 
     16 	"go.lair.cx/go-core/net/htmlx"
     17 	"go.lair.cx/tpls/internal/stacks"
     18 )
     19 
     20 var regexTemplateClassParser = regexp.MustCompile(
     21 	`^\s*(\((?:[^\s)]+[ \t]+)?[^\s)]+\)[ \t]+)?([^\s(]+)\(([^)]+)?\)\s*$`)
     22 
     23 var regexTemplatePlainTextLineParser = regexp.MustCompile(
     24 	`^(?:[\t ]*([\r\n])[\t ]*)?([^\n\r]*)`)
     25 
     26 func TagTemplate(b *Builder, w *Writer, t *htmlx.Tokenizer) error {
     27 	attrs := getAttrs(t)
     28 
     29 	_, plainText := attrs["plain"]
     30 
     31 	className, ok := attrs["class"]
     32 	if !ok {
     33 		return errors.New("class is required for template tag")
     34 	}
     35 	caps := regexTemplateClassParser.FindStringSubmatch(className)
     36 	if caps == nil {
     37 		return fmt.Errorf("invalid template name: %s", strconv.Quote(className))
     38 	}
     39 
     40 	w.WriteGoCode("func ", caps[1], caps[2], "(w tpls.Writer,", caps[3], ") {\n")
     41 
     42 	err := RenderTag(b, w, t, "template", plainText)
     43 	if err != nil {
     44 		return err
     45 	}
     46 
     47 	w.WriteGoCode("}\n\n")
     48 	return nil
     49 }
     50 
     51 func RenderTag(
     52 	b *Builder,
     53 	w *Writer,
     54 	t *htmlx.Tokenizer,
     55 	name string,
     56 	plainText bool,
     57 ) error {
     58 	return renderTag(b, w, t, name, nil, false, plainText)
     59 }
     60 
     61 func RenderTagWithCustomTags(
     62 	b *Builder,
     63 	w *Writer,
     64 	t *htmlx.Tokenizer,
     65 	name string,
     66 	customHandlers map[string]TagHandler,
     67 	useCustomOnly bool,
     68 	plainText bool,
     69 ) error {
     70 	return renderTag(b, w, t, name, customHandlers, useCustomOnly, plainText)
     71 }
     72 
     73 func renderTag(
     74 	b *Builder,
     75 	w *Writer,
     76 	t *htmlx.Tokenizer,
     77 	name string,
     78 	customHandlers map[string]TagHandler,
     79 	useCustomOnly bool,
     80 	plainText bool, // Plain text mode - keep spaces, trim prefix indent, etc.
     81 ) error {
     82 	if t.CurrentType() == htmlx.SelfClosingTagToken {
     83 		return nil
     84 	}
     85 
     86 	buf := bytebufferpool.Get()
     87 	defer bytebufferpool.Put(buf)
     88 
     89 	stackBuf := bytebufferpool.Get()
     90 	defer bytebufferpool.Put(stackBuf)
     91 
     92 	stack := stacks.NewByteStack(stackBuf.B)
     93 
     94 	flushBuffer := func() {
     95 		if len(buf.B) > 0 {
     96 			w.WriteRawBytesWithBacktick(buf.B)
     97 			buf.B = buf.B[:0]
     98 		}
     99 	}
    100 
    101 loop:
    102 	for {
    103 		switch t.Next() {
    104 		case htmlx.StartTagToken, htmlx.SelfClosingTagToken:
    105 			tagName, _ := t.TagName()
    106 			handler := b.Handler(string(tagName))
    107 
    108 			if handler == nil {
    109 				handler, _ = customHandlers[string(tagName)]
    110 			}
    111 			if handler == nil {
    112 				if useCustomOnly {
    113 					return fmt.Errorf("unexpected tag: %s", tagName)
    114 				}
    115 				attrs := getAttrs(t)
    116 				// buf.B = append(buf.B, t.Raw()...)
    117 				buf.B = append(buf.B, '<')
    118 				buf.B = append(buf.B, tagName...)
    119 				for key, attr := range attrs {
    120 					buf.B = append(buf.B, ' ')
    121 					if len(key) > 0 && key[0] == ':' {
    122 						buf.B = append(buf.B, key[1:]...)
    123 						buf.B = append(buf.B, '=', '"')
    124 						flushBuffer()
    125 						w.WriteString(attr)
    126 						buf.B = append(buf.B, '"')
    127 					} else {
    128 						buf.B = append(buf.B, key...)
    129 						buf.B = append(buf.B, '=', '"')
    130 						buf.B = append(buf.B, html.EscapeString(attr)...)
    131 						buf.B = append(buf.B, '"')
    132 					}
    133 				}
    134 				buf.B = append(buf.B, '>')
    135 				stack.Put(tagName)
    136 			} else {
    137 				flushBuffer()
    138 
    139 				err := handler(b, w, t)
    140 				if err != nil {
    141 					return err
    142 				}
    143 			}
    144 
    145 		case htmlx.EndTagToken:
    146 			tagName, _ := t.TagName()
    147 
    148 			stack.Lock()
    149 			if stack.Len() == 0 {
    150 				if string(tagName) != name {
    151 					return errors.New("unexpected end tag: " + string(tagName))
    152 				}
    153 				break loop
    154 			}
    155 
    156 			for {
    157 				item, ok := stack.UnsafePop()
    158 				if !ok && string(tagName) == name {
    159 					break loop
    160 				}
    161 				if bytes.Equal(item, tagName) {
    162 					break
    163 				}
    164 			}
    165 			stack.Unlock()
    166 
    167 			buf.B = append(buf.B, t.Raw()...)
    168 
    169 		case htmlx.TextToken:
    170 			var (
    171 				txt = t.Text()
    172 				l   int
    173 			)
    174 
    175 			for l < len(txt) {
    176 				c, width := utf8.DecodeRune(txt[l:])
    177 				if !unicode.IsSpace(c) {
    178 					break
    179 				}
    180 				l += width
    181 			}
    182 
    183 			// Is empty text?
    184 			if l == len(txt) {
    185 				continue
    186 			}
    187 
    188 			if useCustomOnly {
    189 				return fmt.Errorf("text node is not allowed for %s", name)
    190 			}
    191 
    192 			if plainText {
    193 				// Plain Text Mode
    194 				// In this mode, only the prefix 'space' character on each line
    195 				// is removed. space characters do not include line breaks.
    196 				var src = txt
    197 				var caps [][]byte
    198 				for len(src) > 0 {
    199 					caps = regexTemplatePlainTextLineParser.FindSubmatch(src)
    200 					if caps == nil {
    201 						break
    202 					}
    203 
    204 					buf.B = append(buf.B, caps[1]...)
    205 					buf.B = append(buf.B, caps[2]...)
    206 
    207 					src = src[len(caps[0]):]
    208 				}
    209 			} else {
    210 				// Smart Indent Mode
    211 				// In this mode, space characters are reduced. The space
    212 				// between the tags is removed, and the space characters at the
    213 				// beginning and end of the text node are replaced by ' '.
    214 				// this removes the 'weird padding' between tags which is
    215 				// useless in most cases, and make <if> tag works properly
    216 				// and also saves few bytes while keep a gap.
    217 
    218 				var r int
    219 				for r = len(txt); r > 0; {
    220 					c, width := utf8.DecodeLastRune(txt[:r])
    221 					if !unicode.IsSpace(c) {
    222 						break
    223 					}
    224 					r -= width
    225 				}
    226 
    227 				if l != 0 {
    228 					buf.B = append(buf.B, ' ')
    229 				}
    230 
    231 				buf.B = append(buf.B, txt[l:r]...)
    232 
    233 				if r != len(txt) {
    234 					buf.B = append(buf.B, ' ')
    235 				}
    236 			}
    237 
    238 		case htmlx.ErrorToken:
    239 			err := t.Err()
    240 			if err == io.EOF {
    241 				if stack.Len() == 0 && len(name) == 0 {
    242 					break loop
    243 				}
    244 				return io.ErrUnexpectedEOF
    245 			} else {
    246 				return err
    247 			}
    248 
    249 		default:
    250 			buf.B = append(buf.B, t.Raw()...)
    251 		}
    252 	}
    253 
    254 	flushBuffer()
    255 
    256 	return nil
    257 }