1 #!/usr/bin/env python 2 3 """ 4 Moin wiki format parser. 5 6 Copyright (C) 2017, 2018 Paul Boddie <paul@boddie.org.uk> 7 8 This program is free software; you can redistribute it and/or modify it under 9 the terms of the GNU General Public License as published by the Free Software 10 Foundation; either version 3 of the License, or (at your option) any later 11 version. 12 13 This program is distributed in the hope that it will be useful, but WITHOUT 14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 15 FOR A PARTICULAR PURPOSE. See the GNU General Public License for more 16 details. 17 18 You should have received a copy of the GNU General Public License along with 19 this program. If not, see <http://www.gnu.org/licenses/>. 20 """ 21 22 # Document transformations. 23 24 from moinformat.macros import get_macro 25 26 # Parser functionality and pattern definition. 27 28 from moinformat.parsers.common import ParserBase, get_patterns, choice, \ 29 excl, expect, group, optional, recur, \ 30 repeat 31 32 # Serialisation. 33 34 from moinformat.serialisers import serialise 35 36 # Document tree nodes. 37 38 from moinformat.tree.moin import Anchor, Break, Comment, DefItem, DefTerm, \ 39 Directive, FontStyle, Heading, Larger, \ 40 LineBreak, Link, List, ListItem, Macro, \ 41 Monospace, Region, Rule, Smaller, \ 42 Strikethrough, Subscript, Superscript, Table, \ 43 TableAttr, TableAttrs, TableCell, TableRow, \ 44 Text, Underline 45 46 join = "".join 47 48 class MoinParser(ParserBase): 49 50 "A wiki region parser." 51 52 format = "moin" 53 54 def __init__(self, formats=None, root=None): 55 56 """ 57 Initialise the parser with any given 'formats' mapping from region type 58 names to parser objects. An optional 'root' indicates the document-level 59 parser. 60 """ 61 62 # Introduce this class as the default parser for the wiki format. 63 64 default_formats = {"wiki" : MoinParser, "moin" : MoinParser} 65 if formats: 66 default_formats.update(formats) 67 68 ParserBase.__init__(self, default_formats, root) 69 70 # Record certain node occurrences for later evaluation. 71 72 self.macros = [] 73 74 # Record headings for identifier disambiguation. 75 76 self.headings = [] 77 78 # Principal parser methods. 79 80 def parse(self, s): 81 82 """ 83 Parse page text 's'. Pages consist of regions delimited by markers. 84 """ 85 86 self.items = self.get_items(s) 87 self.region = Region([], type="moin") 88 89 # Parse page header and directives. 90 91 self.parse_region_header(self.region) 92 self.parse_region_directives(self.region) 93 94 # Handle pages directly with this parser. Pages do not need to use an 95 # explicit format indicator. 96 97 if not self.region.type: 98 self.parse_region_content(self.items, self.region) 99 100 # Otherwise, test the type and find an appropriate parser. 101 102 else: 103 self.parse_region_type(self.region) 104 105 # Assign heading identifiers. 106 107 self.identify_headings() 108 109 return self.region 110 111 112 113 # Macro evaluation. 114 115 def evaluate_macros(self): 116 117 "Evaluate the macro nodes in the document." 118 119 for node in self.macros: 120 121 # Obtain a class for the named macro. 122 123 macro_cls = get_macro(node.name) 124 if not macro_cls: 125 continue 126 127 # Instantiate the class and evaluate the macro. 128 129 macro = macro_cls(node, self.region) 130 macro.evaluate() 131 132 # Heading disambiguation. 133 134 def identify_headings(self): 135 136 "Assign identifiers to headings based on their textual content." 137 138 d = {} 139 140 for heading in self.headings: 141 text = heading.text_content() 142 143 if not d.has_key(text): 144 d[text] = 0 145 heading.identifier = text 146 else: 147 d[text] += 1 148 heading.identifier = "%s-%d" % (text, d[text]) 149 150 151 152 # Parser methods supporting different page features. 153 154 def parse_attrname(self, attrs): 155 156 "Handle an attribute name within 'attrs'." 157 158 name = self.match_group("name") 159 attr = TableAttr(name) 160 161 preceding = self.read_until(["attrvalue"], False) 162 if preceding == "": 163 attr.quote = self.match_group("quote") 164 attr.value = self.match_group("value") 165 166 attrs.append(attr) 167 168 def parse_break(self, region): 169 170 "Handle a paragraph break within 'region'." 171 172 self.add_node(region, Break()) 173 self.new_block(region) 174 175 def parse_comment(self, region): 176 177 "Handle a comment within 'region'." 178 179 comment = self.match_group("comment") 180 extra = self.match_group("extra") 181 self.add_node(region, Comment(comment, extra)) 182 self.new_block(region) 183 184 def parse_defitem(self, region, extra=""): 185 186 "Handle a definition item within 'region'." 187 188 pad = self.match_group("pad") 189 item = DefItem([], pad, extra) 190 self.parse_region_details(item, self.listitem_pattern_names) 191 self.add_node(region, item) 192 self.new_block(region) 193 194 def parse_defterm(self, region): 195 196 "Handle a definition term within 'region'." 197 198 pad = self.match_group("pad") 199 term = DefTerm([], pad) 200 self.parse_region_details(term, ["deftermend", "deftermsep"]) 201 self.add_node(region, term) 202 203 if self.matching_pattern() == "deftermsep": 204 self.parse_defitem(region) 205 206 # Add padding from the separator to the term, there being no item. 207 208 else: 209 term.extra = self.match_group("pad") 210 211 def parse_defterm_empty(self, region): 212 213 "Handle an empty definition term within 'region'." 214 215 extra = self.match_group("pad") 216 self.parse_region_details(region, ["deftermsep"]) 217 self.parse_defitem(region, extra) 218 219 def parse_directive(self, region): 220 221 "Handle a processing directive within 'region'." 222 223 directive = self.match_group("directive") 224 extra = self.match_group("extra") 225 self.add_node(region, Directive(directive, extra)) 226 self.new_block(region) 227 228 def parse_fontstyle(self, region): 229 230 "Handle emphasis and strong styles." 231 232 n = len(self.match_group("style")) 233 234 # Handle endings. 235 236 if isinstance(region, FontStyle): 237 emphasis = n in (2, 4, 5) 238 strong = n in (3, 5, 6) 239 active = True 240 241 if region.emphasis and emphasis: 242 active = region.close_emphasis() 243 n -= 2 244 if region.strong and strong: 245 active = region.close_strong() 246 n -= 3 247 248 if not active: 249 if n: 250 self.items.rewind(n) 251 raise StopIteration 252 253 elif not n: 254 return 255 256 # Handle new styles. 257 258 emphasis = n in (2, 4, 5) 259 strong = n in (3, 5, 6) 260 double = n in (4, 6) 261 262 span = FontStyle([], emphasis, strong) 263 if not double: 264 self.parse_region_details(span, self.inline_pattern_names) 265 region.append_inline(span) 266 267 def parse_halign(self, attrs): 268 269 "Handle horizontal alignment within 'attrs'." 270 271 value = self.match_group("value") 272 attr = TableAttr("halign", value == "(" and "left" or value == ")" and "right" or "center", True) 273 attrs.append(attr) 274 275 def parse_heading(self, region): 276 277 "Handle a heading." 278 279 start_extra = self.match_group("extra") 280 level = len(self.match_group("level")) 281 start_pad = self.match_group("pad") 282 heading = Heading([], level, start_extra, start_pad) 283 self.parse_region_details(heading, ["headingend"] + self.inline_pattern_names) 284 self.add_node(region, heading) 285 self.new_block(region) 286 287 # Record the heading for later processing. 288 289 self.root.headings.append(heading) 290 291 def parse_heading_end(self, heading): 292 293 "Handle the end of a heading." 294 295 level = len(self.match_group("level")) 296 if heading.level == level: 297 heading.end_pad = self.match_group("pad") 298 heading.end_extra = self.match_group("extra") 299 raise StopIteration 300 301 def parse_list(self, item): 302 303 "Create a list, starting with 'item'." 304 305 list = List([item], item.indent, item.marker, item.num) 306 self.parse_region_details(list, self.list_pattern_names, True) 307 return list 308 309 def parse_listitem(self, region): 310 311 "Handle a list item marker within 'region'." 312 313 indent = len(self.match_group("indent")) 314 marker = self.match_group("marker") 315 num = self.match_group("num") 316 space = self.match_group("pad") 317 318 last = region.node(-1) 319 320 new_list = not isinstance(last, (List, ListItem)) 321 same_indent = not new_list and indent == last.indent 322 new_marker = not new_list and last.marker != marker and same_indent 323 new_num = not new_list and num is not None and last.num != num and same_indent 324 325 # If the marker or number changes at the same indent, or if the indent 326 # is smaller, queue the item and end the list. 327 328 # Note that Moin format does not seek to support item renumbering, 329 # instead starting new lists on number changes. 330 331 if not new_list and (new_marker or new_num or indent < last.indent): 332 self.queue_match() 333 self.end_region(region) 334 335 # Obtain a list item and populate it. 336 337 item = ListItem([], indent, marker, space, num) 338 self.parse_region_details(item, self.listitem_pattern_names) 339 340 # Start a new list if not preceded by a list item, adding a trailing 341 # block for new elements. 342 343 if new_list: 344 item = self.parse_list(item) 345 self.add_node(region, item) 346 self.new_block(region) 347 348 # Add a nested list to the last item. 349 350 elif indent > last.indent: 351 item = self.parse_list(item) 352 self.add_node(last, item) 353 354 # Add the item to the current list. 355 356 else: 357 self.add_node(region, item) 358 359 def parse_rule(self, region): 360 361 "Handle a horizontal rule within 'region'." 362 363 length = len(self.match_group("rule")) 364 rule = Rule(length) 365 self.add_node(region, rule) 366 self.new_block(region) 367 368 def parse_section(self, region): 369 370 "Handle the start of a new section within 'region'." 371 372 # Parse the section and start a new block after the section. 373 374 indent = len(self.match_group("indent")) 375 level = len(self.match_group("level")) 376 377 section = self.parse_region(level, indent, "inline") 378 379 # If the section is inline, treat it like any other inline element. 380 381 if section.type == "inline": 382 region.append_inline(section) 383 384 # Otherwise, add it as a new block element. 385 386 else: 387 self.add_node(region, section) 388 if region.allow_blocks: 389 self.new_block(region) 390 391 def parse_table_attrs(self, cell): 392 393 "Handle the start of table attributes within 'cell'." 394 395 attrs = TableAttrs([]) 396 self.parse_region_details(attrs, self.table_attr_pattern_names) 397 398 # Test the validity of the attributes. 399 400 last = None 401 402 for node in attrs.nodes: 403 404 # Text separator nodes must be whitespace. 405 406 if isinstance(node, Text): 407 if node.s.strip(): 408 break 409 410 # Named attributes must be preceded by space if not the first. 411 412 elif last and not node.concise and not isinstance(last, Text): 413 break 414 415 last = node 416 417 # All nodes were valid: preserve the collection. 418 419 else: 420 # Add the attributes as a node, also recording their presence. 421 422 cell.append(attrs) 423 cell.attrs = attrs 424 return 425 426 # Invalid nodes were found: serialise the attributes as text. 427 428 cell.append_inline(Text(serialise(attrs))) 429 430 def parse_table_row(self, region): 431 432 "Handle the start of a table row within 'region'." 433 434 # Identify any active table. 435 436 table = region.node(-2) 437 block = region.node(-1) 438 439 if not (isinstance(table, Table) and block.empty()): 440 new_table = table = Table([]) 441 else: 442 new_table = None 443 444 row = TableRow([]) 445 446 while True: 447 cell = TableCell([]) 448 self.parse_region_details(cell, self.table_row_pattern_names) 449 450 # Handle the end of the row. 451 452 if self.matching_pattern() == "tableend": 453 trailing = self.match_group("extra") 454 455 # If the cell was started but not finished, convert the row into text. 456 457 if not row.nodes or not cell.empty(): 458 for node in row.nodes: 459 region.append_inline(Text(serialise(node))) 460 region.append_inline(Text(serialise(cell) + trailing)) 461 462 self.new_block(region) 463 return 464 465 # Append the final cell, if not empty. 466 467 else: 468 row.trailing = trailing 469 470 if not cell.empty(): 471 row.append(cell) 472 break 473 474 # A cell separator has been found. 475 476 row.append(cell) 477 478 # Add the row to the table and any new table to the region. 479 480 table.add(row) 481 if new_table: 482 self.add_node(region, new_table) 483 484 self.new_block(region) 485 486 def parse_valign(self, attrs): 487 488 "Handle vertical alignment within 'attrs'." 489 490 value = self.match_group("value") 491 attr = TableAttr("valign", value == "^" and "top" or "bottom", True) 492 attrs.append(attr) 493 494 495 496 def inline_patterns_for(self, name): 497 names = self.inline_pattern_names[:] 498 names[names.index(name)] = "%send" % name 499 return names 500 501 502 503 # Inline formatting handlers. 504 505 def parse_inline(self, region, cls, pattern_name): 506 507 "Handle an inline region." 508 509 span = cls([]) 510 self.parse_region_details(span, self.inline_patterns_for(pattern_name)) 511 region.append_inline(span) 512 513 def parse_larger(self, region): 514 self.parse_inline(region, Larger, "larger") 515 516 def parse_monospace(self, region): 517 span = Monospace([]) 518 self.parse_region_details(span, ["monospaceend"]) 519 region.append_inline(span) 520 521 def parse_smaller(self, region): 522 self.parse_inline(region, Smaller, "smaller") 523 524 def parse_strike(self, region): 525 self.parse_inline(region, Strikethrough, "strike") 526 527 def parse_sub(self, region): 528 self.parse_inline(region, Subscript, "sub") 529 530 def parse_super(self, region): 531 self.parse_inline(region, Superscript, "super") 532 533 def parse_underline(self, region): 534 self.parse_inline(region, Underline, "underline") 535 536 537 538 # Complete inline pattern handlers. 539 540 def parse_anchor(self, region): 541 target = self.match_group("target") 542 anchor = Anchor(target) 543 region.append_inline(anchor) 544 545 def parse_linebreak(self, region): 546 region.append_inline(LineBreak()) 547 548 def parse_link(self, region): 549 target = self.match_group("target") 550 text = self.match_group("text") 551 link = Link(text and [Text(text)] or [], target) 552 region.append_inline(link) 553 554 def parse_macro(self, region): 555 name = self.match_group("name") 556 args = self.match_group("args") 557 558 # Obtain the raw arguments. Moin usually leaves it to the macro to 559 # interpret the individual arguments. 560 561 arglist = args and args.split(",") or [] 562 macro = Macro(name, arglist, region.append_point()) 563 region.append_inline(macro) 564 565 # Record the macro for later processing. 566 567 self.root.macros.append(macro) 568 569 570 571 # Table attribute handlers. 572 573 def parse_table_attr(self, attrs, pattern_name): 574 575 "Handle a table attribute." 576 577 attrs.append(TableAttr(pattern_name, self.match_group("value"), True)) 578 579 def parse_colour(self, cell): 580 self.parse_table_attr(cell, "colour") 581 582 def parse_colspan(self, cell): 583 self.parse_table_attr(cell, "colspan") 584 585 def parse_rowspan(self, cell): 586 self.parse_table_attr(cell, "rowspan") 587 588 def parse_width(self, cell): 589 self.parse_table_attr(cell, "width") 590 591 592 593 # Regular expressions. 594 595 syntax = { 596 # Page regions: 597 598 "regionstart" : join((group("indent", r"\N*"), # ws... (optional) 599 group("level", repeat("[{]", 3)))), # {{{... 600 601 "regionend" : join((r"\N*", # ws... (optional) 602 group("feature", join(( 603 group("level", repeat("[}]", 3)), # }}}... 604 optional(group("extra", r"\n"))))))), # nl (optional) 605 606 # Region header and directives: 607 608 "header" : join(("#!", # #! 609 group("args", ".*?"), "\n")), # text-excl-nl 610 611 "directive" : join((r"^#", # # 612 group("directive", r".*?$"), # rest of line 613 optional(group("extra", r"\n")))), # nl (optional) 614 615 # Region contents: 616 617 # Line-oriented patterns support features which require their own 618 # separate lines. 619 620 "break" : r"^(\s*?)\n", # blank line 621 622 "comment" : join((r"^##", # ## 623 group("comment", r".*?$"), # rest of line 624 optional(group("extra", r"\n")))), # nl (optional) 625 626 "defterm" : join(("^", 627 group("pad", r"\N+"), # ws... 628 expect(".+?::"))), # text :: 629 630 "defterm_empty" : join(("^", 631 group("pad", r"\N+"), # ws... 632 expect("::\s+"))), # :: ws... 633 634 "heading" : join(("^", 635 group("extra", r"\N*"), # ws... (optional) 636 group("level", "=+"), # =... 637 group("pad", r"\s+"), # ws... 638 expect(join((r".*?\N+", # text 639 recur("level"), # =... 640 r"\N*$"))))), # ws... (optional) 641 642 "listitem" : join(("^", 643 group("indent", r"\N+"), # ws... 644 group("marker", r"\*"), # list-marker 645 group("pad", r"\s*"))), # ws... (optional) 646 647 "listitem_num" : join(("^", 648 group("indent", r"\N+"), # ws... 649 group("marker", r"\d+\."), # decimal-marker 650 optional(join(("#", group("num", r"\d+")))), # # num (optional) 651 group("pad", r"\s+"))), # ws... 652 653 "listitem_alpha": join(("^", 654 group("indent", r"\N+"), # ws... 655 group("marker", r"[aA]\."), # alpha-marker 656 optional(join(("#", group("num", r"\d+")))), # # num (optional) 657 group("pad", r"\s+"))), # ws... 658 659 "listitem_roman": join(("^", 660 group("indent", r"\N+"), # ws... 661 group("marker", r"[iI]\."), # roman-marker 662 optional(join(("#", group("num", r"\d+")))), # # num (optional) 663 group("pad", r"\s+"))), # ws... 664 665 "listitem_dot" : join(("^", 666 group("indent", r"\N+"), # ws... 667 group("marker", r"\."), # dot-marker 668 group("pad", r"\s*"))), # ws... (optional) 669 670 "tablerow" : r"^\|\|", # || 671 672 # Region contents: 673 674 # Inline patterns are for markup features that appear within blocks. 675 # The patterns below start inline spans that can contain other markup 676 # features. 677 678 "fontstyle" : group("style", repeat("'", 2, 6)), # ''... 679 "larger" : r"~\+", # ~+ 680 "monospace" : r"`", # ` 681 "rule" : group("rule", "-----*"), # ----... 682 "smaller" : r"~-", # ~- 683 "strike" : r"--\(", # --( 684 "sub" : r",,", # ,, 685 "super" : r"\^", # ^ 686 "underline" : r"__", # __ 687 688 # Complete inline patterns are for markup features that do not support 689 # arbitrary content within them: 690 691 "anchor" : join((r"\(\(", # (( 692 group("target", ".*?"), # target 693 r"\)\)")), # )) 694 695 "linebreak" : r"\\\\", # \\ 696 697 "link" : join((r"\[\[", # [[ 698 group("target", ".*?"), # target 699 optional(join((r"\|", group("text", ".*?")))), # | text (optional) 700 "]]")), # ]] 701 702 "macro" : join(("<<", # << 703 group("name", "\w+?"), # digit-letter... 704 optional(join((r"\(", # ( (optional) 705 group("args", ".*?"), # not-)... 706 r"\)"))), # ) (optional) 707 ">>")), # >> 708 709 # Ending patterns for inline features: 710 711 "largerend" : r"\+~", # +~ 712 "monospaceend" : r"`", # ` 713 "smallerend" : r"-~", # -~ 714 "strikeend" : r"\)--", # )-- 715 "subend" : r",,", # ,, 716 "superend" : r"\^", # ^ 717 "underlineend" : r"__", # __ 718 719 # Heading contents: 720 721 "headingend" : join((group("pad", r"\N+"), # ws... 722 group("level", "=+"), # =... 723 group("extra", r"\N*\n"))), # ws (optional) nl 724 725 # List contents: 726 727 "deftermend" : join(("::", group("pad", r"\s*?\n"))), # :: 728 # ws... (optional) 729 # nl 730 731 "deftermsep" : join(("::", group("pad", r"\s+"))), # :: 732 # ws... 733 734 "listitemend" : join((r"^", # next line 735 choice((expect(r"[^\s]"), # without indent 736 expect(r"\Z"), # end of string 737 expect(r"\N+\*"), # or with ws... list-marker 738 expect(r"\N+\d\."), # or with ws... decimal-marker 739 expect(r"\N+[aA]\."), # or with ws... alpha-marker 740 expect(r"\N+[iI]\."), # or with ws... roman-marker 741 expect(r"\N+\."), # or with ws... dot-marker 742 expect(r"\N+.+?::\s"), # or with ws... text :: ws (next defterm) 743 expect(r"\N+::\s"))))), # or with ws... :: ws (next defitem) 744 745 # Table contents: 746 747 "tableattrs" : join(("<", # lt 748 excl("<"))), # not-lt 749 750 "tablecell" : r"\|\|", # || 751 752 "tableend" : join((group("extra", r"\s*?"), # ws... (optional) 753 "^")), # next line 754 755 # Table attributes: 756 757 "tableattrsend" : r">", # > 758 "halign" : group("value", "[(:)]"), # halign-marker 759 "valign" : group("value", "[v^]"), # valign-marker 760 "colour" : group("value", join(("\#", # # 761 repeat("[0-9A-F]", 6, 6)))), # nnnnnn 762 763 "colspan" : join(("-", # - 764 group("value", "\d+"))), # n... 765 766 "rowspan" : join((r"\|", # | 767 group("value", "\d+"))), # n... 768 769 "width" : group("value", "\d+%"), # n... % 770 771 "attrname" : join((excl(r"[-\d]"), # not-dash-or-digit 772 group("name", r"[-\w]+"))), # dash-digit-letter... 773 774 "attrvalue" : join(("=", group("quote", r"\Q"), # quote 775 group("value", ".*?"), # non-quote... (optional) 776 recur("quote"))), # quote 777 } 778 779 patterns = get_patterns(syntax) 780 781 782 783 # Patterns available within certain markup features. 784 785 table_attr_pattern_names = [ 786 "attrname", "colour", "colspan", "halign", "rowspan", "tableattrsend", 787 "valign", "width" 788 ] 789 790 inline_pattern_names = [ 791 "anchor", "fontstyle", "larger", "linebreak", "link", "macro", 792 "monospace", "regionstart", "smaller", "strike", "sub", "super", 793 "underline", 794 ] 795 796 list_pattern_names = [ 797 "listitem", "listitem_alpha", "listitem_dot", "listitem_num", 798 "listitem_roman", 799 ] 800 801 listitem_pattern_names = inline_pattern_names + ["listitemend"] 802 803 region_without_table_pattern_names = inline_pattern_names + list_pattern_names + [ 804 "break", "comment", "heading", "defterm", "defterm_empty", 805 "regionend", "rule", 806 ] 807 808 table_row_pattern_names = inline_pattern_names + [ 809 "tableattrs", "tablecell", "tableend" 810 ] 811 812 # The region pattern names are specifically used by the common parser 813 # functionality. 814 815 region_pattern_names = region_without_table_pattern_names + ["tablerow"] 816 817 818 819 # Pattern handlers. 820 821 end_region = ParserBase.end_region 822 parse_section_end = ParserBase.parse_region_end 823 824 handlers = { 825 None : end_region, 826 "anchor" : parse_anchor, 827 "attrname" : parse_attrname, 828 "break" : parse_break, 829 "colour" : parse_colour, 830 "colspan" : parse_colspan, 831 "comment" : parse_comment, 832 "defterm" : parse_defterm, 833 "defterm_empty" : parse_defterm_empty, 834 "deftermend" : end_region, 835 "deftermsep" : end_region, 836 "directive" : parse_directive, 837 "fontstyle" : parse_fontstyle, 838 "halign" : parse_halign, 839 "heading" : parse_heading, 840 "headingend" : parse_heading_end, 841 "larger" : parse_larger, 842 "largerend" : end_region, 843 "linebreak" : parse_linebreak, 844 "link" : parse_link, 845 "macro" : parse_macro, 846 "listitemend" : end_region, 847 "listitem" : parse_listitem, 848 "listitem_alpha" : parse_listitem, 849 "listitem_dot" : parse_listitem, 850 "listitem_num" : parse_listitem, 851 "listitem_roman" : parse_listitem, 852 "monospace" : parse_monospace, 853 "monospaceend" : end_region, 854 "regionstart" : parse_section, 855 "regionend" : parse_section_end, 856 "rowspan" : parse_rowspan, 857 "rule" : parse_rule, 858 "smaller" : parse_smaller, 859 "smallerend" : end_region, 860 "strike" : parse_strike, 861 "strikeend" : end_region, 862 "sub" : parse_sub, 863 "subend" : end_region, 864 "super" : parse_super, 865 "superend" : end_region, 866 "tableattrs" : parse_table_attrs, 867 "tableattrsend" : end_region, 868 "tablerow" : parse_table_row, 869 "tablecell" : end_region, 870 "tableend" : end_region, 871 "underline" : parse_underline, 872 "underlineend" : end_region, 873 "valign" : parse_valign, 874 "width" : parse_width, 875 } 876 877 parser = MoinParser 878 879 # vim: tabstop=4 expandtab shiftwidth=4