001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.doxia.util;
020
021import java.util.HashMap;
022import java.util.Map;
023
024/**
025 * Utility class for escaping and unescaping HTML/XML entities.
026 * This class provides methods that were previously supplied by Apache Commons Text.
027 *
028 * @since 2.1.0
029 */
030public class HtmlEntityUtils {
031
032    private static final Map<String, String> HTML4_ENTITIES = new HashMap<>();
033    private static final Map<String, String> XML_ENTITIES = new HashMap<>();
034
035    static {
036        // XML entities (subset also used in HTML)
037        XML_ENTITIES.put("quot", "\"");
038        XML_ENTITIES.put("amp", "&");
039        XML_ENTITIES.put("lt", "<");
040        XML_ENTITIES.put("gt", ">");
041        XML_ENTITIES.put("apos", "'");
042
043        // Initialize HTML4 entities with common XML entities (but NOT apos)
044        HTML4_ENTITIES.put("quot", "\"");
045        HTML4_ENTITIES.put("amp", "&");
046        HTML4_ENTITIES.put("lt", "<");
047        HTML4_ENTITIES.put("gt", ">");
048
049        // Add common HTML4 entities
050        HTML4_ENTITIES.put("nbsp", "\u00A0");
051        HTML4_ENTITIES.put("iexcl", "\u00A1");
052        HTML4_ENTITIES.put("cent", "\u00A2");
053        HTML4_ENTITIES.put("pound", "\u00A3");
054        HTML4_ENTITIES.put("curren", "\u00A4");
055        HTML4_ENTITIES.put("yen", "\u00A5");
056        HTML4_ENTITIES.put("brvbar", "\u00A6");
057        HTML4_ENTITIES.put("sect", "\u00A7");
058        HTML4_ENTITIES.put("uml", "\u00A8");
059        HTML4_ENTITIES.put("copy", "\u00A9");
060        HTML4_ENTITIES.put("ordf", "\u00AA");
061        HTML4_ENTITIES.put("laquo", "\u00AB");
062        HTML4_ENTITIES.put("not", "\u00AC");
063        HTML4_ENTITIES.put("shy", "\u00AD");
064        HTML4_ENTITIES.put("reg", "\u00AE");
065        HTML4_ENTITIES.put("macr", "\u00AF");
066        HTML4_ENTITIES.put("deg", "\u00B0");
067        HTML4_ENTITIES.put("plusmn", "\u00B1");
068        HTML4_ENTITIES.put("sup2", "\u00B2");
069        HTML4_ENTITIES.put("sup3", "\u00B3");
070        HTML4_ENTITIES.put("acute", "\u00B4");
071        HTML4_ENTITIES.put("micro", "\u00B5");
072        HTML4_ENTITIES.put("para", "\u00B6");
073        HTML4_ENTITIES.put("middot", "\u00B7");
074        HTML4_ENTITIES.put("cedil", "\u00B8");
075        HTML4_ENTITIES.put("sup1", "\u00B9");
076        HTML4_ENTITIES.put("ordm", "\u00BA");
077        HTML4_ENTITIES.put("raquo", "\u00BB");
078        HTML4_ENTITIES.put("frac14", "\u00BC");
079        HTML4_ENTITIES.put("frac12", "\u00BD");
080        HTML4_ENTITIES.put("frac34", "\u00BE");
081        HTML4_ENTITIES.put("iquest", "\u00BF");
082        HTML4_ENTITIES.put("Agrave", "\u00C0");
083        HTML4_ENTITIES.put("Aacute", "\u00C1");
084        HTML4_ENTITIES.put("Acirc", "\u00C2");
085        HTML4_ENTITIES.put("Atilde", "\u00C3");
086        HTML4_ENTITIES.put("Auml", "\u00C4");
087        HTML4_ENTITIES.put("Aring", "\u00C5");
088        HTML4_ENTITIES.put("AElig", "\u00C6");
089        HTML4_ENTITIES.put("Ccedil", "\u00C7");
090        HTML4_ENTITIES.put("Egrave", "\u00C8");
091        HTML4_ENTITIES.put("Eacute", "\u00C9");
092        HTML4_ENTITIES.put("Ecirc", "\u00CA");
093        HTML4_ENTITIES.put("Euml", "\u00CB");
094        HTML4_ENTITIES.put("Igrave", "\u00CC");
095        HTML4_ENTITIES.put("Iacute", "\u00CD");
096        HTML4_ENTITIES.put("Icirc", "\u00CE");
097        HTML4_ENTITIES.put("Iuml", "\u00CF");
098        HTML4_ENTITIES.put("ETH", "\u00D0");
099        HTML4_ENTITIES.put("Ntilde", "\u00D1");
100        HTML4_ENTITIES.put("Ograve", "\u00D2");
101        HTML4_ENTITIES.put("Oacute", "\u00D3");
102        HTML4_ENTITIES.put("Ocirc", "\u00D4");
103        HTML4_ENTITIES.put("Otilde", "\u00D5");
104        HTML4_ENTITIES.put("Ouml", "\u00D6");
105        HTML4_ENTITIES.put("times", "\u00D7");
106        HTML4_ENTITIES.put("Oslash", "\u00D8");
107        HTML4_ENTITIES.put("Ugrave", "\u00D9");
108        HTML4_ENTITIES.put("Uacute", "\u00DA");
109        HTML4_ENTITIES.put("Ucirc", "\u00DB");
110        HTML4_ENTITIES.put("Uuml", "\u00DC");
111        HTML4_ENTITIES.put("Yacute", "\u00DD");
112        HTML4_ENTITIES.put("THORN", "\u00DE");
113        HTML4_ENTITIES.put("szlig", "\u00DF");
114        HTML4_ENTITIES.put("agrave", "\u00E0");
115        HTML4_ENTITIES.put("aacute", "\u00E1");
116        HTML4_ENTITIES.put("acirc", "\u00E2");
117        HTML4_ENTITIES.put("atilde", "\u00E3");
118        HTML4_ENTITIES.put("auml", "\u00E4");
119        HTML4_ENTITIES.put("aring", "\u00E5");
120        HTML4_ENTITIES.put("aelig", "\u00E6");
121        HTML4_ENTITIES.put("ccedil", "\u00E7");
122        HTML4_ENTITIES.put("egrave", "\u00E8");
123        HTML4_ENTITIES.put("eacute", "\u00E9");
124        HTML4_ENTITIES.put("ecirc", "\u00EA");
125        HTML4_ENTITIES.put("euml", "\u00EB");
126        HTML4_ENTITIES.put("igrave", "\u00EC");
127        HTML4_ENTITIES.put("iacute", "\u00ED");
128        HTML4_ENTITIES.put("icirc", "\u00EE");
129        HTML4_ENTITIES.put("iuml", "\u00EF");
130        HTML4_ENTITIES.put("eth", "\u00F0");
131        HTML4_ENTITIES.put("ntilde", "\u00F1");
132        HTML4_ENTITIES.put("ograve", "\u00F2");
133        HTML4_ENTITIES.put("oacute", "\u00F3");
134        HTML4_ENTITIES.put("ocirc", "\u00F4");
135        HTML4_ENTITIES.put("otilde", "\u00F5");
136        HTML4_ENTITIES.put("ouml", "\u00F6");
137        HTML4_ENTITIES.put("divide", "\u00F7");
138        HTML4_ENTITIES.put("oslash", "\u00F8");
139        HTML4_ENTITIES.put("ugrave", "\u00F9");
140        HTML4_ENTITIES.put("uacute", "\u00FA");
141        HTML4_ENTITIES.put("ucirc", "\u00FB");
142        HTML4_ENTITIES.put("uuml", "\u00FC");
143        HTML4_ENTITIES.put("yacute", "\u00FD");
144        HTML4_ENTITIES.put("thorn", "\u00FE");
145        HTML4_ENTITIES.put("yuml", "\u00FF");
146        // Additional common entities
147        HTML4_ENTITIES.put("OElig", "\u0152");
148        HTML4_ENTITIES.put("oelig", "\u0153");
149        HTML4_ENTITIES.put("Scaron", "\u0160");
150        HTML4_ENTITIES.put("scaron", "\u0161");
151        HTML4_ENTITIES.put("Yuml", "\u0178");
152        HTML4_ENTITIES.put("fnof", "\u0192");
153        HTML4_ENTITIES.put("circ", "\u02C6");
154        HTML4_ENTITIES.put("tilde", "\u02DC");
155        HTML4_ENTITIES.put("Alpha", "\u0391");
156        HTML4_ENTITIES.put("Beta", "\u0392");
157        HTML4_ENTITIES.put("Gamma", "\u0393");
158        HTML4_ENTITIES.put("Delta", "\u0394");
159        HTML4_ENTITIES.put("Epsilon", "\u0395");
160        HTML4_ENTITIES.put("Zeta", "\u0396");
161        HTML4_ENTITIES.put("Eta", "\u0397");
162        HTML4_ENTITIES.put("Theta", "\u0398");
163        HTML4_ENTITIES.put("Iota", "\u0399");
164        HTML4_ENTITIES.put("Kappa", "\u039A");
165        HTML4_ENTITIES.put("Lambda", "\u039B");
166        HTML4_ENTITIES.put("Mu", "\u039C");
167        HTML4_ENTITIES.put("Nu", "\u039D");
168        HTML4_ENTITIES.put("Xi", "\u039E");
169        HTML4_ENTITIES.put("Omicron", "\u039F");
170        HTML4_ENTITIES.put("Pi", "\u03A0");
171        HTML4_ENTITIES.put("Rho", "\u03A1");
172        HTML4_ENTITIES.put("Sigma", "\u03A3");
173        HTML4_ENTITIES.put("Tau", "\u03A4");
174        HTML4_ENTITIES.put("Upsilon", "\u03A5");
175        HTML4_ENTITIES.put("Phi", "\u03A6");
176        HTML4_ENTITIES.put("Chi", "\u03A7");
177        HTML4_ENTITIES.put("Psi", "\u03A8");
178        HTML4_ENTITIES.put("Omega", "\u03A9");
179        HTML4_ENTITIES.put("alpha", "\u03B1");
180        HTML4_ENTITIES.put("beta", "\u03B2");
181        HTML4_ENTITIES.put("gamma", "\u03B3");
182        HTML4_ENTITIES.put("delta", "\u03B4");
183        HTML4_ENTITIES.put("epsilon", "\u03B5");
184        HTML4_ENTITIES.put("zeta", "\u03B6");
185        HTML4_ENTITIES.put("eta", "\u03B7");
186        HTML4_ENTITIES.put("theta", "\u03B8");
187        HTML4_ENTITIES.put("iota", "\u03B9");
188        HTML4_ENTITIES.put("kappa", "\u03BA");
189        HTML4_ENTITIES.put("lambda", "\u03BB");
190        HTML4_ENTITIES.put("mu", "\u03BC");
191        HTML4_ENTITIES.put("nu", "\u03BD");
192        HTML4_ENTITIES.put("xi", "\u03BE");
193        HTML4_ENTITIES.put("omicron", "\u03BF");
194        HTML4_ENTITIES.put("pi", "\u03C0");
195        HTML4_ENTITIES.put("rho", "\u03C1");
196        HTML4_ENTITIES.put("sigmaf", "\u03C2");
197        HTML4_ENTITIES.put("sigma", "\u03C3");
198        HTML4_ENTITIES.put("tau", "\u03C4");
199        HTML4_ENTITIES.put("upsilon", "\u03C5");
200        HTML4_ENTITIES.put("phi", "\u03C6");
201        HTML4_ENTITIES.put("chi", "\u03C7");
202        HTML4_ENTITIES.put("psi", "\u03C8");
203        HTML4_ENTITIES.put("omega", "\u03C9");
204        HTML4_ENTITIES.put("thetasym", "\u03D1");
205        HTML4_ENTITIES.put("upsih", "\u03D2");
206        HTML4_ENTITIES.put("piv", "\u03D6");
207        HTML4_ENTITIES.put("ensp", "\u2002");
208        HTML4_ENTITIES.put("emsp", "\u2003");
209        HTML4_ENTITIES.put("thinsp", "\u2009");
210        HTML4_ENTITIES.put("zwnj", "\u200C");
211        HTML4_ENTITIES.put("zwj", "\u200D");
212        HTML4_ENTITIES.put("lrm", "\u200E");
213        HTML4_ENTITIES.put("rlm", "\u200F");
214        HTML4_ENTITIES.put("ndash", "\u2013");
215        HTML4_ENTITIES.put("mdash", "\u2014");
216        HTML4_ENTITIES.put("lsquo", "\u2018");
217        HTML4_ENTITIES.put("rsquo", "\u2019");
218        HTML4_ENTITIES.put("sbquo", "\u201A");
219        HTML4_ENTITIES.put("ldquo", "\u201C");
220        HTML4_ENTITIES.put("rdquo", "\u201D");
221        HTML4_ENTITIES.put("bdquo", "\u201E");
222        HTML4_ENTITIES.put("dagger", "\u2020");
223        HTML4_ENTITIES.put("Dagger", "\u2021");
224        HTML4_ENTITIES.put("bull", "\u2022");
225        HTML4_ENTITIES.put("hellip", "\u2026");
226        HTML4_ENTITIES.put("permil", "\u2030");
227        HTML4_ENTITIES.put("prime", "\u2032");
228        HTML4_ENTITIES.put("Prime", "\u2033");
229        HTML4_ENTITIES.put("lsaquo", "\u2039");
230        HTML4_ENTITIES.put("rsaquo", "\u203A");
231        HTML4_ENTITIES.put("oline", "\u203E");
232        HTML4_ENTITIES.put("frasl", "\u2044");
233        HTML4_ENTITIES.put("euro", "\u20AC");
234        HTML4_ENTITIES.put("image", "\u2111");
235        HTML4_ENTITIES.put("weierp", "\u2118");
236        HTML4_ENTITIES.put("real", "\u211C");
237        HTML4_ENTITIES.put("trade", "\u2122");
238        HTML4_ENTITIES.put("alefsym", "\u2135");
239        HTML4_ENTITIES.put("larr", "\u2190");
240        HTML4_ENTITIES.put("uarr", "\u2191");
241        HTML4_ENTITIES.put("rarr", "\u2192");
242        HTML4_ENTITIES.put("darr", "\u2193");
243        HTML4_ENTITIES.put("harr", "\u2194");
244        HTML4_ENTITIES.put("crarr", "\u21B5");
245        HTML4_ENTITIES.put("lArr", "\u21D0");
246        HTML4_ENTITIES.put("uArr", "\u21D1");
247        HTML4_ENTITIES.put("rArr", "\u21D2");
248        HTML4_ENTITIES.put("dArr", "\u21D3");
249        HTML4_ENTITIES.put("hArr", "\u21D4");
250        HTML4_ENTITIES.put("forall", "\u2200");
251        HTML4_ENTITIES.put("part", "\u2202");
252        HTML4_ENTITIES.put("exist", "\u2203");
253        HTML4_ENTITIES.put("empty", "\u2205");
254        HTML4_ENTITIES.put("nabla", "\u2207");
255        HTML4_ENTITIES.put("isin", "\u2208");
256        HTML4_ENTITIES.put("notin", "\u2209");
257        HTML4_ENTITIES.put("ni", "\u220B");
258        HTML4_ENTITIES.put("prod", "\u220F");
259        HTML4_ENTITIES.put("sum", "\u2211");
260        HTML4_ENTITIES.put("minus", "\u2212");
261        HTML4_ENTITIES.put("lowast", "\u2217");
262        HTML4_ENTITIES.put("radic", "\u221A");
263        HTML4_ENTITIES.put("prop", "\u221D");
264        HTML4_ENTITIES.put("infin", "\u221E");
265        HTML4_ENTITIES.put("ang", "\u2220");
266        HTML4_ENTITIES.put("and", "\u2227");
267        HTML4_ENTITIES.put("or", "\u2228");
268        HTML4_ENTITIES.put("cap", "\u2229");
269        HTML4_ENTITIES.put("cup", "\u222A");
270        HTML4_ENTITIES.put("int", "\u222B");
271        HTML4_ENTITIES.put("there4", "\u2234");
272        HTML4_ENTITIES.put("sim", "\u223C");
273        HTML4_ENTITIES.put("cong", "\u2245");
274        HTML4_ENTITIES.put("asymp", "\u2248");
275        HTML4_ENTITIES.put("ne", "\u2260");
276        HTML4_ENTITIES.put("equiv", "\u2261");
277        HTML4_ENTITIES.put("le", "\u2264");
278        HTML4_ENTITIES.put("ge", "\u2265");
279        HTML4_ENTITIES.put("sub", "\u2282");
280        HTML4_ENTITIES.put("sup", "\u2283");
281        HTML4_ENTITIES.put("nsub", "\u2284");
282        HTML4_ENTITIES.put("sube", "\u2286");
283        HTML4_ENTITIES.put("supe", "\u2287");
284        HTML4_ENTITIES.put("oplus", "\u2295");
285        HTML4_ENTITIES.put("otimes", "\u2297");
286        HTML4_ENTITIES.put("perp", "\u22A5");
287        HTML4_ENTITIES.put("sdot", "\u22C5");
288        HTML4_ENTITIES.put("lceil", "\u2308");
289        HTML4_ENTITIES.put("rceil", "\u2309");
290        HTML4_ENTITIES.put("lfloor", "\u230A");
291        HTML4_ENTITIES.put("rfloor", "\u230B");
292        HTML4_ENTITIES.put("lang", "\u2329");
293        HTML4_ENTITIES.put("rang", "\u232A");
294        HTML4_ENTITIES.put("loz", "\u25CA");
295        HTML4_ENTITIES.put("spades", "\u2660");
296        HTML4_ENTITIES.put("clubs", "\u2663");
297        HTML4_ENTITIES.put("hearts", "\u2665");
298        HTML4_ENTITIES.put("diams", "\u2666");
299    }
300
301    private HtmlEntityUtils() {
302        // utility class
303    }
304
305    /**
306     * Unescapes XML entities in a string.
307     *
308     * @param text the text to unescape
309     * @return the unescaped text
310     */
311    public static String unescapeXml(String text) {
312        return unescapeEntities(text, XML_ENTITIES);
313    }
314
315    /**
316     * Unescapes HTML4 entities in a string.
317     *
318     * @param text the text to unescape
319     * @return the unescaped text
320     */
321    public static String unescapeHtml4(String text) {
322        return unescapeEntities(text, HTML4_ENTITIES);
323    }
324
325    private static String unescapeEntities(String text, Map<String, String> entities) {
326        if (text == null || text.isEmpty()) {
327            return text;
328        }
329
330        StringBuilder result = new StringBuilder(text.length());
331        int i = 0;
332        int len = text.length();
333
334        while (i < len) {
335            char c = text.charAt(i);
336            if (c == '&') {
337                int end = text.indexOf(';', i + 1);
338                if (end != -1) {
339                    String entityContent = text.substring(i + 1, end);
340                    String replacement = null;
341
342                    // Check for numeric entity (&#123; or &#xAB;)
343                    if (entityContent.length() > 0 && entityContent.charAt(0) == '#') {
344                        try {
345                            if (entityContent.length() > 1 && entityContent.charAt(1) == 'x') {
346                                // Hexadecimal - need at least "x" plus one hex digit
347                                if (entityContent.length() > 2) {
348                                    int codePoint = Integer.parseInt(entityContent.substring(2), 16);
349                                    replacement = new String(Character.toChars(codePoint));
350                                }
351                            } else if (entityContent.length() > 1) {
352                                // Decimal - need at least one decimal digit after '#'
353                                int codePoint = Integer.parseInt(entityContent.substring(1));
354                                replacement = new String(Character.toChars(codePoint));
355                            }
356                        } catch (NumberFormatException e) {
357                            // Invalid numeric entity, leave as is
358                        } catch (IllegalArgumentException e) {
359                            // Invalid code point, leave as is
360                        }
361                    } else {
362                        // Named entity
363                        replacement = entities.get(entityContent);
364                    }
365
366                    if (replacement != null) {
367                        result.append(replacement);
368                        i = end + 1;
369                        continue;
370                    }
371                }
372            }
373            result.append(c);
374            i++;
375        }
376
377        return result.toString();
378    }
379}