autoid.go (3222B)
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 goldmark 15 16 import ( 17 "bytes" 18 "strconv" 19 "unicode" 20 "unicode/utf8" 21 22 "github.com/gohugoio/hugo/markup/blackfriday" 23 24 "github.com/gohugoio/hugo/markup/goldmark/goldmark_config" 25 26 "github.com/gohugoio/hugo/common/text" 27 28 "github.com/yuin/goldmark/ast" 29 "github.com/yuin/goldmark/parser" 30 "github.com/yuin/goldmark/util" 31 32 bp "github.com/gohugoio/hugo/bufferpool" 33 ) 34 35 func sanitizeAnchorNameString(s string, idType string) string { 36 return string(sanitizeAnchorName([]byte(s), idType)) 37 } 38 39 func sanitizeAnchorName(b []byte, idType string) []byte { 40 return sanitizeAnchorNameWithHook(b, idType, nil) 41 } 42 43 func sanitizeAnchorNameWithHook(b []byte, idType string, hook func(buf *bytes.Buffer)) []byte { 44 buf := bp.GetBuffer() 45 46 if idType == goldmark_config.AutoHeadingIDTypeBlackfriday { 47 // TODO(bep) make it more efficient. 48 buf.WriteString(blackfriday.SanitizedAnchorName(string(b))) 49 } else { 50 asciiOnly := idType == goldmark_config.AutoHeadingIDTypeGitHubAscii 51 52 if asciiOnly { 53 // Normalize it to preserve accents if possible. 54 b = text.RemoveAccents(b) 55 } 56 57 b = bytes.TrimSpace(b) 58 59 for len(b) > 0 { 60 r, size := utf8.DecodeRune(b) 61 switch { 62 case asciiOnly && size != 1: 63 case r == '-' || r == ' ': 64 buf.WriteRune('-') 65 case isAlphaNumeric(r): 66 buf.WriteRune(unicode.ToLower(r)) 67 default: 68 } 69 70 b = b[size:] 71 } 72 } 73 74 if hook != nil { 75 hook(buf) 76 } 77 78 result := make([]byte, buf.Len()) 79 copy(result, buf.Bytes()) 80 81 bp.PutBuffer(buf) 82 83 return result 84 } 85 86 func isAlphaNumeric(r rune) bool { 87 return r == '_' || unicode.IsLetter(r) || unicode.IsDigit(r) 88 } 89 90 var _ parser.IDs = (*idFactory)(nil) 91 92 type idFactory struct { 93 idType string 94 vals map[string]struct{} 95 } 96 97 func newIDFactory(idType string) *idFactory { 98 return &idFactory{ 99 vals: make(map[string]struct{}), 100 idType: idType, 101 } 102 } 103 104 func (ids *idFactory) Generate(value []byte, kind ast.NodeKind) []byte { 105 return sanitizeAnchorNameWithHook(value, ids.idType, func(buf *bytes.Buffer) { 106 if buf.Len() == 0 { 107 if kind == ast.KindHeading { 108 buf.WriteString("heading") 109 } else { 110 buf.WriteString("id") 111 } 112 } 113 114 if _, found := ids.vals[util.BytesToReadOnlyString(buf.Bytes())]; found { 115 // Append a hypen and a number, starting with 1. 116 buf.WriteRune('-') 117 pos := buf.Len() 118 for i := 1; ; i++ { 119 buf.WriteString(strconv.Itoa(i)) 120 if _, found := ids.vals[util.BytesToReadOnlyString(buf.Bytes())]; !found { 121 break 122 } 123 buf.Truncate(pos) 124 } 125 } 126 127 ids.vals[buf.String()] = struct{}{} 128 }) 129 } 130 131 func (ids *idFactory) Put(value []byte) { 132 ids.vals[util.BytesToReadOnlyString(value)] = struct{}{} 133 }