/
html.cr
161 lines (151 loc) · 4.39 KB
/
html.cr
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
require "./html/entities"
# Provides HTML escaping and unescaping methods.
#
# For HTML *parsing* see module XML, especially `XML.parse_html`.
module HTML
private SUBSTITUTIONS = {
'&' => "&",
'<' => "<",
'>' => ">",
'"' => """,
'\'' => "'",
}
# Escapes special characters in HTML, namely
# `&`, `<`, `>`, `"` and `'`.
#
# ```
# require "html"
#
# HTML.escape("Crystal & You") # => "Crystal & You"
# ```
def self.escape(string : String) : String
string.gsub(SUBSTITUTIONS)
end
# Same as `escape(string)` but ouputs the result to
# the given *io*.
#
# ```
# require "html"
#
# io = IO::Memory.new
# HTML.escape("Crystal & You", io) # => nil
# io.to_s # => "Crystal & You"
# ```
def self.escape(string : String, io : IO) : Nil
string.each_char do |char|
io << SUBSTITUTIONS.fetch(char, char)
end
end
# These replacements permit compatibility with old numeric entities that
# assumed Windows-1252 encoding.
# http://www.whatwg.org/specs/web-apps/current-work/multipage/tokenization.html#consume-a-character-reference
private CHARACTER_REPLACEMENTS = {
'\u20AC', # First entry is what 0x80 should be replaced with.
'\u0081',
'\u201A',
'\u0192',
'\u201E',
'\u2026',
'\u2020',
'\u2021',
'\u02C6',
'\u2030',
'\u0160',
'\u2039',
'\u0152',
'\u008D',
'\u017D',
'\u008F',
'\u0090',
'\u2018',
'\u2019',
'\u201C',
'\u201D',
'\u2022',
'\u2013',
'\u2014',
'\u02DC',
'\u2122',
'\u0161',
'\u203A',
'\u0153',
'\u009D',
'\u017E',
'\u0178', # Last entry is 0x9F.
# 0x00->'\uFFFD' is handled programmatically.
# 0x0D->'\u000D' is a no-op.
}
# Returns a string where named and numeric character references
# (e.g. >, >, &x3e;) in *string* are replaced with the corresponding
# unicode characters. This method decodes all HTML5 entities including those
# without a trailing semicolon (such as `©`).
#
# ```
# require "html"
#
# HTML.unescape("Crystal & You") # => "Crystal & You"
# ```
def self.unescape(string : String) : String
string.gsub(/&(?:([a-zA-Z0-9]{2,32};?)|\#([0-9]+);?|\#[xX]([0-9A-Fa-f]+);?)/) do |string, match|
if code = match[1]?
# Try to find the code
value = named_entity(code)
unless value || code.ends_with?(';')
# If we can't find it and it doesn't end with ';',
# we need to find each prefix of it.
# We start from the largest prefix.
removed = 0
until code.empty?
code = code.rchop
removed += 1
value = named_entity(code)
if value
# If we find it, we need to append the part that
# isn't part of the matched code
value += string[-removed..-1]
break
end
end
end
# We either found the code or not,
# in which case we need to return the original string
value || string
elsif code = match[2]?
# Find by decimal code
decode_codepoint(code.to_i) || string
elsif code = match[3]?
# Find by hexadecimal code
decode_codepoint(code.to_i(16)) || string
else
string
end
end
end
private def self.named_entity(code)
HTML::SINGLE_CHAR_ENTITIES[code]? || HTML::DOUBLE_CHAR_ENTITIES[code]?
end
# see https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
private def self.decode_codepoint(codepoint)
case codepoint
when 0x80..0x9F
# Replace characters from Windows-1252 with UTF-8 equivalents.
CHARACTER_REPLACEMENTS[codepoint - 0x80].to_s
when 0,
.>(Char::MAX_CODEPOINT),
0xD800..0xDFFF # unicode surrogate characters
# Replace invalid characters with replacement character.
'\uFFFD'
else
# don't replace disallowed codepoints
unless codepoint == 0x007F ||
# unicode noncharacters
(0xFDD0..0xFDEF).includes?(codepoint) ||
# last two of each plane (nonchars) disallowed
codepoint & 0xFFFF >= 0xFFFE ||
# unicode control characters expect space
(codepoint < 0x0020 && !{0x0009, 0x000A, 0x000C}.includes?(codepoint))
codepoint.unsafe_chr
end
end
end
end