Update packaging for squeeze
[python-htmlentities.git] / htmlentities.py
1 #!/usr/bin/env python
2 # -*- encoding: utf-8 -*-
3
4 __all__ = ['resolve', 'expand', 'cleanCDATA']
5
6 entities = {
7         u'nbsp': 160,
8         u'iexcl': 161,
9         u'cent': 162,
10         u'pound': 163,
11         u'curren': 164,
12         u'yen': 165,
13         u'brvbar': 166,
14         u'sect': 167,
15         u'uml': 168,
16         u'copy': 169,
17         u'ordf': 170,
18         u'laquo': 171,
19         u'not': 172,
20         u'shy': 173,
21         u'reg': 174,
22         u'macr': 175,
23         u'deg': 176,
24         u'plusmn': 177,
25         u'sup2': 178,
26         u'sup3': 179,
27         u'acute': 180,
28         u'micro': 181,
29         u'para': 182,
30         u'middot': 183,
31         u'cedil': 184,
32         u'sup1': 185,
33         u'ordm': 186,
34         u'raquo': 187,
35         u'frac14': 188,
36         u'frac12': 189,
37         u'frac34': 190,
38         u'iquest': 191,
39         u'Agrave': 192,
40         u'Aacute': 193,
41         u'Acirc': 194,
42         u'Atilde': 195,
43         u'Auml': 196,
44         u'Aring': 197,
45         u'AElig': 198,
46         u'Ccedil': 199,
47         u'Egrave': 200,
48         u'Eacute': 201,
49         u'Ecirc': 202,
50         u'Euml': 203,
51         u'Igrave': 204,
52         u'Iacute': 205,
53         u'Icirc': 206,
54         u'Iuml': 207,
55         u'ETH': 208,
56         u'Ntilde': 209,
57         u'Ograve': 210,
58         u'Oacute': 211,
59         u'Ocirc': 212,
60         u'Otilde': 213,
61         u'Ouml': 214,
62         u'times': 215,
63         u'Oslash': 216,
64         u'Ugrave': 217,
65         u'Uacute': 218,
66         u'Ucirc': 219,
67         u'Uuml': 220,
68         u'Yacute': 221,
69         u'THORN': 222,
70         u'szlig': 223,
71         u'agrave': 224,
72         u'aacute': 225,
73         u'acirc': 226,
74         u'atilde': 227,
75         u'auml': 228,
76         u'aring': 229,
77         u'aelig': 230,
78         u'ccedil': 231,
79         u'egrave': 232,
80         u'eacute': 233,
81         u'ecirc': 234,
82         u'euml': 235,
83         u'igrave': 236,
84         u'iacute': 237,
85         u'icirc': 238,
86         u'iuml': 239,
87         u'eth': 240,
88         u'ntilde': 241,
89         u'ograve': 242,
90         u'oacute': 243,
91         u'ocirc': 244,
92         u'otilde': 245,
93         u'ouml': 246,
94         u'divide': 247,
95         u'oslash': 248,
96         u'ugrave': 249,
97         u'uacute': 250,
98         u'ucirc': 251,
99         u'uuml': 252,
100         u'yacute': 253,
101         u'thorn': 254,
102         u'yuml': 255,
103         u'fnof': 402,
104         u'Alpha': 913,
105         u'Beta': 914,
106         u'Gamma': 915,
107         u'Delta': 916,
108         u'Epsilon': 917,
109         u'Zeta': 918,
110         u'Eta': 919,
111         u'Theta': 920,
112         u'Iota': 921,
113         u'Kappa': 922,
114         u'Lambda': 923,
115         u'Mu': 924,
116         u'Nu': 925,
117         u'Xi': 926,
118         u'Omicron': 927,
119         u'Pi': 928,
120         u'Rho': 929,
121         u'Sigma': 931,
122         u'Tau': 932,
123         u'Upsilon': 933,
124         u'Phi': 934,
125         u'Chi': 935,
126         u'Psi': 936,
127         u'Omega': 937,
128         u'alpha': 945,
129         u'beta': 946,
130         u'gamma': 947,
131         u'delta': 948,
132         u'epsilon': 949,
133         u'zeta': 950,
134         u'eta': 951,
135         u'theta': 952,
136         u'iota': 953,
137         u'kappa': 954,
138         u'lambda': 955,
139         u'mu': 956,
140         u'nu': 957,
141         u'xi': 958,
142         u'omicron': 959,
143         u'pi': 960,
144         u'rho': 961,
145         u'sigmaf': 962,
146         u'sigma': 963,
147         u'tau': 964,
148         u'upsilon': 965,
149         u'phi': 966,
150         u'chi': 967,
151         u'psi': 968,
152         u'omega': 969,
153         u'thetasym': 977,
154         u'upsih': 978,
155         u'piv': 982,
156         u'bull': 8226,
157         u'hellip': 8230,
158         u'prime': 8242,
159         u'Prime': 8243,
160         u'oline': 8254,
161         u'frasl': 8260,
162         u'weierp': 8472,
163         u'image': 8465,
164         u'real': 8476,
165         u'trade': 8482,
166         u'alefsym': 8501,
167         u'larr': 8592,
168         u'uarr': 8593,
169         u'rarr': 8594,
170         u'darr': 8595,
171         u'harr': 8596,
172         u'crarr': 8629,
173         u'lArr': 8656,
174         u'uArr': 8657,
175         u'rArr': 8658,
176         u'dArr': 8659,
177         u'hArr': 8660,
178         u'forall': 8704,
179         u'part': 8706,
180         u'exist': 8707,
181         u'empty': 8709,
182         u'nabla': 8711,
183         u'isin': 8712,
184         u'notin': 8713,
185         u'ni': 8715,
186         u'prod': 8719,
187         u'sum': 8721,
188         u'minus': 8722,
189         u'lowast': 8727,
190         u'radic': 8730,
191         u'prop': 8733,
192         u'infin': 8734,
193         u'ang': 8736,
194         u'and': 8743,
195         u'or': 8744,
196         u'cap': 8745,
197         u'cup': 8746,
198         u'int': 8747,
199         u'there4': 8756,
200         u'sim': 8764,
201         u'cong': 8773,
202         u'asymp': 8776,
203         u'ne': 8800,
204         u'equiv': 8801,
205         u'le': 8804,
206         u'ge': 8805,
207         u'sub': 8834,
208         u'sup': 8835,
209         u'nsub': 8836,
210         u'sube': 8838,
211         u'supe': 8839,
212         u'oplus': 8853,
213         u'otimes': 8855,
214         u'perp': 8869,
215         u'sdot': 8901,
216         u'lceil': 8968,
217         u'rceil': 8969,
218         u'lfloor': 8970,
219         u'rfloor': 8971,
220         u'lang': 9001,
221         u'rang': 9002,
222         u'loz': 9674,
223         u'spades': 9824,
224         u'clubs': 9827,
225         u'hearts': 9829,
226         u'diams': 9830,
227         u'quot': 34,
228         u'amp': 38,
229         u'lt': 60,
230         u'gt': 62,
231         u'OElig': 338,
232         u'oelig': 339,
233         u'Scaron': 352,
234         u'scaron': 353,
235         u'Yuml': 376,
236         u'circ': 710,
237         u'tilde': 732,
238         u'ensp': 8194,
239         u'emsp': 8195,
240         u'thinsp': 8201,
241         u'zwnj': 8204,
242         u'zwj': 8205,
243         u'lrm': 8206,
244         u'rlm': 8207,
245         u'ndash': 8211,
246         u'mdash': 8212,
247         u'lsquo': 8216,
248         u'rsquo': 8217,
249         u'sbquo': 8218,
250         u'ldquo': 8220,
251         u'rdquo': 8221,
252         u'bdquo': 8222,
253         u'dagger': 8224,
254         u'Dagger': 8225,
255         u'permil': 8240,
256         u'lsaquo': 8249,
257         u'rsaquo': 8250,
258         u'euro': 8364,
259 }
260
261 entities_autocomplete = {}
262 longestEntityLen = 0
263
264 for key,value in entities.iteritems():
265     if value<=255:
266         entities_autocomplete[key] = value
267     l = len(key)
268     if l>longestEntityLen:
269         longestEntityLen = l
270
271 # Characters in range 127-159 are illegals, but they are sometimes wrongly used in web pages
272 # Internet Explorer assumes it is taken from Microsoft extension to Latin 1 page 8859-1 aka CP1512
273 # However, to be clean, we must remap them to their real unicode values
274 # Unknown codes are translated into a space
275 iso88591_remap = [
276         32,             # 127: ???
277         8364,   # 128: Euro symbol
278         32,             # 129: ???
279         8218,   # 130: Single Low-9 Quotation Mark
280         402,    # 131: Latin Small Letter F With Hook
281         8222,   # 132: Double Low-9 Quotation Mark
282         8230,   # 133: Horizontal Ellipsis
283         8224,   # 134: Dagger
284         8225,   # 135: Double Dagger
285         710,    # 136: Modifier Letter Circumflex Accent
286         8240,   # 137: Per Mille Sign
287         352,    # 138: Latin Capital Letter S With Caron
288         8249,   # 139: Single Left-Pointing Angle Quotation Mark
289         338,    # 140: Latin Capital Ligature OE
290         32,             # 141: ???
291         381,    # 142: Latin Capital Letter Z With Caron
292         32,             # 143: ???
293         32,             # 144: ???
294         8216,   # 145: Left Single Quotation Mark
295         8217,   # 146: Right Single Quotation Mark
296         8220,   # 147: Left Double Quotation Mark
297         8221,   # 148: Right Double Quotation Mark
298         8226,   # 149: Bullet
299         8211,   # 150: En Dash
300         8212,   # 151: Em Dash
301         732,    # 152: Small Tilde
302         8482,   # 153: Trade Mark Sign
303         353,    # 154: Latin Small Letter S With Caron
304         8250,   # 155: Single Right-Pointing Angle Quotation Mark
305         339,    # 156: Latin Small Ligature OE
306         32,             # 157: ???
307         382,    # 158: Latin Small Letter Z With Caron
308         376             # 159: Latin Capital Letter Y With Diaeresis
309 ]
310
311
312 def checkForUnicodeReservedChar(value):
313     if value >= 0xfffe:
314         return ord('?')
315     if value < 127 or value > 159:
316         return value
317     return iso88591_remap[value-127]
318
319
320 def expand(text):
321     result = u''
322     for c in text:
323         oc = ord(c)
324         oc = checkForUnicodeReservedChar(oc)
325         if oc<32 or c==u'&' or c==u'<' or c==u'>' or c==u'"' or oc>127:
326             result += u'&#'+unicode(oc)+u';'
327         else:
328             result += c
329     return result
330
331
332 def resolve(text):
333     pos = 0
334     result = u''
335     l = len(text)
336     while True:
337         prevpos = pos
338         pos = text.find(u'&', prevpos)
339         if pos == -1:
340             ## print "No more &"
341             break
342
343         if pos >= l-2:
344             ## print "Too shoort"
345             break
346                 # here we are sure the next two chars exist
347         
348         result += text[prevpos:pos]
349         c = text[pos+1]
350         if c == u'#':
351             ## print "numeric entity"
352                         # This looks like an char whose unicode if given raw
353             c = text[pos+2]
354             if c == u'x' or c == u'X' and pos < l-3:
355                 tmppos = text.find(u';', pos+3)
356                 if tmppos != -1:
357                     s = text[pos+3: tmppos]
358                     try:
359                         value = int(s, 16)
360                         value = checkForUnicodeReservedChar(value) # remap unicode char if in range 127-159
361                         result += unichr(value)
362                         pos = tmppos + 1
363                         continue # ok, we did it
364                     except ValueError:
365                                             # there pos is not updated so that the original escape-like sequence is kept unchanged
366                         pass
367             else:
368                                 # the given unicode value is decimal
369                                 # IE behavior: parse until non digital char, no conversion if this is not
370                 sb = u''
371                 tmppos = pos+2
372                 while True:
373                     if tmppos >= l:
374                         break # out of range
375                     c = text[tmppos]
376                     if c == u';':
377                         tmppos += 1
378                         break
379                     if c<u'0' or c>u'9':
380                         break
381                     sb += c
382                     tmppos += 1
383                 try:
384                     value = int(sb)
385                     value = checkForUnicodeReservedChar(value); # remap unicode char if in range 127-159
386                     result += unichr(value)
387                     pos = tmppos
388                     continue # ok, we did it
389                 except ValueError:
390                     # there pos is not updated so that the original escape-like sequence is kept unchanged
391                     pass
392         else:
393             # here the first character is not a '#'
394             # let's try the known html entities
395
396             sb = u''
397             tmppos = pos + 1
398             while True:
399                 if tmppos >= l or tmppos-pos > longestEntityLen + 1: # 1 more for ';'
400                     c2 = entities_autocomplete.get(sb, 0)
401                     break
402                 c = text[tmppos]
403                 if c == u';':
404                     tmppos += 1
405                     c2 = entities.get(sb, 0)
406                     break
407                 c2 = entities_autocomplete.get(sb, 0)
408                 if c2:
409                     break
410                 sb += c
411                 tmppos += 1
412             if c2:
413                 result += unichr(c2)
414                 pos = tmppos
415                 continue # ok, we did it
416                         
417         result += u'&' # something went wrong, just skip is '&'
418         pos += 1
419
420     result += text[prevpos:] 
421     return result
422
423 def cleanCDATA(text):
424     """
425     resolve entities
426     removes useless whites, \r, \n and \t with whites
427     expand back entities
428     """
429     tmp = resolve(text)
430     result = u''
431     was_white = False # so that first white is not removed
432     for c in tmp:
433         if c in ' \r\n\t':
434             if not was_white:
435                 result += u' '
436                 was_white = True
437         else:
438             result += c
439             was_white = False
440
441     return expand(result)
442
443
444 if __name__ == '__main__':
445     import sys
446     from optparse import OptionParser
447     parser = OptionParser()
448     parser.add_option("-a", "--action", help="action: resolve, expand or clean [default: %default]", action="store", dest="action", choices=['expand', 'resolve', 'clean'], default='clean')
449     (options, args) = parser.parse_args()
450     if not args:
451         print >> sys.stderr, u"Missing required parameter. Try '&centest'"
452         sys.exit(1)
453     input = unicode(' '.join(args), 'utf-8')
454     if options.action=='resolve':
455         print resolve(input).encode('utf-8')
456     elif options.action=='expand':
457         print expand(input)
458     else: # options.action=='clean':
459         print cleanCDATA(input)