github.com/graemephi/kahugo@v0.62.3-0.20211121071557-d78c0423784d/hugolib/shortcode.go (about) 1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package hugolib 15 16 import ( 17 "bytes" 18 "fmt" 19 "html/template" 20 "path" 21 "reflect" 22 "regexp" 23 "sort" 24 "strconv" 25 "strings" 26 "sync" 27 28 "github.com/gohugoio/hugo/helpers" 29 30 "github.com/gohugoio/hugo/common/herrors" 31 "github.com/pkg/errors" 32 33 "github.com/gohugoio/hugo/parser/pageparser" 34 "github.com/gohugoio/hugo/resources/page" 35 36 "github.com/gohugoio/hugo/common/maps" 37 "github.com/gohugoio/hugo/common/text" 38 "github.com/gohugoio/hugo/common/urls" 39 "github.com/gohugoio/hugo/output" 40 41 bp "github.com/gohugoio/hugo/bufferpool" 42 "github.com/gohugoio/hugo/tpl" 43 ) 44 45 var ( 46 _ urls.RefLinker = (*ShortcodeWithPage)(nil) 47 _ pageWrapper = (*ShortcodeWithPage)(nil) 48 _ text.Positioner = (*ShortcodeWithPage)(nil) 49 ) 50 51 // ShortcodeWithPage is the "." context in a shortcode template. 52 type ShortcodeWithPage struct { 53 Params interface{} 54 Inner template.HTML 55 Page page.Page 56 Parent *ShortcodeWithPage 57 Name string 58 IsNamedParams bool 59 60 // Zero-based ordinal in relation to its parent. If the parent is the page itself, 61 // this ordinal will represent the position of this shortcode in the page content. 62 Ordinal int 63 64 // pos is the position in bytes in the source file. Used for error logging. 65 posInit sync.Once 66 posOffset int 67 pos text.Position 68 69 scratch *maps.Scratch 70 } 71 72 // Position returns this shortcode's detailed position. Note that this information 73 // may be expensive to calculate, so only use this in error situations. 74 func (scp *ShortcodeWithPage) Position() text.Position { 75 scp.posInit.Do(func() { 76 if p, ok := mustUnwrapPage(scp.Page).(pageContext); ok { 77 scp.pos = p.posOffset(scp.posOffset) 78 } 79 }) 80 return scp.pos 81 } 82 83 // Site returns information about the current site. 84 func (scp *ShortcodeWithPage) Site() page.Site { 85 return scp.Page.Site() 86 } 87 88 // Ref is a shortcut to the Ref method on Page. It passes itself as a context 89 // to get better error messages. 90 func (scp *ShortcodeWithPage) Ref(args map[string]interface{}) (string, error) { 91 return scp.Page.RefFrom(args, scp) 92 } 93 94 // RelRef is a shortcut to the RelRef method on Page. It passes itself as a context 95 // to get better error messages. 96 func (scp *ShortcodeWithPage) RelRef(args map[string]interface{}) (string, error) { 97 return scp.Page.RelRefFrom(args, scp) 98 } 99 100 // Scratch returns a scratch-pad scoped for this shortcode. This can be used 101 // as a temporary storage for variables, counters etc. 102 func (scp *ShortcodeWithPage) Scratch() *maps.Scratch { 103 if scp.scratch == nil { 104 scp.scratch = maps.NewScratch() 105 } 106 return scp.scratch 107 } 108 109 // Get is a convenience method to look up shortcode parameters by its key. 110 func (scp *ShortcodeWithPage) Get(key interface{}) interface{} { 111 if scp.Params == nil { 112 return nil 113 } 114 if reflect.ValueOf(scp.Params).Len() == 0 { 115 return nil 116 } 117 118 var x reflect.Value 119 120 switch key.(type) { 121 case int64, int32, int16, int8, int: 122 if reflect.TypeOf(scp.Params).Kind() == reflect.Map { 123 // We treat this as a non error, so people can do similar to 124 // {{ $myParam := .Get "myParam" | default .Get 0 }} 125 // Without having to do additional checks. 126 return nil 127 } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { 128 idx := int(reflect.ValueOf(key).Int()) 129 ln := reflect.ValueOf(scp.Params).Len() 130 if idx > ln-1 { 131 return "" 132 } 133 x = reflect.ValueOf(scp.Params).Index(idx) 134 } 135 case string: 136 if reflect.TypeOf(scp.Params).Kind() == reflect.Map { 137 x = reflect.ValueOf(scp.Params).MapIndex(reflect.ValueOf(key)) 138 if !x.IsValid() { 139 return "" 140 } 141 } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { 142 // We treat this as a non error, so people can do similar to 143 // {{ $myParam := .Get "myParam" | default .Get 0 }} 144 // Without having to do additional checks. 145 return nil 146 } 147 } 148 149 return x.Interface() 150 } 151 152 func (scp *ShortcodeWithPage) page() page.Page { 153 return scp.Page 154 } 155 156 // Note - this value must not contain any markup syntax 157 const shortcodePlaceholderPrefix = "HAHAHUGOSHORTCODE" 158 159 func createShortcodePlaceholder(id string, ordinal int) string { 160 return shortcodePlaceholderPrefix + "-" + id + strconv.Itoa(ordinal) + "-HBHB" 161 } 162 163 type shortcode struct { 164 name string 165 isInline bool // inline shortcode. Any inner will be a Go template. 166 isClosing bool // whether a closing tag was provided 167 inner []interface{} // string or nested shortcode 168 params interface{} // map or array 169 ordinal int 170 err error 171 172 info tpl.Info // One of the output formats (arbitrary) 173 templs []tpl.Template // All output formats 174 175 // If set, the rendered shortcode is sent as part of the surrounding content 176 // to Blackfriday and similar. 177 // Before Hug0 0.55 we didn't send any shortcode output to the markup 178 // renderer, and this flag told Hugo to process the {{ .Inner }} content 179 // separately. 180 // The old behaviour can be had by starting your shortcode template with: 181 // {{ $_hugo_config := `{ "version": 1 }`}} 182 doMarkup bool 183 184 // the placeholder in the source when passed to Blackfriday etc. 185 // This also identifies the rendered shortcode. 186 placeholder string 187 188 pos int // the position in bytes in the source file 189 length int // the length in bytes in the source file 190 } 191 192 func (s shortcode) insertPlaceholder() bool { 193 return !s.doMarkup || s.configVersion() == 1 194 } 195 196 func (s shortcode) configVersion() int { 197 if s.info == nil { 198 // Not set for inline shortcodes. 199 return 2 200 } 201 202 return s.info.ParseInfo().Config.Version 203 } 204 205 func (s shortcode) innerString() string { 206 var sb strings.Builder 207 208 for _, inner := range s.inner { 209 sb.WriteString(inner.(string)) 210 } 211 212 return sb.String() 213 } 214 215 func (sc shortcode) String() string { 216 // for testing (mostly), so any change here will break tests! 217 var params interface{} 218 switch v := sc.params.(type) { 219 case map[string]interface{}: 220 // sort the keys so test assertions won't fail 221 var keys []string 222 for k := range v { 223 keys = append(keys, k) 224 } 225 sort.Strings(keys) 226 tmp := make(map[string]interface{}) 227 228 for _, k := range keys { 229 tmp[k] = v[k] 230 } 231 params = tmp 232 233 default: 234 // use it as is 235 params = sc.params 236 } 237 238 return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner) 239 } 240 241 type shortcodeHandler struct { 242 p *pageState 243 244 s *Site 245 246 // Ordered list of shortcodes for a page. 247 shortcodes []*shortcode 248 249 // All the shortcode names in this set. 250 nameSet map[string]bool 251 252 // Configuration 253 enableInlineShortcodes bool 254 } 255 256 func newShortcodeHandler(p *pageState, s *Site, placeholderFunc func() string) *shortcodeHandler { 257 sh := &shortcodeHandler{ 258 p: p, 259 s: s, 260 enableInlineShortcodes: s.enableInlineShortcodes, 261 shortcodes: make([]*shortcode, 0, 4), 262 nameSet: make(map[string]bool), 263 } 264 265 return sh 266 } 267 268 const ( 269 innerNewlineRegexp = "\n" 270 innerCleanupRegexp = `\A<p>(.*)</p>\n\z` 271 innerCleanupExpand = "$1" 272 ) 273 274 func renderShortcode( 275 level int, 276 s *Site, 277 tplVariants tpl.TemplateVariants, 278 sc *shortcode, 279 parent *ShortcodeWithPage, 280 p *pageState) (string, bool, error) { 281 var tmpl tpl.Template 282 283 // Tracks whether this shortcode or any of its children has template variations 284 // in other languages or output formats. We are currently only interested in 285 // the output formats, so we may get some false positives -- we 286 // should improve on that. 287 var hasVariants bool 288 289 if sc.isInline { 290 if !p.s.enableInlineShortcodes { 291 return "", false, nil 292 } 293 templName := path.Join("_inline_shortcode", p.File().Path(), sc.name) 294 if sc.isClosing { 295 templStr := sc.innerString() 296 297 var err error 298 tmpl, err = s.TextTmpl().Parse(templName, templStr) 299 if err != nil { 300 fe := herrors.ToFileError("html", err) 301 l1, l2 := p.posOffset(sc.pos).LineNumber, fe.Position().LineNumber 302 fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1) 303 return "", false, p.wrapError(fe) 304 } 305 306 } else { 307 // Re-use of shortcode defined earlier in the same page. 308 var found bool 309 tmpl, found = s.TextTmpl().Lookup(templName) 310 if !found { 311 return "", false, errors.Errorf("no earlier definition of shortcode %q found", sc.name) 312 } 313 } 314 } else { 315 var found, more bool 316 tmpl, found, more = s.Tmpl().LookupVariant(sc.name, tplVariants) 317 if !found { 318 s.Log.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.File().Path()) 319 return "", false, nil 320 } 321 hasVariants = hasVariants || more 322 } 323 324 data := &ShortcodeWithPage{Ordinal: sc.ordinal, posOffset: sc.pos, Params: sc.params, Page: newPageForShortcode(p), Parent: parent, Name: sc.name} 325 if sc.params != nil { 326 data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map 327 } 328 329 if len(sc.inner) > 0 { 330 var inner string 331 for _, innerData := range sc.inner { 332 switch innerData := innerData.(type) { 333 case string: 334 inner += innerData 335 case *shortcode: 336 s, more, err := renderShortcode(level+1, s, tplVariants, innerData, data, p) 337 if err != nil { 338 return "", false, err 339 } 340 hasVariants = hasVariants || more 341 inner += s 342 default: 343 s.Log.Errorf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ", 344 sc.name, p.File().Path(), reflect.TypeOf(innerData)) 345 return "", false, nil 346 } 347 } 348 349 // Pre Hugo 0.55 this was the behaviour even for the outer-most 350 // shortcode. 351 if sc.doMarkup && (level > 0 || sc.configVersion() == 1) { 352 var err error 353 b, err := p.pageOutput.cp.renderContent([]byte(inner), false) 354 if err != nil { 355 return "", false, err 356 } 357 358 newInner := b.Bytes() 359 360 // If the type is “” (unknown) or “markdown”, we assume the markdown 361 // generation has been performed. Given the input: `a line`, markdown 362 // specifies the HTML `<p>a line</p>\n`. When dealing with documents as a 363 // whole, this is OK. When dealing with an `{{ .Inner }}` block in Hugo, 364 // this is not so good. This code does two things: 365 // 366 // 1. Check to see if inner has a newline in it. If so, the Inner data is 367 // unchanged. 368 // 2 If inner does not have a newline, strip the wrapping <p> block and 369 // the newline. 370 switch p.m.markup { 371 case "", "markdown": 372 if match, _ := regexp.MatchString(innerNewlineRegexp, inner); !match { 373 cleaner, err := regexp.Compile(innerCleanupRegexp) 374 375 if err == nil { 376 newInner = cleaner.ReplaceAll(newInner, []byte(innerCleanupExpand)) 377 } 378 } 379 } 380 381 // TODO(bep) we may have plain text inner templates. 382 data.Inner = template.HTML(newInner) 383 } else { 384 data.Inner = template.HTML(inner) 385 } 386 387 } 388 389 result, err := renderShortcodeWithPage(s.Tmpl(), tmpl, data) 390 391 if err != nil && sc.isInline { 392 fe := herrors.ToFileError("html", err) 393 l1, l2 := p.posFromPage(sc.pos).LineNumber, fe.Position().LineNumber 394 fe = herrors.ToFileErrorWithLineNumber(fe, l1+l2-1) 395 return "", false, fe 396 } 397 398 return result, hasVariants, err 399 } 400 401 func (s *shortcodeHandler) hasShortcodes() bool { 402 return s != nil && len(s.shortcodes) > 0 403 } 404 405 func (s *shortcodeHandler) renderShortcodesForPage(p *pageState, f output.Format) (map[string]string, bool, error) { 406 rendered := make(map[string]string) 407 408 tplVariants := tpl.TemplateVariants{ 409 Language: p.Language().Lang, 410 OutputFormat: f, 411 } 412 413 var hasVariants bool 414 415 for _, v := range s.shortcodes { 416 s, more, err := renderShortcode(0, s.s, tplVariants, v, nil, p) 417 if err != nil { 418 err = p.parseError(errors.Wrapf(err, "failed to render shortcode %q", v.name), p.source.parsed.Input(), v.pos) 419 return nil, false, err 420 } 421 hasVariants = hasVariants || more 422 rendered[v.placeholder] = s 423 424 } 425 426 return rendered, hasVariants, nil 427 } 428 429 var errShortCodeIllegalState = errors.New("Illegal shortcode state") 430 431 func (s *shortcodeHandler) parseError(err error, input []byte, pos int) error { 432 if s.p != nil { 433 return s.p.parseError(err, input, pos) 434 } 435 return err 436 } 437 438 // pageTokens state: 439 // - before: positioned just before the shortcode start 440 // - after: shortcode(s) consumed (plural when they are nested) 441 func (s *shortcodeHandler) extractShortcode(ordinal, level int, pt *pageparser.Iterator) (*shortcode, error) { 442 if s == nil { 443 panic("handler nil") 444 } 445 sc := &shortcode{ordinal: ordinal} 446 447 cnt := 0 448 nestedOrdinal := 0 449 nextLevel := level + 1 450 451 fail := func(err error, i pageparser.Item) error { 452 return s.parseError(err, pt.Input(), i.Pos) 453 } 454 455 Loop: 456 for { 457 currItem := pt.Next() 458 switch { 459 case currItem.IsLeftShortcodeDelim(): 460 next := pt.Peek() 461 if next.IsRightShortcodeDelim() { 462 // no name: {{< >}} or {{% %}} 463 return sc, errors.New("shortcode has no name") 464 } 465 if next.IsShortcodeClose() { 466 continue 467 } 468 469 if cnt > 0 { 470 // nested shortcode; append it to inner content 471 pt.Backup() 472 nested, err := s.extractShortcode(nestedOrdinal, nextLevel, pt) 473 nestedOrdinal++ 474 if nested != nil && nested.name != "" { 475 s.nameSet[nested.name] = true 476 } 477 478 if err == nil { 479 sc.inner = append(sc.inner, nested) 480 } else { 481 return sc, err 482 } 483 484 } else { 485 sc.doMarkup = currItem.IsShortcodeMarkupDelimiter() 486 } 487 488 cnt++ 489 490 case currItem.IsRightShortcodeDelim(): 491 // we trust the template on this: 492 // if there's no inner, we're done 493 if !sc.isInline { 494 if sc.info == nil { 495 // This should not happen. 496 return sc, fail(errors.New("BUG: template info not set"), currItem) 497 } 498 if !sc.info.ParseInfo().IsInner { 499 return sc, nil 500 } 501 } 502 503 case currItem.IsShortcodeClose(): 504 next := pt.Peek() 505 if !sc.isInline { 506 if sc.info == nil || !sc.info.ParseInfo().IsInner { 507 if next.IsError() { 508 // return that error, more specific 509 continue 510 } 511 return sc, fail(errors.Errorf("shortcode %q has no .Inner, yet a closing tag was provided", next.Val), next) 512 } 513 } 514 if next.IsRightShortcodeDelim() { 515 // self-closing 516 pt.Consume(1) 517 } else { 518 sc.isClosing = true 519 pt.Consume(2) 520 } 521 522 return sc, nil 523 case currItem.IsText(): 524 sc.inner = append(sc.inner, currItem.ValStr()) 525 case currItem.Type == pageparser.TypeEmoji: 526 // TODO(bep) avoid the duplication of these "text cases", to prevent 527 // more of #6504 in the future. 528 val := currItem.ValStr() 529 if emoji := helpers.Emoji(val); emoji != nil { 530 sc.inner = append(sc.inner, string(emoji)) 531 } else { 532 sc.inner = append(sc.inner, val) 533 } 534 case currItem.IsShortcodeName(): 535 536 sc.name = currItem.ValStr() 537 538 // Used to check if the template expects inner content. 539 templs := s.s.Tmpl().LookupVariants(sc.name) 540 if templs == nil { 541 return nil, errors.Errorf("template for shortcode %q not found", sc.name) 542 } 543 544 sc.info = templs[0].(tpl.Info) 545 sc.templs = templs 546 case currItem.IsInlineShortcodeName(): 547 sc.name = currItem.ValStr() 548 sc.isInline = true 549 case currItem.IsShortcodeParam(): 550 if !pt.IsValueNext() { 551 continue 552 } else if pt.Peek().IsShortcodeParamVal() { 553 // named params 554 if sc.params == nil { 555 params := make(map[string]interface{}) 556 params[currItem.ValStr()] = pt.Next().ValTyped() 557 sc.params = params 558 } else { 559 if params, ok := sc.params.(map[string]interface{}); ok { 560 params[currItem.ValStr()] = pt.Next().ValTyped() 561 } else { 562 return sc, errShortCodeIllegalState 563 } 564 } 565 } else { 566 // positional params 567 if sc.params == nil { 568 var params []interface{} 569 params = append(params, currItem.ValTyped()) 570 sc.params = params 571 } else { 572 if params, ok := sc.params.([]interface{}); ok { 573 params = append(params, currItem.ValTyped()) 574 sc.params = params 575 } else { 576 return sc, errShortCodeIllegalState 577 } 578 } 579 } 580 case currItem.IsDone(): 581 // handled by caller 582 pt.Backup() 583 break Loop 584 585 } 586 } 587 return sc, nil 588 } 589 590 // Replace prefixed shortcode tokens with the real content. 591 // Note: This function will rewrite the input slice. 592 func replaceShortcodeTokens(source []byte, replacements map[string]string) ([]byte, error) { 593 if len(replacements) == 0 { 594 return source, nil 595 } 596 597 start := 0 598 599 pre := []byte(shortcodePlaceholderPrefix) 600 post := []byte("HBHB") 601 pStart := []byte("<p>") 602 pEnd := []byte("</p>") 603 604 k := bytes.Index(source[start:], pre) 605 606 for k != -1 { 607 j := start + k 608 postIdx := bytes.Index(source[j:], post) 609 if postIdx < 0 { 610 // this should never happen, but let the caller decide to panic or not 611 return nil, errors.New("illegal state in content; shortcode token missing end delim") 612 } 613 614 end := j + postIdx + 4 615 616 newVal := []byte(replacements[string(source[j:end])]) 617 618 // Issue #1148: Check for wrapping p-tags <p> 619 if j >= 3 && bytes.Equal(source[j-3:j], pStart) { 620 if (k+4) < len(source) && bytes.Equal(source[end:end+4], pEnd) { 621 j -= 3 622 end += 4 623 } 624 } 625 626 // This and other cool slice tricks: https://github.com/golang/go/wiki/SliceTricks 627 source = append(source[:j], append(newVal, source[end:]...)...) 628 start = j 629 k = bytes.Index(source[start:], pre) 630 631 } 632 633 return source, nil 634 } 635 636 func renderShortcodeWithPage(h tpl.TemplateHandler, tmpl tpl.Template, data *ShortcodeWithPage) (string, error) { 637 buffer := bp.GetBuffer() 638 defer bp.PutBuffer(buffer) 639 640 err := h.Execute(tmpl, buffer, data) 641 if err != nil { 642 return "", errors.Wrap(err, "failed to process shortcode") 643 } 644 return buffer.String(), nil 645 }