1 #!/usr/bin/env python 2 3 """ 4 HTML serialiser. 5 6 Copyright (C) 2017, 2018, 2019, 2021, 2022, 7 2023 Paul Boddie <paul@boddie.org.uk> 8 9 This program is free software; you can redistribute it and/or modify it under 10 the terms of the GNU General Public License as published by the Free Software 11 Foundation; either version 3 of the License, or (at your option) any later 12 version. 13 14 This program is distributed in the hope that it will be useful, but WITHOUT 15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 16 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 17 details. 18 19 You should have received a copy of the GNU General Public License along with 20 this program. If not, see <http://www.gnu.org/licenses/>. 21 """ 22 23 from moinformat.serialisers.common import escape_attr, escape_text, Serialiser 24 from moinformat.tree.moin import LinkLabel, LinkParameter, Text 25 from moinformat.utils.links import parse_link_target 26 27 class HTMLSerialiser(Serialiser): 28 29 "Serialisation of the page." 30 31 input_formats = ["moin", "wiki"] 32 formats = ["html"] 33 34 # Support methods. 35 36 list_tags = { 37 "i" : "lower-roman", 38 "I" : "upper-roman", 39 "a" : "lower-latin", 40 "A" : "upper-latin", 41 } 42 43 def _get_list_tag(self, marker): 44 if marker: 45 if marker[0].isdigit(): 46 return "ol", "decimal" 47 style_type = self.list_tags.get(marker[0]) 48 if style_type: 49 return "ol", style_type 50 51 return "ul", None 52 53 def _link(self, target, nodes, tag, attr): 54 link = self.linker and self.linker.translate(target) or None 55 56 self.out('<%s %s="%s"' % (tag, attr, escape_attr(link.get_target()))) 57 58 # Provide link parameters as attributes. 59 60 if nodes: 61 for node in nodes: 62 if isinstance(node, LinkParameter): 63 self.out(" ") 64 node.visit(self) 65 66 # Close the tag if an image. 67 68 if tag == "img": 69 self.out(" />") 70 71 # Provide the link label if specified. Otherwise, use a generated 72 # default for the label. 73 74 else: 75 self.out(">") 76 77 for node in nodes or []: 78 if isinstance(node, LinkLabel): 79 node.visit(self) 80 break 81 else: 82 self.out(escape_text(link.get_label())) 83 84 self.out("</%s>" % tag) 85 86 def _region_tag(self, type): 87 88 # NOTE: Need to support types in general. 89 90 type = type and type.split()[0] 91 92 if type == "inline": 93 return "tt" 94 elif type in (None, "python"): 95 return "pre" 96 else: 97 return "span" 98 99 # Node handler methods. 100 101 def region(self, region): 102 tag = self._region_tag(region.type) 103 104 # Generate attributes, joining them when preparing the tag. 105 106 attrs = [] 107 attr = attrs.append 108 109 if region.level: 110 attr("level-%d" % region.level) 111 112 if region.indent: 113 attr("indent-%d" % region.indent) 114 115 # NOTE: Encode type details for CSS. 116 117 attr("type-%s" % escape_attr(region.type or "opaque")) 118 119 # Inline regions must preserve "indent" as space in the text. 120 121 if region.type == "inline" and region.indent: 122 self.out(" " * region.indent) 123 124 self.out("<%s class='%s'>" % (tag, " ".join(attrs))) 125 126 # Serialise the region content. 127 128 self.visit_region(region) 129 130 # End the region with the previous serialiser. 131 132 self.out("</%s>" % tag) 133 134 # Block node methods. 135 136 def block(self, block): 137 self.out("<p>") 138 self.container(block) 139 self.out("</p>") 140 141 def defitem(self, defitem): 142 self.out("<dd>") 143 self.container(defitem) 144 self.out("</dd>") 145 146 def defterm(self, defterm): 147 self.out("<dt>") 148 self.container(defterm) 149 self.out("</dt>") 150 151 def fontstyle(self, fontstyle): 152 if fontstyle.emphasis: 153 self.out("<em>") 154 elif fontstyle.strong: 155 self.out("<strong>") 156 self.container(fontstyle) 157 if fontstyle.emphasis: 158 self.out("</em>") 159 elif fontstyle.strong: 160 self.out("</strong>") 161 162 def heading(self, heading): 163 self.out("<h%d id='%s'>" % ( 164 heading.level, 165 escape_attr(self.linker.make_id(heading.identifier)))) 166 self.container(heading) 167 self.out("</h%d>" % heading.level) 168 169 def larger(self, larger): 170 self.out("<big>") 171 self.container(larger) 172 self.out("</big>") 173 174 def list(self, list): 175 tag, style_type = self._get_list_tag(list.marker) 176 style = style_type and \ 177 ' style="list-style-type: %s"' % escape_attr(style_type) or "" 178 start = style_type and \ 179 list.num is not None and ' start="%s"' % escape_attr(list.num) or "" 180 self.out("<%s%s%s>" % (tag, style, start)) 181 self.container(list) 182 self.out("</%s>" % tag) 183 184 def listitem(self, listitem): 185 self.out("<li>") 186 self.container(listitem) 187 self.out("</li>") 188 189 def macro(self, macro): 190 191 # Special case of a deliberately unexpanded macro. 192 193 if macro.nodes is None: 194 return 195 196 tag = macro.inline and "span" or "div" 197 self.out("<%s class='macro %s'>" % (tag, escape_text(macro.name))) 198 199 # Fallback case for when macros are not replaced. 200 201 if not macro.nodes: 202 self.out(escape_text("<<")) 203 self.out("<span class='name'>%s</span>" % escape_text(macro.name)) 204 if macro.args: 205 self.out("(") 206 first = True 207 for arg in macro.args: 208 if not first: 209 self.out(",") 210 self.out("<span class='arg'>%s</span>" % escape_text(arg)) 211 first = False 212 if macro.args: 213 self.out(")") 214 self.out(escape_text(">>")) 215 216 # Produce the expanded macro content. 217 218 else: 219 self.container(macro) 220 221 tag = macro.inline and "span" or "div" 222 self.out("</%s>" % tag) 223 224 def monospace(self, monospace): 225 self.out("<tt>") 226 self.container(monospace) 227 self.out("</tt>") 228 229 def smaller(self, smaller): 230 self.out("<small>") 231 self.container(smaller) 232 self.out("</small>") 233 234 def strikethrough(self, strikethrough): 235 self.out("<del>") 236 self.container(strikethrough) 237 self.out("</del>") 238 239 def subscript(self, subscript): 240 self.out("<sub>") 241 self.container(subscript) 242 self.out("</sub>") 243 244 def superscript(self, superscript): 245 self.out("<sup>") 246 self.container(superscript) 247 self.out("</sup>") 248 249 def table(self, table): 250 self.out("<table>") 251 self.container(table) 252 self.out("</table>") 253 254 def table_cell(self, table_cell): 255 self.out("<td") 256 257 # Handle the attributes separately from their container. 258 259 if table_cell.attrs and not table_cell.attrs.empty(): 260 for attr in table_cell.attrs.nodes: 261 attr.visit(self) 262 263 self.out(">") 264 self.container(table_cell) 265 self.out("</td>") 266 267 def table_row(self, table_row): 268 self.out("<tr>") 269 self.container(table_row) 270 self.out("</tr>") 271 272 def underline(self, underline): 273 self.out("<span style='text-decoration: underline'>") 274 self.container(underline) 275 self.out("</span>") 276 277 # Inline node methods. 278 279 def anchor(self, anchor): 280 self.out("<a name='%s' />" % escape_attr(self.linker.make_id(anchor.target))) 281 282 def break_(self, break_): 283 pass 284 285 def comment(self, comment): 286 pass 287 288 def directive(self, directive): 289 290 # Obtain a blank value if the value is missing. 291 292 name, text = (directive.directive.split(None, 1) + [""])[:2] 293 294 # Produce a readable redirect. 295 296 if name.lower() == "redirect": 297 self.start_block() 298 299 # Process the redirect argument as a link target, producing a link 300 # element. 301 302 target = parse_link_target(text, self.metadata) 303 self._link(target, [LinkLabel([Text(text)])], "a", "href") 304 305 self.end_block() 306 307 def linebreak(self, linebreak): 308 self.out("<br />") 309 310 def link(self, link): 311 self._link(link.target, link.nodes, "a", "href") 312 313 def link_label(self, link_label): 314 self.container(link_label) 315 316 def link_parameter(self, link_parameter): 317 s = link_parameter.text_content() 318 key_value = s.split("=", 1) 319 320 if len(key_value) == 1: 321 self.out(key_value[0]) 322 else: 323 key, value = key_value 324 self.out("%s='%s'" % (key, escape_attr(value))) 325 326 def nbsp(self, nbsp): 327 self.out(" ") 328 329 def rule(self, rule): 330 self.out("<hr style='height: %dpt' />" % min(rule.height, 10)) 331 332 def table_attrs(self, table_attrs): 333 334 # Skip the attributes in their original form. 335 336 pass 337 338 def table_attr(self, table_attr): 339 self.out(" %s%s" % ( 340 escape_text(table_attr.name), 341 table_attr.value is not None and 342 "='%s'" % escape_attr(table_attr.value) or "")) 343 344 def text(self, text): 345 self.out(escape_text(text.s)) 346 347 def transclusion(self, transclusion): 348 self._link(transclusion.target, transclusion.nodes, "img", "src") 349 350 def verbatim(self, verbatim): 351 self.out(escape_text(verbatim.text)) 352 353 serialiser = HTMLSerialiser 354 355 # vim: tabstop=4 expandtab shiftwidth=4