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 }