1 module hunt.markdown.ext.heading.anchor.IdGenerator; 2 3 import hunt.collection.HashMap; 4 import hunt.collection.Map; 5 import hunt.Integer; 6 7 import std.string; 8 import std.regex; 9 import std.conv : to; 10 11 import hunt.text; 12 import hunt.util.StringBuilder; 13 /** 14 * Generates strings to be used as identifiers. 15 * <p> 16 * Use {@link #builder()} to create an instance. 17 */ 18 class IdGenerator { 19 private Regex!char allowedCharacters; 20 private Map!(string, int) identityMap; 21 private string prefix; 22 private string suffix; 23 private string defaultIdentifier; 24 25 private this(Builder builder) { 26 this.allowedCharacters = compileAllowedCharactersPattern(); 27 this.defaultIdentifier = builder._defaultIdentifier; 28 this.prefix = builder._prefix; 29 this.suffix = builder._suffix; 30 this.identityMap = new HashMap!(string, int)(); 31 } 32 33 /** 34 * @return a new builder with default arguments 35 */ 36 public static Builder builder() { 37 return new Builder(); 38 } 39 40 /** 41 * <p> 42 * Generate an ID based on the provided text and previously generated IDs. 43 * <p> 44 * This method is not thread safe, concurrent calls can end up 45 * with non-unique identifiers. 46 * <p> 47 * Note that collision can occur in the case that 48 * <ul> 49 * <li>Method called with 'X'</li> 50 * <li>Method called with 'X' again</li> 51 * <li>Method called with 'X-1'</li> 52 * </ul> 53 * <p> 54 * In that case, the three generated IDs will be: 55 * <ul> 56 * <li>X</li> 57 * <li>X-1</li> 58 * <li>X-1</li> 59 * </ul> 60 * <p> 61 * Therefore if collisions are unacceptable you should ensure that 62 * numbers are stripped from end of {@code text}. 63 * 64 * @param text Text that the identifier should be based on. Will be normalised, then used to generate the 65 * identifier. 66 * @return {@code text} if this is the first instance that the {@code text} has been passed 67 * to the method. Otherwise, {@code text ~ "-" ~ X} will be returned, where X is the number of times 68 * that {@code text} has previously been passed in. If {@code text} is empty, the default 69 * identifier given in the constructor will be used. 70 */ 71 public string generateId(string text) { 72 string normalizedIdentity = text !is null ? normalizeText(text) : defaultIdentifier; 73 74 if (normalizedIdentity.length == 0) { 75 normalizedIdentity = defaultIdentifier; 76 } 77 78 if (!identityMap.containsKey(normalizedIdentity)) { 79 identityMap.put(normalizedIdentity, 1); 80 return prefix ~ normalizedIdentity ~ suffix; 81 } else { 82 int currentCount = identityMap.get(normalizedIdentity); 83 identityMap.put(normalizedIdentity, currentCount + 1); 84 return prefix ~ normalizedIdentity ~ "-" ~ currentCount.to!string() ~ suffix; 85 } 86 } 87 88 private static Regex!char compileAllowedCharactersPattern() { 89 return regex("[\\w\\-_]+"); 90 } 91 92 /** 93 * Assume we've been given a space separated text. 94 * 95 * @param text Text to normalize to an ID 96 */ 97 // private string normalizeText(string text) { 98 // string firstPassNormalising = text.toLower().replace(" ", "-"); 99 100 // StringBuilder sb = new StringBuilder(); 101 // Matcher matcher = allowedCharacters.matcher(firstPassNormalising); 102 103 // while (matcher.find()) { 104 // sb.append(matcher.group()); 105 // } 106 107 // return sb.toString(); 108 // } 109 110 private string normalizeText(string text) { 111 string firstPassNormalising = text.toLower().replace(" ", "-"); 112 113 StringBuilder sb = new StringBuilder(); 114 115 foreach (c ; matchAll(firstPassNormalising, allowedCharacters)) { 116 sb.append(c[0]); 117 } 118 119 return sb.toString(); 120 } 121 122 public static class Builder { 123 private string _defaultIdentifier = "id"; 124 private string _prefix = ""; 125 private string _suffix = ""; 126 127 public IdGenerator build() { 128 return new IdGenerator(this); 129 } 130 131 /** 132 * @param defaultId the default identifier to use in case the provided text is empty or only contains unusable characters 133 * @return {@code this} 134 */ 135 public Builder defaultId(string defaultId) { 136 this._defaultIdentifier = defaultId; 137 return this; 138 } 139 140 /** 141 * @param prefix the text to place before the generated identity 142 * @return {@code this} 143 */ 144 public Builder prefix(string prefix) { 145 this._prefix = prefix; 146 return this; 147 } 148 149 /** 150 * @param suffix the text to place after the generated identity 151 * @return {@code this} 152 */ 153 public Builder suffix(string suffix) { 154 this._suffix = suffix; 155 return this; 156 } 157 } 158 }