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 }