// Copyright 2015 The Hugo Authors. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package hugolib import ( "bytes" "errors" "fmt" "html/template" "reflect" "regexp" "sort" "strings" "sync" bp "github.com/spf13/hugo/bufferpool" "github.com/spf13/hugo/helpers" "github.com/spf13/hugo/tpl" jww "github.com/spf13/jwalterweatherman" ) // ShortcodeWithPage is the "." context in a shortcode template. type ShortcodeWithPage struct { Params interface{} Inner template.HTML Page *Page Parent *ShortcodeWithPage IsNamedParams bool scratch *Scratch } // Site returns information about the current site. func (scp *ShortcodeWithPage) Site() *SiteInfo { return scp.Page.Site } // Ref is a shortcut to the Ref method on Page. func (scp *ShortcodeWithPage) Ref(ref string) (string, error) { return scp.Page.Ref(ref) } // RelRef is a shortcut to the RelRef method on Page. func (scp *ShortcodeWithPage) RelRef(ref string) (string, error) { return scp.Page.RelRef(ref) } // Scratch returns a scratch-pad scoped for this shortcode. This can be used // as a temporary storage for variables, counters etc. func (scp *ShortcodeWithPage) Scratch() *Scratch { if scp.scratch == nil { scp.scratch = newScratch() } return scp.scratch } // Get is a convenience method to look up shortcode parameters by its key. func (scp *ShortcodeWithPage) Get(key interface{}) interface{} { if reflect.ValueOf(scp.Params).Len() == 0 { return nil } var x reflect.Value switch key.(type) { case int64, int32, int16, int8, int: if reflect.TypeOf(scp.Params).Kind() == reflect.Map { return "error: cannot access named params by position" } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { idx := int(reflect.ValueOf(key).Int()) ln := reflect.ValueOf(scp.Params).Len() if idx > ln-1 { helpers.DistinctErrorLog.Printf("No shortcode param at .Get %d in page %s, have params: %v", idx, scp.Page.FullFilePath(), scp.Params) return fmt.Sprintf("error: index out of range for positional param at position %d", idx) } x = reflect.ValueOf(scp.Params).Index(idx) } case string: if reflect.TypeOf(scp.Params).Kind() == reflect.Map { x = reflect.ValueOf(scp.Params).MapIndex(reflect.ValueOf(key)) if !x.IsValid() { return "" } } else if reflect.TypeOf(scp.Params).Kind() == reflect.Slice { if reflect.ValueOf(scp.Params).Len() == 1 && reflect.ValueOf(scp.Params).Index(0).String() == "" { return nil } return "error: cannot access positional params by string name" } } switch x.Kind() { case reflect.String: return x.String() case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int: return x.Int() default: return x } } // Note - this value must not contain any markup syntax const shortcodePlaceholderPrefix = "HUGOSHORTCODE" type shortcode struct { name string inner []interface{} // string or nested shortcode params interface{} // map or array err error doMarkup bool } func (sc shortcode) String() string { // for testing (mostly), so any change here will break tests! var params interface{} switch v := sc.params.(type) { case map[string]string: // sort the keys so test assertions won't fail var keys []string for k := range v { keys = append(keys, k) } sort.Strings(keys) var tmp = make([]string, len(keys)) for i, k := range keys { tmp[i] = k + ":" + v[k] } params = tmp default: // use it as is params = sc.params } return fmt.Sprintf("%s(%q, %t){%s}", sc.name, params, sc.doMarkup, sc.inner) } // HandleShortcodes does all in one go: extract, render and replace // only used for testing func HandleShortcodes(stringToParse string, page *Page, t tpl.Template) (string, error) { tmpContent, tmpShortcodes, err := extractAndRenderShortcodes(stringToParse, page, t) if err != nil { return "", err } if len(tmpShortcodes) > 0 { tmpContentWithTokensReplaced, err := replaceShortcodeTokens([]byte(tmpContent), shortcodePlaceholderPrefix, tmpShortcodes) if err != nil { return "", fmt.Errorf("Fail to replace short code tokens in %s:\n%s", page.BaseFileName(), err.Error()) } return string(tmpContentWithTokensReplaced), nil } return tmpContent, nil } var isInnerShortcodeCache = struct { sync.RWMutex m map[string]bool }{m: make(map[string]bool)} // to avoid potential costly look-aheads for closing tags we look inside the template itself // we could change the syntax to self-closing tags, but that would make users cry // the value found is cached func isInnerShortcode(t *template.Template) (bool, error) { isInnerShortcodeCache.RLock() m, ok := isInnerShortcodeCache.m[t.Name()] isInnerShortcodeCache.RUnlock() if ok { return m, nil } isInnerShortcodeCache.Lock() defer isInnerShortcodeCache.Unlock() if t.Tree == nil { return false, errors.New("Template failed to compile") } match, _ := regexp.MatchString("{{.*?\\.Inner.*?}}", t.Tree.Root.String()) isInnerShortcodeCache.m[t.Name()] = match return match, nil } func createShortcodePlaceholder(id int) string { return fmt.Sprintf("{#{#%s-%d#}#}", shortcodePlaceholderPrefix, id) } const innerNewlineRegexp = "\n" const innerCleanupRegexp = `\A
(.*)
\n\z` const innerCleanupExpand = "$1" func renderShortcode(sc shortcode, parent *ShortcodeWithPage, p *Page, t tpl.Template) string { tmpl := getShortcodeTemplate(sc.name, t) if tmpl == nil { jww.ERROR.Printf("Unable to locate template for shortcode '%s' in page %s", sc.name, p.BaseFileName()) return "" } data := &ShortcodeWithPage{Params: sc.params, Page: p, Parent: parent} if sc.params != nil { data.IsNamedParams = reflect.TypeOf(sc.params).Kind() == reflect.Map } if len(sc.inner) > 0 { var inner string for _, innerData := range sc.inner { switch innerData.(type) { case string: inner += innerData.(string) case shortcode: inner += renderShortcode(innerData.(shortcode), data, p, t) default: jww.ERROR.Printf("Illegal state on shortcode rendering of '%s' in page %s. Illegal type in inner data: %s ", sc.name, p.BaseFileName(), reflect.TypeOf(innerData)) return "" } } if sc.doMarkup { newInner := helpers.RenderBytes(&helpers.RenderingContext{ Content: []byte(inner), PageFmt: p.determineMarkupType(), DocumentID: p.UniqueID(), Config: p.getRenderingConfig()}) // If the type is “unknown” or “markdown”, we assume the markdown // generation has been performed. Given the input: `a line`, markdown // specifies the HTML `a line
\n`. When dealing with documents as a // whole, this is OK. When dealing with an `{{ .Inner }}` block in Hugo, // this is not so good. This code does two things: // // 1. Check to see if inner has a newline in it. If so, the Inner data is // unchanged. // 2 If inner does not have a newline, strip the wrappingblock and // the newline. This was previously tricked out by wrapping shortcode // substitutions in
") pEnd := []byte("
") k := bytes.Index(source[start:], pre) for k != -1 { j := start + k postIdx := bytes.Index(source[j:], post) if postIdx < 0 { // this should never happen, but let the caller decide to panic or not return nil, errors.New("illegal state in content; shortcode token missing end delim") } end := j + postIdx + 4 newVal := []byte(replacements[string(source[j:end])]) // Issue #1148: Check for wrapping p-tagsif j >= 3 && bytes.Equal(source[j-3:j], pStart) { if (k+4) < sourceLen && bytes.Equal(source[end:end+4], pEnd) { j -= 3 end += 4 } } // This and other cool slice tricks: https://github.com/golang/go/wiki/SliceTricks source = append(source[:j], append(newVal, source[end:]...)...) start = j k = bytes.Index(source[start:], pre) } return source, nil } func getShortcodeTemplate(name string, t tpl.Template) *template.Template { if x := t.Lookup("shortcodes/" + name + ".html"); x != nil { return x } if x := t.Lookup("theme/shortcodes/" + name + ".html"); x != nil { return x } return t.Lookup("_internal/shortcodes/" + name + ".html") } func renderShortcodeWithPage(tmpl *template.Template, data *ShortcodeWithPage) string { buffer := bp.GetBuffer() defer bp.PutBuffer(buffer) err := tmpl.Execute(buffer, data) if err != nil { jww.ERROR.Println("error processing shortcode", tmpl.Name(), "\n ERR:", err) jww.WARN.Println(data) } return buffer.String() }