1# -*- coding: iso-8859-1 -*- 2""" 3 MoinMoin - "text/html+css" Formatter 4 5 @copyright: 2000-2004 by Juergen Hermann <jh@web.de> 6 @license: GNU GPL, see COPYING for details. 7""" 8import os.path, re 9 10from MoinMoin import log 11logging = log.getLogger(__name__) 12 13from MoinMoin.formatter import FormatterBase 14from MoinMoin import wikiutil, i18n 15from MoinMoin.Page import Page 16from MoinMoin.action import AttachFile 17 18# insert IDs into output wherever they occur 19# warning: breaks toggle line numbers javascript 20_id_debug = False 21 22line_anchors = True 23prettyprint = False 24 25# These are the HTML elements that we treat as block elements. 26_blocks = set(['dd', 'div', 'dl', 'dt', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 27 'hr', 'li', 'ol', 'p', 'pre', 'table', 'tbody', 'td', 'tfoot', 'th', 28 'thead', 'tr', 'ul', 'blockquote', ]) 29 30# These are the HTML elements which are typically only used with 31# an opening tag without a separate closing tag. We do not 32# include 'script' or 'style' because sometimes they do have 33# content, and also IE has a parsing bug with those two elements (only) 34# when they don't have a closing tag even if valid XHTML. 35 36_self_closing_tags = set(['area', 'base', 'br', 'col', 'frame', 'hr', 'img', 37 'input', 'isindex', 'link', 'meta', 'param']) 38 39# We only open those tags and let the browser auto-close them: 40_auto_closing_tags = set(['p']) 41 42# These are the elements which generally should cause an increase in the 43# indention level in the html souce code. 44_indenting_tags = set(['ol', 'ul', 'dl', 'li', 'dt', 'dd', 'tr', 'td']) 45 46# These are the elements that discard any whitespace they contain as 47# immediate child nodes. 48_space_eating_tags = set(['colgroup', 'dl', 'frameset', 'head', 'map' 'menu', 49 'ol', 'optgroup', 'select', 'table', 'tbody', 'tfoot', 50 'thead', 'tr', 'ul']) 51 52# These are standard HTML attributes which are typically used without any 53# value; e.g., as boolean flags indicated by their presence. 54_html_attribute_boolflags = set(['compact', 'disabled', 'ismap', 'nohref', 55 'noresize', 'noshade', 'nowrap', 'readonly', 56 'selected', 'wrap']) 57 58# These are all the standard HTML attributes that are allowed on any element. 59_common_attributes = set(['accesskey', 'class', 'dir', 'disabled', 'id', 'lang', 60 'style', 'tabindex', 'title']) 61 62 63def rewrite_attribute_name(name, default_namespace='html'): 64 """ 65 Takes an attribute name and tries to make it HTML correct. 66 67 This function takes an attribute name as a string, as it may be 68 passed in to a formatting method using a keyword-argument syntax, 69 and tries to convert it into a real attribute name. This is 70 necessary because some attributes may conflict with Python 71 reserved words or variable syntax (such as 'for', 'class', or 72 'z-index'); and also to help with backwards compatibility with 73 older versions of MoinMoin where different names may have been 74 used (such as 'content_id' or 'css'). 75 76 Returns a tuple of strings: (namespace, attribute). 77 78 Namespaces: The default namespace is always assumed to be 'html', 79 unless the input string contains a colon or a double-underscore; 80 in which case the first such occurance is assumed to separate the 81 namespace prefix from name. So, for example, to get the HTML 82 attribute 'for' (as on a <label> element), you can pass in the 83 string 'html__for' or even '__for'. 84 85 Hyphens: To better support hyphens (which are not allowed in Python 86 variable names), all occurances of two underscores will be replaced 87 with a hyphen. If you use this, then you must also provide a 88 namespace since the first occurance of '__' separates a namespace 89 from the name. 90 91 Special cases: Within the 'html' namespace, mainly to preserve 92 backwards compatibility, these exceptions ars recognized: 93 'content_type', 'content_id', 'css_class', and 'css'. 94 Additionally all html attributes starting with 'on' are forced to 95 lower-case. Also the string 'xmlns' is recognized as having 96 no namespace. 97 98 Examples: 99 'id' -> ('html', 'id') 100 'css_class' -> ('html', 'class') 101 'content_id' -> ('html', 'id') 102 'content_type' -> ('html', 'type') 103 'html__for' -> ('html', 'for) 104 'xml__space' -> ('xml', 'space') 105 '__z__index' -> ('html', 'z-index') 106 '__http__equiv' -> ('html', 'http-equiv') 107 'onChange' -> ('html', 'onchange') 108 'xmlns' -> ('', 'xmlns') 109 'xmlns__abc' -> ('xmlns', 'abc') 110 111 (In actuality we only deal with namespace prefixes, not any real 112 namespace URI...we only care about the syntax not the meanings.) 113 """ 114 115 # Handle any namespaces (just in case someday we support XHTML) 116 if ':' in name: 117 ns, name = name.split(':', 1) 118 elif '__' in name: 119 ns, name = name.split('__', 1) 120 elif name == 'xmlns': 121 ns = '' 122 else: 123 ns = default_namespace 124 125 name.replace('__', '-') 126 if ns == 'html': 127 # We have an HTML attribute, fix according to DTD 128 if name == 'content_type': # MIME type such as in <a> and <link> elements 129 name = 'type' 130 elif name == 'content_id': # moin historical convention 131 name = 'id' 132 elif name in ('css_class', 'css'): # to avoid python word 'class' 133 name = 'class' 134 elif name.startswith('on'): # event handler hook 135 name = name.lower() 136 return ns, name 137 138 139def extend_attribute_dictionary(attributedict, ns, name, value): 140 """Add a new attribute to an attribute dictionary, merging values where possible. 141 142 The attributedict must be a dictionary with tuple-keys of the form: 143 (namespace, attrname). 144 145 The given ns, name, and value will be added to the dictionary. It 146 will replace the old value if it existed, except for a few special 147 cases where the values are logically merged instead (CSS class 148 names and style rules). 149 150 As a special case, if value is None (not just ''), then the 151 attribute is actually deleted from the dictionary. 152 """ 153 154 key = ns, name 155 if value is None: 156 if key in attributedict: 157 del attributedict[key] 158 else: 159 if ns == 'html' and key in attributedict: 160 if name == 'class': 161 # CSS classes are appended by space-separated list 162 value = attributedict[key] + ' ' + value 163 elif name == 'style': 164 # CSS styles are appended by semicolon-separated rules list 165 value = attributedict[key] + '; ' + value 166 elif name in _html_attribute_boolflags: 167 # All attributes must have a value. According to XHTML those 168 # traditionally used as flags should have their value set to 169 # the same as the attribute name. 170 value = name 171 attributedict[key] = value 172 173 174class Formatter(FormatterBase): 175 """ 176 Send HTML data. 177 """ 178 179 hardspace = ' ' 180 indentspace = ' ' 181 182 def __init__(self, request, **kw): 183 FormatterBase.__init__(self, request, **kw) 184 self._indent_level = 0 185 186 self._in_code = 0 # used by text_gedit 187 self._in_code_area = 0 188 self._in_code_line = 0 189 self._code_area_js = 0 190 self._code_area_state = ['', 0, -1, -1, 0] 191 192 # code format string. id - code block id, num - line number. 193 # Caution: upon changing, also check line numbers hide/show js. 194 self._code_id_format = "%(id)s_%(num)d" 195 196 self._show_section_numbers = None 197 self.pagelink_preclosed = False 198 self._is_included = kw.get('is_included', False) 199 self.request = request 200 self.cfg = request.cfg 201 self.no_magic = kw.get('no_magic', False) # disabled tag auto closing 202 203 if not hasattr(request, '_fmt_hd_counters'): 204 request._fmt_hd_counters = [] 205 206 # Primitive formatter functions ##################################### 207 208 # all other methods should use these to format tags. This keeps the 209 # code clean and handle pathological cases like unclosed p and 210 # inline tags. 211 212 def _langAttr(self, lang=None): 213 """ Return lang and dir attribute 214 (INTERNAL USE BY HTML FORMATTER ONLY!) 215 216 Must be used on all block elements - div, p, table, etc. 217 @param lang: if defined, will return attributes for lang. if not 218 defined, will return attributes only if the current lang is 219 different from the content lang. 220 @rtype: dict 221 @return: language attributes 222 """ 223 if not lang: 224 lang = self.request.current_lang 225 # Actions that generate content in user language should change 226 # the content lang from the default defined in cfg. 227 if lang == self.request.content_lang: 228 # lang is inherited from content div 229 return {} 230 231 #attr = {'xml:lang': lang, 'lang': lang, 'dir': i18n.getDirection(lang),} 232 attr = {'lang': lang, 'dir': i18n.getDirection(lang), } 233 return attr 234 235 def _formatAttributes(self, attr=None, allowed_attrs=None, **kw): 236 """ Return HTML attributes formatted as a single string. (INTERNAL USE BY HTML FORMATTER ONLY!) 237 238 @param attr: dict containing keys and values 239 @param allowed_attrs: A list of allowable attribute names 240 @param kw: other arbitrary attributes expressed as keyword arguments. 241 @rtype: string 242 @return: formated attributes or empty string 243 244 The attributes and their values can either be given in the 245 'attr' dictionary, or as extra keyword arguments. They are 246 both merged together. See the function 247 rewrite_attribute_name() for special notes on how to name 248 attributes. 249 250 Setting a value to None rather than a string (or string 251 coercible) will remove that attribute from the list. 252 253 If the list of allowed_attrs is provided, then an error is 254 raised if an HTML attribute is encountered that is not in that 255 list (or is not a common attribute which is always allowed or 256 is not in another XML namespace using the double-underscore 257 syntax). 258 """ 259 260 # Merge the attr dict and kw dict into a single attributes 261 # dictionary (rewriting any attribute names, extracting 262 # namespaces, and merging some values like css classes). 263 attributes = {} # dict of key=(namespace,name): value=attribute_value 264 if attr: 265 for a, v in attr.items(): 266 a_ns, a_name = rewrite_attribute_name(a) 267 extend_attribute_dictionary(attributes, a_ns, a_name, v) 268 if kw: 269 for a, v in kw.items(): 270 a_ns, a_name = rewrite_attribute_name(a) 271 extend_attribute_dictionary(attributes, a_ns, a_name, v) 272 273 # Add title attribute if missing, but it has an alt. 274 if ('html', 'alt') in attributes and ('html', 'title') not in attributes: 275 attributes[('html', 'title')] = attributes[('html', 'alt')] 276 277 # Force both lang and xml:lang to be present and identical if 278 # either exists. The lang takes precedence over xml:lang if 279 # both exist. 280 #if ('html', 'lang') in attributes: 281 # attributes[('xml', 'lang')] = attributes[('html', 'lang')] 282 #elif ('xml', 'lang') in attributes: 283 # attributes[('html', 'lang')] = attributes[('xml', 'lang')] 284 285 # Check all the HTML attributes to see if they are known and 286 # allowed. Ignore attributes if in non-HTML namespaces. 287 if allowed_attrs: 288 for name in [key[1] for key in attributes if key[0] == 'html']: 289 if name in _common_attributes or name in allowed_attrs: 290 pass 291 elif name.startswith('on'): 292 pass # Too many event handlers to enumerate, just let them all pass. 293 else: 294 # Unknown or unallowed attribute. 295 err = 'Illegal HTML attribute "%s" passed to formatter' % name 296 raise ValueError(err) 297 298 # Finally, format them all as a single string. 299 if attributes: 300 # Construct a formatted string containing all attributes 301 # with their values escaped. Any html:* namespace 302 # attributes drop the namespace prefix. We build this by 303 # separating the attributes into three categories: 304 # 305 # * Those without any namespace (should only be xmlns attributes) 306 # * Those in the HTML namespace (we drop the html: prefix for these) 307 # * Those in any other non-HTML namespace, including xml: 308 309 xmlnslist = ['%s="%s"' % (k[1], wikiutil.escape(v, 1)) 310 for k, v in attributes.items() if not k[0]] 311 htmllist = ['%s="%s"' % (k[1], wikiutil.escape(v, 1)) 312 for k, v in attributes.items() if k[0] == 'html'] 313 otherlist = ['%s:%s="%s"' % (k[0], k[1], wikiutil.escape(v, 1)) 314 for k, v in attributes.items() if k[0] and k[0] != 'html'] 315 316 # Join all these lists together in a space-separated string. Also 317 # prefix the whole thing with a space too. 318 htmllist.sort() 319 otherlist.sort() 320 all = [''] + xmlnslist + htmllist + otherlist 321 return ' '.join(all) 322 return '' 323 324 def _open(self, tag, newline=False, attr=None, allowed_attrs=None, 325 is_unique=False, **kw): 326 """ Open a tag with optional attributes (INTERNAL USE BY HTML FORMATTER ONLY!) 327 328 @param tag: html tag, string 329 @param newline: render tag so following data is on a separate line 330 @param attr: dict with tag attributes 331 @param allowed_attrs: list of allowed attributes for this element 332 @param is_unique: ID is already unique 333 @param kw: arbitrary attributes and values 334 @rtype: string ? 335 @return: open tag with attributes as a string 336 """ 337 # If it is self-closing, then don't expect a closing tag later on. 338 is_self_closing = (tag in _self_closing_tags) and ' /' or '' 339 340 # make ID unique 341 id = None 342 if not is_unique: 343 if attr and 'id' in attr: 344 id = self.make_id_unique(attr['id']) 345 id = self.qualify_id(id) 346 attr['id'] = id 347 if 'id' in kw: 348 id = self.make_id_unique(kw['id']) 349 id = self.qualify_id(id) 350 kw['id'] = id 351 else: 352 if attr and 'id' in attr: 353 id = attr['id'] 354 if 'id' in kw: 355 id = kw['id'] 356 357 if tag in _blocks: 358 # Block elements 359 result = [] 360 361 # Add language attributes, but let caller overide the default 362 attributes = self._langAttr() 363 if attr: 364 attributes.update(attr) 365 366 # Format 367 attributes = self._formatAttributes(attributes, allowed_attrs=allowed_attrs, **kw) 368 result.append('<%s%s%s>' % (tag, attributes, is_self_closing)) 369 if newline: 370 result.append(self._newline()) 371 if _id_debug and id: 372 result.append('(%s) ' % id) 373 tagstr = ''.join(result) 374 else: 375 # Inline elements 376 tagstr = '<%s%s%s>' % (tag, 377 self._formatAttributes(attr, allowed_attrs, **kw), 378 is_self_closing) 379 return tagstr 380 381 def _close(self, tag, newline=False): 382 """ Close tag (INTERNAL USE BY HTML FORMATTER ONLY!) 383 384 @param tag: html tag, string 385 @param newline: render tag so following data is on a separate line 386 @rtype: string 387 @return: closing tag as a string 388 """ 389 if tag in _self_closing_tags or (tag in _auto_closing_tags and not self.no_magic): 390 # This tag was already closed 391 tagstr = '' 392 elif tag in _blocks: 393 # Block elements 394 result = [] 395 if newline: 396 result.append(self._newline()) 397 result.append('</%s>' % (tag)) 398 tagstr = ''.join(result) 399 else: 400 # Inline elements 401 tagstr = '</%s>' % tag 402 403 if newline: 404 tagstr += self._newline() 405 return tagstr 406 407 # Public methods ################################################### 408 409 def startContent(self, content_id='content', newline=True, **kw): 410 """ Start page content div. 411 412 A link anchor is provided at the beginning of the div, with 413 an id of 'top' (modified by the request ID cache). 414 """ 415 416 if hasattr(self, 'page'): 417 self.request.uid_generator.begin(self.page.page_name) 418 419 result = [] 420 # Use the content language 421 attr = self._langAttr(self.request.content_lang) 422 attr['id'] = content_id 423 result.append(self._open('div', newline=False, attr=attr, 424 allowed_attrs=['align'], **kw)) 425 result.append(self.anchordef('top')) 426 if newline: 427 result.append('\n') 428 return ''.join(result) 429 430 def endContent(self, newline=True): 431 """ Close page content div. 432 433 A link anchor is provided at the end of the div, with 434 an id of 'bottom' (modified by the request ID cache). 435 """ 436 437 result = [] 438 result.append(self.anchordef('bottom')) 439 result.append(self._close('div', newline=newline)) 440 if hasattr(self, 'page'): 441 self.request.uid_generator.end() 442 return ''.join(result) 443 444 def lang(self, on, lang_name): 445 """ Insert text with specific lang and direction. 446 447 Enclose within span tag if lang_name is different from 448 the current lang 449 """ 450 tag = 'span' 451 if lang_name != self.request.current_lang: 452 # Enclose text in span using lang attributes 453 if on: 454 attr = self._langAttr(lang=lang_name) 455 return self._open(tag, attr=attr) 456 return self._close(tag) 457 458 # Direction did not change, no need for span 459 return '' 460 461 # Links ############################################################## 462 463 def pagelink(self, on, pagename='', page=None, **kw): 464 """ Link to a page. 465 466 formatter.text_python will use an optimized call with a page!=None 467 parameter. DO NOT USE THIS YOURSELF OR IT WILL BREAK. 468 469 See wikiutil.link_tag() for possible keyword parameters. 470 """ 471 FormatterBase.pagelink(self, on, pagename, page, **kw) 472 if 'generated' in kw: 473 del kw['generated'] 474 if page is None: 475 page = Page(self.request, pagename, formatter=self) 476 if self.request.user.show_nonexist_qm and on and not page.exists(): 477 self.pagelink_preclosed = True 478 return (page.link_to(self.request, on=1, **kw) + 479 self.text("?") + 480 page.link_to(self.request, on=0, **kw)) 481 elif not on and self.pagelink_preclosed: 482 self.pagelink_preclosed = False 483 return "" 484 else: 485 return page.link_to(self.request, on=on, **kw) 486 487 def interwikilink(self, on, interwiki='', pagename='', **kw): 488 """ 489 @keyword title: override using the interwiki wikiname as title 490 """ 491 querystr = kw.get('querystr', {}) 492 wikitag, wikiurl, wikitail, wikitag_bad = wikiutil.resolve_interwiki(self.request, interwiki, pagename) 493 wikiurl = wikiutil.mapURL(self.request, wikiurl) 494 if wikitag == 'Self': # for own wiki, do simple links 495 wikitail = wikiutil.url_unquote(wikitail) 496 try: # XXX this is the only place where we access self.page - do we need it? Crashes silently on actions! 497 pagename = wikiutil.AbsPageName(self.page.page_name, wikitail) 498 except: 499 pagename = wikitail 500 return self.pagelink(on, pagename, **kw) 501 else: # return InterWiki hyperlink 502 if on: 503 href = wikiutil.join_wiki(wikiurl, wikitail) 504 if querystr: 505 separator = ('?', '&')['?' in href] 506 href = '%s%s%s' % (href, separator, wikiutil.makeQueryString(querystr)) 507 anchor = kw.get('anchor') 508 if anchor: 509 href = '%s#%s' % (href, self.sanitize_to_id(anchor)) 510 if wikitag_bad: 511 html_class = 'badinterwiki' 512 else: 513 html_class = 'interwiki' 514 title = kw.get('title', wikitag) 515 return self.url(1, href, title=title, css=html_class) # interwiki links with umlauts 516 else: 517 return self.url(0) 518 519 def url(self, on, url=None, css=None, do_escape=None, **kw): 520 """ 521 Inserts an <a> element (you can give any A tag attributes as kw args). 522 523 @param on: 1 to start the link, 0 to end the link (no other arguments are needed when on==0). 524 @param url: the URL to link to; will go through Wiki URL mapping. 525 @param css: a space-separated list of CSS classes 526 @param do_escape: DEPRECATED and not used any more, please remove it from your code! 527 We will remove this parameter in moin 1.8 (it used to filter url 528 param through wikiutil.escape, but text_html formatter's _open 529 will do it again, so this just leads to double escaping now). 530 """ 531 if do_escape is not None: 532 if do_escape: 533 logging.warning("Deprecation warning: MoinMoin.formatter.text_html.url being called with do_escape=1/True parameter, please review caller.") 534 else: 535 logging.warning("Deprecation warning: MoinMoin.formatter.text_html.url being called with do_escape=0/False parameter, please remove it from the caller.") 536 if on: 537 attrs = self._langAttr() 538 539 # Handle the URL mapping 540 if url is None and 'href' in kw: 541 url = kw['href'] 542 del kw['href'] 543 if url is not None: 544 url = wikiutil.mapURL(self.request, url) 545 attrs['href'] = url 546 547 if css: 548 attrs['class'] = css 549 550 markup = self._open('a', attr=attrs, **kw) 551 else: 552 markup = self._close('a') 553 return markup 554 555 def anchordef(self, id): 556 """Inserts an invisible element used as a link target. 557 558 Inserts an empty <span> element with an id attribute, used as an anchor 559 for link references. We use <span></span> rather than <span/> 560 for browser portability. 561 """ 562 # Don't add newlines, \n, as it will break pre and 563 # line-numbered code sections (from line_achordef() method). 564 #return '<a id="%s"></a>' % (id, ) # do not use - this breaks PRE sections for IE 565 id = self.make_id_unique(id) 566 id = self.qualify_id(id) 567 return '<span class="anchor" id="%s"></span>' % id 568 569 def line_anchordef(self, lineno): 570 if line_anchors: 571 return self.anchordef("line-%d" % lineno) 572 else: 573 return '' 574 575 def anchorlink(self, on, name='', **kw): 576 """Insert an <a> link pointing to an anchor on the same page. 577 578 Call once with on=1 to start the link, and a second time with 579 on=0 to end it. No other arguments are needed on the second 580 call. 581 582 The name argument should be the same as the id provided to the 583 anchordef() method, or some other elment. It should NOT start 584 with '#' as that will be added automatically. 585 586 The id argument, if provided, is instead the id of this link 587 itself and not of the target element the link references. 588 """ 589 attrs = self._langAttr() 590 if name: 591 name = self.sanitize_to_id(name) 592 attrs['href'] = '#' + self.qualify_id(name) 593 if 'href' in kw: 594 del kw['href'] 595 if on: 596 str = self._open('a', attr=attrs, **kw) 597 else: 598 str = self._close('a') 599 return str 600 601 def line_anchorlink(self, on, lineno=0): 602 if line_anchors: 603 return self.anchorlink(on, name="line-%d" % lineno) 604 else: 605 return '' 606 607 # Attachments ###################################################### 608 609 def attachment_link(self, on, url=None, querystr=None, **kw): 610 """ Link to an attachment. 611 612 @param on: 1/True=start link, 0/False=end link 613 @param url: filename.ext or PageName/filename.ext 614 """ 615 assert on in (0, 1, False, True) # make sure we get called the new way, not like the 1.5 api was 616 _ = self.request.getText 617 if querystr is None: 618 querystr = {} 619 assert isinstance(querystr, dict) # new in 1.6, only support dicts 620 if 'do' not in querystr: 621 querystr['do'] = 'view' 622 if on: 623 pagename, filename = AttachFile.absoluteName(url, self.page.page_name) 624 #logging.debug("attachment_link: url %s pagename %s filename %s" % (url, pagename, filename)) 625 fname = wikiutil.taintfilename(filename) 626 if AttachFile.exists(self.request, pagename, fname): 627 target = AttachFile.getAttachUrl(pagename, fname, self.request, do=querystr['do']) 628 if not 'title' in kw: 629 kw['title'] = "attachment:%s" % url 630 kw['css'] = 'attachment' 631 else: 632 target = AttachFile.getAttachUrl(pagename, fname, self.request, do='upload_form') 633 kw['title'] = _('Upload new attachment "%(filename)s"') % {'filename': fname} 634 kw['css'] = 'attachment nonexistent' 635 return self.url(on, target, **kw) 636 else: 637 return self.url(on) 638 639 def attachment_image(self, url, **kw): 640 _ = self.request.getText 641 pagename, filename = AttachFile.absoluteName(url, self.page.page_name) 642 fname = wikiutil.taintfilename(filename) 643 exists = AttachFile.exists(self.request, pagename, fname) 644 if exists: 645 kw['css'] = 'attachment' 646 kw['src'] = AttachFile.getAttachUrl(pagename, fname, self.request, addts=1) 647 title = _('Inlined image: %(url)s') % {'url': self.text(url)} 648 if not 'title' in kw: 649 kw['title'] = title 650 # alt is required for images: 651 if not 'alt' in kw: 652 kw['alt'] = kw['title'] 653 return self.image(**kw) 654 else: 655 title = _('Upload new attachment "%(filename)s"') % {'filename': fname} 656 img = self.icon('attachimg') 657 css = 'nonexistent' 658 target = AttachFile.getAttachUrl(pagename, fname, self.request, do='upload_form') 659 return self.url(1, target, css=css, title=title) + img + self.url(0) 660 661 def attachment_drawing(self, url, text, **kw): 662 # ToDo try to move this to a better place e.g. __init__ 663 try: 664 drawing_action = AttachFile.get_action(self.request, url, do='modify') 665 assert drawing_action is not None 666 attachment_drawing = wikiutil.importPlugin(self.request.cfg, 'action', 667 drawing_action, 'attachment_drawing') 668 return attachment_drawing(self, url, text, **kw) 669 except (wikiutil.PluginMissingError, wikiutil.PluginAttributeError, AssertionError): 670 return url 671 672 # Text ############################################################## 673 674 def _text(self, text): 675 text = wikiutil.escape(text) 676 if self._in_code: 677 text = text.replace(' ', self.hardspace) 678 return text 679 680 # Inline ########################################################### 681 682 def strong(self, on, **kw): 683 """Creates an HTML <strong> element. 684 685 Call once with on=1 to start the region, and a second time 686 with on=0 to end it. 687 """ 688 tag = 'strong' 689 if on: 690 return self._open(tag, allowed_attrs=[], **kw) 691 return self._close(tag) 692 693 def emphasis(self, on, **kw): 694 """Creates an HTML <em> element. 695 696 Call once with on=1 to start the region, and a second time 697 with on=0 to end it. 698 """ 699 tag = 'em' 700 if on: 701 return self._open(tag, allowed_attrs=[], **kw) 702 return self._close(tag) 703 704 def underline(self, on, **kw): 705 """Creates a text span for underlining (css class "u"). 706 707 Call once with on=1 to start the region, and a second time 708 with on=0 to end it. 709 """ 710 tag = 'span' 711 if on: 712 return self._open(tag, attr={'class': 'u'}, allowed_attrs=[], **kw) 713 return self._close(tag) 714 715 def highlight(self, on, **kw): 716 """Creates a text span for highlighting (css class "highlight"). 717 718 Call once with on=1 to start the region, and a second time 719 with on=0 to end it. 720 """ 721 tag = 'strong' 722 if on: 723 return self._open(tag, attr={'class': 'highlight'}, allowed_attrs=[], **kw) 724 return self._close(tag) 725 726 def sup(self, on, **kw): 727 """Creates a <sup> element for superscript text. 728 729 Call once with on=1 to start the region, and a second time 730 with on=0 to end it. 731 """ 732 tag = 'sup' 733 if on: 734 return self._open(tag, allowed_attrs=[], **kw) 735 return self._close(tag) 736 737 def sub(self, on, **kw): 738 """Creates a <sub> element for subscript text. 739 740 Call once with on=1 to start the region, and a second time 741 with on=0 to end it. 742 """ 743 tag = 'sub' 744 if on: 745 return self._open(tag, allowed_attrs=[], **kw) 746 return self._close(tag) 747 748 def strike(self, on, **kw): 749 """Creates a text span for line-through (strikeout) text (css class 'strike'). 750 751 Call once with on=1 to start the region, and a second time 752 with on=0 to end it. 753 """ 754 # This does not use <strike> because has been deprecated in standard HTML. 755 tag = 'span' 756 if on: 757 return self._open(tag, attr={'class': 'strike'}, 758 allowed_attrs=[], **kw) 759 return self._close(tag) 760 761 def code(self, on, **kw): 762 """Creates a <tt> element for inline code or monospaced text. 763 764 Call once with on=1 to start the region, and a second time 765 with on=0 to end it. 766 767 Any text within this section will have spaces converted to 768 non-break spaces. 769 """ 770 tag = 'tt' 771 self._in_code = on 772 if on: 773 return self._open(tag, allowed_attrs=[], **kw) 774 return self._close(tag) 775 776 def small(self, on, **kw): 777 """Creates a <small> element for smaller font. 778 779 Call once with on=1 to start the region, and a second time 780 with on=0 to end it. 781 """ 782 tag = 'small' 783 if on: 784 return self._open(tag, allowed_attrs=[], **kw) 785 return self._close(tag) 786 787 def big(self, on, **kw): 788 """Creates a <big> element for larger font. 789 790 Call once with on=1 to start the region, and a second time 791 with on=0 to end it. 792 """ 793 tag = 'big' 794 if on: 795 return self._open(tag, allowed_attrs=[], **kw) 796 return self._close(tag) 797 798 799 # Block elements #################################################### 800 801 def preformatted(self, on, **kw): 802 """Creates a preformatted text region, with a <pre> element. 803 804 Call once with on=1 to start the region, and a second time 805 with on=0 to end it. 806 """ 807 FormatterBase.preformatted(self, on) 808 tag = 'pre' 809 if on: 810 return self._open(tag, newline=1, **kw) 811 return self._close(tag) 812 813 # Use by code area 814 _toggleLineNumbersScript = """ 815<script type="text/javascript"> 816function isnumbered(obj) { 817 return obj.childNodes.length && obj.firstChild.childNodes.length && obj.firstChild.firstChild.className == 'LineNumber'; 818} 819function nformat(num,chrs,add) { 820 var nlen = Math.max(0,chrs-(''+num).length), res = ''; 821 while (nlen>0) { res += ' '; nlen-- } 822 return res+num+add; 823} 824function addnumber(did, nstart, nstep) { 825 var c = document.getElementById(did), l = c.firstChild, n = 1; 826 if (!isnumbered(c)) { 827 if (typeof nstart == 'undefined') nstart = 1; 828 if (typeof nstep == 'undefined') nstep = 1; 829 var n = nstart; 830 while (l != null) { 831 if (l.tagName == 'SPAN') { 832 var s = document.createElement('SPAN'); 833 var a = document.createElement('A'); 834 s.className = 'LineNumber'; 835 a.appendChild(document.createTextNode(nformat(n,4,''))); 836 a.href = '#' + did + '_' + n; 837 s.appendChild(a); 838 s.appendChild(document.createTextNode(' ')); 839 n += nstep; 840 if (l.childNodes.length) { 841 l.insertBefore(s, l.firstChild); 842 } 843 else { 844 l.appendChild(s); 845 } 846 } 847 l = l.nextSibling; 848 } 849 } 850 return false; 851} 852function remnumber(did) { 853 var c = document.getElementById(did), l = c.firstChild; 854 if (isnumbered(c)) { 855 while (l != null) { 856 if (l.tagName == 'SPAN' && l.firstChild.className == 'LineNumber') l.removeChild(l.firstChild); 857 l = l.nextSibling; 858 } 859 } 860 return false; 861} 862function togglenumber(did, nstart, nstep) { 863 var c = document.getElementById(did); 864 if (isnumbered(c)) { 865 remnumber(did); 866 } else { 867 addnumber(did,nstart,nstep); 868 } 869 return false; 870} 871</script> 872""" 873 874 def code_area(self, on, code_id, code_type='code', show=0, start=-1, step=-1, msg=None): 875 """Creates a formatted code region, with line numbering. 876 877 This region is formatted as a <div> with a <pre> inside it. The 878 code_id argument is assigned to the 'id' of the div element, and 879 must be unique within the document. The show, start, and step are 880 used for line numbering. 881 882 Note this is not like most formatter methods, it can not take any 883 extra keyword arguments. 884 885 Call once with on=1 to start the region, and a second time 886 with on=0 to end it. 887 888 the msg string is not escaped 889 """ 890 _ = self.request.getText 891 res = [] 892 if on: 893 code_id = self.sanitize_to_id('CA-%s' % code_id) 894 ci = self.qualify_id(self.make_id_unique(code_id)) 895 896 # Open a code area 897 self._in_code_area = 1 898 self._in_code_line = 0 899 # id in here no longer used 900 self._code_area_state = [None, show, start, step, start, ci] 901 902 if msg: 903 attr = {'class': 'codemsg'} 904 res.append(self._open('div', attr={'class': 'codemsg'})) 905 res.append(msg) 906 res.append(self._close('div')) 907 908 # Open the code div - using left to right always! 909 attr = {'class': 'codearea', 'lang': 'en', 'dir': 'ltr'} 910 res.append(self._open('div', attr=attr)) 911 912 # Add the script only in the first code area on the page 913 if self._code_area_js == 0 and self._code_area_state[1] >= 0: 914 res.append(self._toggleLineNumbersScript) 915 self._code_area_js = 1 916 917 # Add line number link, but only for JavaScript enabled browsers. 918 if self._code_area_state[1] >= 0: 919 toggleLineNumbersLink = r''' 920<script type="text/javascript"> 921document.write('<a href="#" onclick="return togglenumber(\'%s\', %d, %d);" \ 922 class="codenumbers">%s<\/a>'); 923</script> 924''' % (ci, self._code_area_state[2], self._code_area_state[3], 925 _("Toggle line numbers")) 926 res.append(toggleLineNumbersLink) 927 928 # Open pre - using left to right always! 929 attr = {'id': ci, 'lang': 'en', 'dir': 'ltr'} 930 res.append(self._open('pre', newline=True, attr=attr, is_unique=True)) 931 else: 932 # Close code area 933 res = [] 934 if self._in_code_line: 935 res.append(self.code_line(0)) 936 res.append(self._close('pre')) 937 res.append(self._close('div')) 938 939 # Update state 940 self._in_code_area = 0 941 942 return ''.join(res) 943 944 def code_line(self, on): 945 res = '' 946 if not on or (on and self._in_code_line): 947 res += '</span>\n' 948 if on: 949 res += '<span class="line">' 950 if self._code_area_state[1] > 0: 951 res += ('<span class="LineNumber"><a href="#%(fmt)s">%%(num)4d</a> </span><span class="LineAnchor" id="%(fmt)s"></span>' % {'fmt': self._code_id_format, }) % { 952 'id': self._code_area_state[5], 953 'num': self._code_area_state[4], 954 } 955 self._code_area_state[4] += self._code_area_state[3] 956 self._in_code_line = on != 0 957 return res 958 959 def code_token(self, on, tok_type): 960 return ['<span class="%s">' % tok_type, '</span>'][not on] 961 962 # Paragraphs, Lines, Rules ########################################### 963 964 def _indent_spaces(self): 965 """Returns space(s) for indenting the html source so list nesting is easy to read. 966 967 Note that this mostly works, but because of caching may not always be accurate.""" 968 if prettyprint: 969 return self.indentspace * self._indent_level 970 else: 971 return '' 972 973 def _newline(self): 974 """Returns the whitespace for starting a new html source line, properly indented.""" 975 if prettyprint: 976 return '\n' + self._indent_spaces() 977 else: 978 return '' 979 980 def linebreak(self, preformatted=1): 981 """Creates a line break in the HTML output. 982 983 If preformatted is true a <br> element is inserted, otherwise 984 the linebreak will only be visible in the HTML source. 985 """ 986 if self._in_code_area: 987 preformatted = 1 988 return ['\n', '<br>\n'][not preformatted] + self._indent_spaces() 989 990 def paragraph(self, on, **kw): 991 """Creates a paragraph with a <p> element. 992 993 Call once with on=1 to start the region, and a second time 994 with on=0 to end it. 995 """ 996 if self._terse: 997 return '' 998 FormatterBase.paragraph(self, on) 999 tag = 'p' 1000 if on: 1001 tagstr = self._open(tag, **kw) 1002 else: 1003 tagstr = self._close(tag) 1004 return tagstr 1005 1006 def rule(self, size=None, **kw): 1007 """Creates a horizontal rule with an <hr> element. 1008 1009 If size is a number in the range [1..6], the CSS class of the rule 1010 is set to 'hr1' through 'hr6'. The intent is that the larger the 1011 size number the thicker or bolder the rule will be. 1012 """ 1013 if size and 1 <= size <= 6: 1014 # Add hr class: hr1 - hr6 1015 return self._open('hr', newline=1, attr={'class': 'hr%d' % size}, **kw) 1016 return self._open('hr', newline=1, **kw) 1017 1018 # Images / Transclusion ############################################## 1019 1020 def icon(self, type): 1021 return self.request.theme.make_icon(type) 1022 1023 smiley = icon 1024 1025 def image(self, src=None, **kw): 1026 """Creates an inline image with an <img> element. 1027 1028 The src argument must be the URL to the image file. 1029 """ 1030 if src: 1031 kw['src'] = src 1032 return self._open('img', **kw) 1033 1034 def transclusion(self, on, **kw): 1035 """Transcludes (includes/embeds) another object.""" 1036 if on: 1037 return self._open('object', 1038 allowed_attrs=['archive', 'classid', 'codebase', 1039 'codetype', 'data', 'declare', 1040 'height', 'name', 'standby', 1041 'type', 'width', ], 1042 **kw) 1043 else: 1044 return self._close('object') 1045 1046 def transclusion_param(self, **kw): 1047 """Give a parameter to a transcluded object.""" 1048 return self._open('param', 1049 allowed_attrs=['name', 'type', 'value', 'valuetype', ], 1050 **kw) 1051 1052 # Lists ############################################################## 1053 1054 def number_list(self, on, type=None, start=None, **kw): 1055 """Creates an HTML ordered list, <ol> element. 1056 1057 The 'type' if specified can be any legal numbered 1058 list-style-type, such as 'decimal','lower-roman', etc. 1059 1060 The 'start' argument if specified gives the numeric value of 1061 the first list item (default is 1). 1062 1063 Call once with on=1 to start the list, and a second time 1064 with on=0 to end it. 1065 """ 1066 tag = 'ol' 1067 if on: 1068 attr = {} 1069 if type is not None: 1070 attr['type'] = type 1071 if start is not None: 1072 attr['start'] = start 1073 tagstr = self._open(tag, newline=1, attr=attr, **kw) 1074 else: 1075 tagstr = self._close(tag, newline=1) 1076 return tagstr 1077 1078 def bullet_list(self, on, **kw): 1079 """Creates an HTML ordered list, <ul> element. 1080 1081 The 'type' if specified can be any legal unnumbered 1082 list-style-type, such as 'disc','square', etc. 1083 1084 Call once with on=1 to start the list, and a second time 1085 with on=0 to end it. 1086 """ 1087 tag = 'ul' 1088 if on: 1089 tagstr = self._open(tag, newline=1, **kw) 1090 else: 1091 tagstr = self._close(tag, newline=1) 1092 return tagstr 1093 1094 def listitem(self, on, **kw): 1095 """Adds a list item, <li> element, to a previously opened 1096 bullet or number list. 1097 1098 Call once with on=1 to start the region, and a second time 1099 with on=0 to end it. 1100 """ 1101 tag = 'li' 1102 if on: 1103 tagstr = self._open(tag, newline=1, **kw) 1104 else: 1105 tagstr = self._close(tag, newline=1) 1106 return tagstr 1107 1108 def definition_list(self, on, **kw): 1109 """Creates an HTML definition list, <dl> element. 1110 1111 Call once with on=1 to start the list, and a second time 1112 with on=0 to end it. 1113 """ 1114 tag = 'dl' 1115 if on: 1116 tagstr = self._open(tag, newline=1, **kw) 1117 else: 1118 tagstr = self._close(tag, newline=1) 1119 return tagstr 1120 1121 def definition_term(self, on, **kw): 1122 """Adds a new term to a definition list, HTML element <dt>. 1123 1124 Call once with on=1 to start the term, and a second time 1125 with on=0 to end it. 1126 """ 1127 tag = 'dt' 1128 if on: 1129 tagstr = self._open(tag, newline=1, **kw) 1130 else: 1131 tagstr = self._close(tag, newline=0) 1132 return tagstr 1133 1134 def definition_desc(self, on, **kw): 1135 """Gives the definition to a definition item, HTML element <dd>. 1136 1137 Call once with on=1 to start the definition, and a second time 1138 with on=0 to end it. 1139 """ 1140 tag = 'dd' 1141 if on: 1142 tagstr = self._open(tag, newline=1, **kw) 1143 else: 1144 tagstr = self._close(tag, newline=0) 1145 return tagstr 1146 1147 def heading(self, on, depth, **kw): 1148 # remember depth of first heading, and adapt counting depth accordingly 1149 if not self._base_depth: 1150 self._base_depth = depth 1151 1152 count_depth = max(depth - (self._base_depth - 1), 1) 1153 1154 # check numbering, possibly changing the default 1155 if self._show_section_numbers is None: 1156 self._show_section_numbers = self.cfg.show_section_numbers 1157 numbering = self.request.getPragma('section-numbers', '').lower() 1158 if numbering in ['0', 'off']: 1159 self._show_section_numbers = 0 1160 elif numbering in ['1', 'on']: 1161 self._show_section_numbers = 1 1162 elif numbering in ['2', '3', '4', '5', '6']: 1163 # explicit base level for section number display 1164 self._show_section_numbers = int(numbering) 1165 1166 heading_depth = depth 1167 1168 # closing tag, with empty line after, to make source more readable 1169 if not on: 1170 return self._close('h%d' % heading_depth) + '\n' 1171 1172 # create section number 1173 number = '' 1174 if self._show_section_numbers: 1175 # count headings on all levels 1176 self.request._fmt_hd_counters = self.request._fmt_hd_counters[:count_depth] 1177 while len(self.request._fmt_hd_counters) < count_depth: 1178 self.request._fmt_hd_counters.append(0) 1179 self.request._fmt_hd_counters[-1] = self.request._fmt_hd_counters[-1] + 1 1180 number = '.'.join([str(x) for x in self.request._fmt_hd_counters[self._show_section_numbers-1:]]) 1181 if number: number += ". " 1182 1183 # Add space before heading, easier to check source code 1184 result = '\n' + self._open('h%d' % heading_depth, **kw) 1185 1186 if self.request.user.show_topbottom: 1187 result += "%s%s%s%s%s%s" % ( 1188 self.anchorlink(1, "bottom"), self.icon('bottom'), self.anchorlink(0), 1189 self.anchorlink(1, "top"), self.icon('top'), self.anchorlink(0)) 1190 1191 return "%s%s" % (result, number) 1192 1193 1194 # Tables ############################################################# 1195 1196 _allowed_table_attrs = { 1197 'table': ['class', 'id', 'style'], 1198 'row': ['class', 'id', 'style'], 1199 '': ['colspan', 'rowspan', 'class', 'id', 'style', 'abbr'], 1200 } 1201 1202 def _checkTableAttr(self, attrs, prefix): 1203 """ Check table attributes 1204 1205 Convert from wikitable attributes to html 4 attributes. 1206 1207 @param attrs: attribute dict 1208 @param prefix: used in wiki table attributes 1209 @rtype: dict 1210 @return: valid table attributes 1211 """ 1212 if not attrs: 1213 return {} 1214 1215 result = {} 1216 s = [] # we collect synthesized style in s 1217 for key, val in attrs.items(): 1218 # Ignore keys that don't start with prefix 1219 if prefix and key[:len(prefix)] != prefix: 1220 continue 1221 key = key[len(prefix):] 1222 val = val.strip('"') 1223 # remove invalid attrs from dict and synthesize style 1224 if key == 'width': 1225 s.append("width: %s" % val) 1226 elif key == 'height': 1227 s.append("height: %s" % val) 1228 elif key == 'bgcolor': 1229 s.append("background-color: %s" % val) 1230 elif key == 'align': 1231 s.append("text-align: %s" % val) 1232 elif key == 'valign': 1233 s.append("vertical-align: %s" % val) 1234 # Ignore unknown keys 1235 if key not in self._allowed_table_attrs[prefix]: 1236 continue 1237 result[key] = val 1238 st = result.get('style', '').split(';') 1239 st = '; '.join(st + s) 1240 st = st.strip(';') 1241 st = st.strip() 1242 if not st: 1243 try: 1244 del result['style'] # avoid empty style attr 1245 except: 1246 pass 1247 else: 1248 result['style'] = st 1249 #logging.debug("_checkTableAttr returns %r" % result) 1250 return result 1251 1252 1253 def table(self, on, attrs=None, **kw): 1254 """ Create table 1255 1256 @param on: start table 1257 @param attrs: table attributes 1258 @rtype: string 1259 @return start or end tag of a table 1260 """ 1261 result = [] 1262 if on: 1263 # Open div to get correct alignment with table width smaller 1264 # than 100% 1265 result.append(self._open('div', newline=1)) 1266 1267 # Open table 1268 if not attrs: 1269 attrs = {} 1270 else: 1271 attrs = self._checkTableAttr(attrs, 'table') 1272 result.append(self._open('table', newline=1, attr=attrs, 1273 allowed_attrs=self._allowed_table_attrs['table'], 1274 **kw)) 1275 result.append(self._open('tbody', newline=1)) 1276 else: 1277 # Close tbody, table, and then div 1278 result.append(self._close('tbody')) 1279 result.append(self._close('table')) 1280 result.append(self._close('div')) 1281 1282 return ''.join(result) 1283 1284 def table_row(self, on, attrs=None, **kw): 1285 tag = 'tr' 1286 if on: 1287 if not attrs: 1288 attrs = {} 1289 else: 1290 attrs = self._checkTableAttr(attrs, 'row') 1291 return self._open(tag, newline=1, attr=attrs, 1292 allowed_attrs=self._allowed_table_attrs['row'], 1293 **kw) 1294 return self._close(tag) + '\n' 1295 1296 def table_cell(self, on, attrs=None, **kw): 1297 tag = 'td' 1298 if on: 1299 if not attrs: 1300 attrs = {} 1301 else: 1302 attrs = self._checkTableAttr(attrs, '') 1303 return ' ' + self._open(tag, attr=attrs, 1304 allowed_attrs=self._allowed_table_attrs[''], 1305 **kw) 1306 return self._close(tag) + '\n' 1307 1308 def text(self, text, **kw): 1309 txt = FormatterBase.text(self, text, **kw) 1310 if kw: 1311 return self._open('span', **kw) + txt + self._close('span') 1312 return txt 1313 1314 def escapedText(self, text, **kw): 1315 txt = wikiutil.escape(text) 1316 if kw: 1317 return self._open('span', **kw) + txt + self._close('span') 1318 return txt 1319 1320 def rawHTML(self, markup): 1321 return markup 1322 1323 def sysmsg(self, on, **kw): 1324 tag = 'div' 1325 if on: 1326 return self._open(tag, attr={'class': 'message'}, **kw) 1327 return self._close(tag) 1328 1329 def div(self, on, **kw): 1330 css_class = kw.get('css_class') 1331 # the display of comment class divs depends on a user setting: 1332 if css_class and 'comment' in css_class.split(): 1333 style = kw.get('style') 1334 display = self.request.user.show_comments and "display:''" or "display:none" 1335 if not style: 1336 style = display 1337 else: 1338 style += "; %s" % display 1339 kw['style'] = style 1340 tag = 'div' 1341 if on: 1342 return self._open(tag, **kw) 1343 return self._close(tag) 1344 1345 def span(self, on, **kw): 1346 css_class = kw.get('css_class') 1347 # the display of comment class spans depends on a user setting: 1348 if css_class and 'comment' in css_class.split(): 1349 style = kw.get('style') 1350 display = self.request.user.show_comments and "display:''" or "display:none" 1351 if not style: 1352 style = display 1353 else: 1354 style += "; %s" % display 1355 kw['style'] = style 1356 tag = 'span' 1357 if on: 1358 return self._open(tag, **kw) 1359 return self._close(tag) 1360 1361 def sanitize_to_id(self, text): 1362 return wikiutil.anchor_name_from_text(text) 1363 1364