1 | require 'rdoc/markup/simple_markup/fragments'
|
---|
2 | require 'rdoc/markup/simple_markup/inline'
|
---|
3 |
|
---|
4 | require 'cgi'
|
---|
5 |
|
---|
6 | module SM
|
---|
7 |
|
---|
8 | class ToHtml
|
---|
9 |
|
---|
10 | LIST_TYPE_TO_HTML = {
|
---|
11 | ListBase::BULLET => [ "<ul>", "</ul>" ],
|
---|
12 | ListBase::NUMBER => [ "<ol>", "</ol>" ],
|
---|
13 | ListBase::UPPERALPHA => [ "<ol>", "</ol>" ],
|
---|
14 | ListBase::LOWERALPHA => [ "<ol>", "</ol>" ],
|
---|
15 | ListBase::LABELED => [ "<dl>", "</dl>" ],
|
---|
16 | ListBase::NOTE => [ "<table>", "</table>" ],
|
---|
17 | }
|
---|
18 |
|
---|
19 | InlineTag = Struct.new(:bit, :on, :off)
|
---|
20 |
|
---|
21 | def initialize
|
---|
22 | init_tags
|
---|
23 | end
|
---|
24 |
|
---|
25 | ##
|
---|
26 | # Set up the standard mapping of attributes to HTML tags
|
---|
27 | #
|
---|
28 | def init_tags
|
---|
29 | @attr_tags = [
|
---|
30 | InlineTag.new(SM::Attribute.bitmap_for(:BOLD), "<b>", "</b>"),
|
---|
31 | InlineTag.new(SM::Attribute.bitmap_for(:TT), "<tt>", "</tt>"),
|
---|
32 | InlineTag.new(SM::Attribute.bitmap_for(:EM), "<em>", "</em>"),
|
---|
33 | ]
|
---|
34 | end
|
---|
35 |
|
---|
36 | ##
|
---|
37 | # Add a new set of HTML tags for an attribute. We allow
|
---|
38 | # separate start and end tags for flexibility
|
---|
39 | #
|
---|
40 | def add_tag(name, start, stop)
|
---|
41 | @attr_tags << InlineTag.new(SM::Attribute.bitmap_for(name), start, stop)
|
---|
42 | end
|
---|
43 |
|
---|
44 | ##
|
---|
45 | # Given an HTML tag, decorate it with class information
|
---|
46 | # and the like if required. This is a no-op in the base
|
---|
47 | # class, but is overridden in HTML output classes that
|
---|
48 | # implement style sheets
|
---|
49 |
|
---|
50 | def annotate(tag)
|
---|
51 | tag
|
---|
52 | end
|
---|
53 |
|
---|
54 | ##
|
---|
55 | # Here's the client side of the visitor pattern
|
---|
56 |
|
---|
57 | def start_accepting
|
---|
58 | @res = ""
|
---|
59 | @in_list_entry = []
|
---|
60 | end
|
---|
61 |
|
---|
62 | def end_accepting
|
---|
63 | @res
|
---|
64 | end
|
---|
65 |
|
---|
66 | def accept_paragraph(am, fragment)
|
---|
67 | @res << annotate("<p>") + "\n"
|
---|
68 | @res << wrap(convert_flow(am.flow(fragment.txt)))
|
---|
69 | @res << annotate("</p>") + "\n"
|
---|
70 | end
|
---|
71 |
|
---|
72 | def accept_verbatim(am, fragment)
|
---|
73 | @res << annotate("<pre>") + "\n"
|
---|
74 | @res << CGI.escapeHTML(fragment.txt)
|
---|
75 | @res << annotate("</pre>") << "\n"
|
---|
76 | end
|
---|
77 |
|
---|
78 | def accept_rule(am, fragment)
|
---|
79 | size = fragment.param
|
---|
80 | size = 10 if size > 10
|
---|
81 | @res << "<hr size=\"#{size}\"></hr>"
|
---|
82 | end
|
---|
83 |
|
---|
84 | def accept_list_start(am, fragment)
|
---|
85 | @res << html_list_name(fragment.type, true) <<"\n"
|
---|
86 | @in_list_entry.push false
|
---|
87 | end
|
---|
88 |
|
---|
89 | def accept_list_end(am, fragment)
|
---|
90 | if tag = @in_list_entry.pop
|
---|
91 | @res << annotate(tag) << "\n"
|
---|
92 | end
|
---|
93 | @res << html_list_name(fragment.type, false) <<"\n"
|
---|
94 | end
|
---|
95 |
|
---|
96 | def accept_list_item(am, fragment)
|
---|
97 | if tag = @in_list_entry.last
|
---|
98 | @res << annotate(tag) << "\n"
|
---|
99 | end
|
---|
100 | @res << list_item_start(am, fragment)
|
---|
101 | @res << wrap(convert_flow(am.flow(fragment.txt))) << "\n"
|
---|
102 | @in_list_entry[-1] = list_end_for(fragment.type)
|
---|
103 | end
|
---|
104 |
|
---|
105 | def accept_blank_line(am, fragment)
|
---|
106 | # @res << annotate("<p />") << "\n"
|
---|
107 | end
|
---|
108 |
|
---|
109 | def accept_heading(am, fragment)
|
---|
110 | @res << convert_heading(fragment.head_level, am.flow(fragment.txt))
|
---|
111 | end
|
---|
112 |
|
---|
113 | # This is a higher speed (if messier) version of wrap
|
---|
114 |
|
---|
115 | def wrap(txt, line_len = 76)
|
---|
116 | res = ""
|
---|
117 | sp = 0
|
---|
118 | ep = txt.length
|
---|
119 | while sp < ep
|
---|
120 | # scan back for a space
|
---|
121 | p = sp + line_len - 1
|
---|
122 | if p >= ep
|
---|
123 | p = ep
|
---|
124 | else
|
---|
125 | while p > sp and txt[p] != ?\s
|
---|
126 | p -= 1
|
---|
127 | end
|
---|
128 | if p <= sp
|
---|
129 | p = sp + line_len
|
---|
130 | while p < ep and txt[p] != ?\s
|
---|
131 | p += 1
|
---|
132 | end
|
---|
133 | end
|
---|
134 | end
|
---|
135 | res << txt[sp...p] << "\n"
|
---|
136 | sp = p
|
---|
137 | sp += 1 while sp < ep and txt[sp] == ?\s
|
---|
138 | end
|
---|
139 | res
|
---|
140 | end
|
---|
141 |
|
---|
142 | #######################################################################
|
---|
143 |
|
---|
144 | private
|
---|
145 |
|
---|
146 | #######################################################################
|
---|
147 |
|
---|
148 | def on_tags(res, item)
|
---|
149 | attr_mask = item.turn_on
|
---|
150 | return if attr_mask.zero?
|
---|
151 |
|
---|
152 | @attr_tags.each do |tag|
|
---|
153 | if attr_mask & tag.bit != 0
|
---|
154 | res << annotate(tag.on)
|
---|
155 | end
|
---|
156 | end
|
---|
157 | end
|
---|
158 |
|
---|
159 | def off_tags(res, item)
|
---|
160 | attr_mask = item.turn_off
|
---|
161 | return if attr_mask.zero?
|
---|
162 |
|
---|
163 | @attr_tags.reverse_each do |tag|
|
---|
164 | if attr_mask & tag.bit != 0
|
---|
165 | res << annotate(tag.off)
|
---|
166 | end
|
---|
167 | end
|
---|
168 | end
|
---|
169 |
|
---|
170 | def convert_flow(flow)
|
---|
171 | res = ""
|
---|
172 | flow.each do |item|
|
---|
173 | case item
|
---|
174 | when String
|
---|
175 | res << convert_string(item)
|
---|
176 | when AttrChanger
|
---|
177 | off_tags(res, item)
|
---|
178 | on_tags(res, item)
|
---|
179 | when Special
|
---|
180 | res << convert_special(item)
|
---|
181 | else
|
---|
182 | raise "Unknown flow element: #{item.inspect}"
|
---|
183 | end
|
---|
184 | end
|
---|
185 | res
|
---|
186 | end
|
---|
187 |
|
---|
188 | # some of these patterns are taken from SmartyPants...
|
---|
189 |
|
---|
190 | def convert_string(item)
|
---|
191 | CGI.escapeHTML(item).
|
---|
192 |
|
---|
193 |
|
---|
194 | # convert -- to em-dash, (-- to en-dash)
|
---|
195 | gsub(/---?/, '—'). #gsub(/--/, '–').
|
---|
196 |
|
---|
197 | # convert ... to elipsis (and make sure .... becomes .<elipsis>)
|
---|
198 | gsub(/\.\.\.\./, '.…').gsub(/\.\.\./, '…').
|
---|
199 |
|
---|
200 | # convert single closing quote
|
---|
201 | gsub(%r{([^ \t\r\n\[\{\(])\'}) { "#$1’" }.
|
---|
202 | gsub(%r{\'(?=\W|s\b)}) { "’" }.
|
---|
203 |
|
---|
204 | # convert single opening quote
|
---|
205 | gsub(/'/, '‘').
|
---|
206 |
|
---|
207 | # convert double closing quote
|
---|
208 | gsub(%r{([^ \t\r\n\[\{\(])\'(?=\W)}) { "#$1”" }.
|
---|
209 |
|
---|
210 | # convert double opening quote
|
---|
211 | gsub(/'/, '“').
|
---|
212 |
|
---|
213 | # convert copyright
|
---|
214 | gsub(/\(c\)/, '©').
|
---|
215 |
|
---|
216 | # convert and registered trademark
|
---|
217 | gsub(/\(r\)/, '®')
|
---|
218 |
|
---|
219 | end
|
---|
220 |
|
---|
221 | def convert_special(special)
|
---|
222 | handled = false
|
---|
223 | Attribute.each_name_of(special.type) do |name|
|
---|
224 | method_name = "handle_special_#{name}"
|
---|
225 | if self.respond_to? method_name
|
---|
226 | special.text = send(method_name, special)
|
---|
227 | handled = true
|
---|
228 | end
|
---|
229 | end
|
---|
230 | raise "Unhandled special: #{special}" unless handled
|
---|
231 | special.text
|
---|
232 | end
|
---|
233 |
|
---|
234 | def convert_heading(level, flow)
|
---|
235 | res =
|
---|
236 | annotate("<h#{level}>") +
|
---|
237 | convert_flow(flow) +
|
---|
238 | annotate("</h#{level}>\n")
|
---|
239 | end
|
---|
240 |
|
---|
241 | def html_list_name(list_type, is_open_tag)
|
---|
242 | tags = LIST_TYPE_TO_HTML[list_type] || raise("Invalid list type: #{list_type.inspect}")
|
---|
243 | annotate(tags[ is_open_tag ? 0 : 1])
|
---|
244 | end
|
---|
245 |
|
---|
246 | def list_item_start(am, fragment)
|
---|
247 | case fragment.type
|
---|
248 | when ListBase::BULLET, ListBase::NUMBER
|
---|
249 | annotate("<li>")
|
---|
250 |
|
---|
251 | when ListBase::UPPERALPHA
|
---|
252 | annotate("<li type=\"A\">")
|
---|
253 |
|
---|
254 | when ListBase::LOWERALPHA
|
---|
255 | annotate("<li type=\"a\">")
|
---|
256 |
|
---|
257 | when ListBase::LABELED
|
---|
258 | annotate("<dt>") +
|
---|
259 | convert_flow(am.flow(fragment.param)) +
|
---|
260 | annotate("</dt>") +
|
---|
261 | annotate("<dd>")
|
---|
262 |
|
---|
263 | when ListBase::NOTE
|
---|
264 | annotate("<tr>") +
|
---|
265 | annotate("<td valign=\"top\">") +
|
---|
266 | convert_flow(am.flow(fragment.param)) +
|
---|
267 | annotate("</td>") +
|
---|
268 | annotate("<td>")
|
---|
269 | else
|
---|
270 | raise "Invalid list type"
|
---|
271 | end
|
---|
272 | end
|
---|
273 |
|
---|
274 | def list_end_for(fragment_type)
|
---|
275 | case fragment_type
|
---|
276 | when ListBase::BULLET, ListBase::NUMBER, ListBase::UPPERALPHA, ListBase::LOWERALPHA
|
---|
277 | "</li>"
|
---|
278 | when ListBase::LABELED
|
---|
279 | "</dd>"
|
---|
280 | when ListBase::NOTE
|
---|
281 | "</td></tr>"
|
---|
282 | else
|
---|
283 | raise "Invalid list type"
|
---|
284 | end
|
---|
285 | end
|
---|
286 |
|
---|
287 | end
|
---|
288 |
|
---|
289 | end
|
---|