1#!/usr/local/bin/python3.8 2# 3# This file is part of the LibreOffice project. 4# 5# This Source Code Form is subject to the terms of the Mozilla Public 6# License, v. 2.0. If a copy of the MPL was not distributed with this 7# file, You can obtain one at http://mozilla.org/MPL/2.0/. 8# 9 10import os, sys, thread, threading, time, re, copy 11import xml.parsers.expat 12import codecs 13from threading import Thread 14 15root="source/" 16max_threads = 25 17 18titles = [] 19 20# map of id -> localized text 21localization_data = {} 22 23# to collect a list of pages that will be redirections to the pages with nice 24# names 25redirects = [] 26 27# to collect images that we will up-load later 28images = set() 29 30# various types of paragraphs 31replace_paragraph_role = \ 32 {'start':{'bascode': '', 33 'code': '<code>', 34 'codeintip': '<code>', 35 'emph' : '', # must be empty to be able to strip empty <emph/> 36 'example': '<code>', 37 'heading1': '= ', 38 'heading2': '== ', 39 'heading3': '=== ', 40 'heading4': '==== ', 41 'heading5': '===== ', 42 'heading6': '====== ', 43 'head1': '= ', # used only in one file, probably in error? 44 'head2': '== ', # used only in one file, probably in error? 45 'listitem': '', 46 'logocode': '<code>', 47 'note': '{{Note|1=', 48 'null': '', # special paragraph for Variable, CaseInline, etc. 49 'ol_item': '', 50 'paragraph': '', 51 'related': '', # used only in one file, probably in error? 52 'relatedtopics': '', # used only in one file, probably in error? 53 'sup' : '', 54 'tablecontent': '| | ', 55 'tablecontentcode': '| | <code>', 56 'tablecontentnote': '| |{{Note|1=', 57 'tablecontenttip': '| |{{Tip|1=', 58 'tablecontentwarning': '| |{{Warning|1=', 59 'tablehead': '! scope="col" | ', 60 'tablenextnote': '\n{{Note|1=', 61 'tablenextpara': '\n', 62 'tablenextparacode': '\n<code>', 63 'tablenexttip': '\n{{Tip|1=', 64 'tablenextwarning': '\n{{Warning|1=', 65 'tip': '{{Tip|1=', 66 'ul_item': '', 67 'variable': '', 68 'warning': '{{Warning|1=', 69 }, 70 'end':{'bascode': '\n', 71 'code': '</code>\n\n', 72 'codeintip': '</code>\n\n', 73 'emph' : '', 74 'example': '</code>\n\n', 75 'heading1': ' =\n\n', 76 'heading2': ' ==\n\n', 77 'heading3': ' ===\n\n', 78 'heading4': ' ====\n\n', 79 'heading5': ' =====\n\n', 80 'heading6': ' ======\n\n', 81 'head1': ' =\n\n', # used only in one file, probably in error? 82 'head2': ' ==\n\n', # used only in one file, probably in error? 83 'listitem': '', 84 'logocode': '</code>\n\n', 85 'note': '}}\n\n', 86 'null': '', # special paragraph for Variable, CaseInline, etc. 87 'ol_item': '', 88 'paragraph': '\n\n', 89 'related': '\n\n', # used only in one file, probably in error? 90 'relatedtopics': '\n\n', # used only in one file, probably in error? 91 'sup' : '', 92 'tablecontent': '\n', 93 'tablecontentcode': '</code>\n', 94 'tablecontentnote': '}}\n\n', 95 'tablecontenttip': '}}\n\n', 96 'tablecontentwarning': '}}\n\n', 97 'tablehead': '\n', 98 'tablenextnote': '}}\n\n', 99 'tablenextpara': '\n', 100 'tablenextparacode': '</code>\n', 101 'tablenexttip': '}}\n\n', 102 'tablenextwarning': '}}\n\n', 103 'tip': '}}\n\n', 104 'ul_item': '', 105 'variable': '', 106 'warning': '}}\n\n', 107 }, 108 'templ':{'bascode': False, 109 'code': False, 110 'codeintip': False, 111 'emph' : False, 112 'example': False, 113 'heading1': False, 114 'heading2': False, 115 'heading3': False, 116 'heading4': False, 117 'heading5': False, 118 'heading6': False, 119 'head1': False, 120 'head2': False, 121 'listitem': False, 122 'logocode': False, 123 'note': True, 124 'null': False, 125 'ol_item': False, 126 'paragraph': False, 127 'related': False, 128 'relatedtopics': False, 129 'sup' : False, 130 'tablecontent': False, 131 'tablecontentcode': False, 132 'tablecontentnote': True, 133 'tablecontenttip': True, 134 'tablecontentwarning': True, 135 'tablehead': False, 136 'tablenextnote': True, 137 'tablenextpara': False, 138 'tablenextparacode': False, 139 'tablenexttip': True, 140 'tablenextwarning': True, 141 'tip': True, 142 'ul_item': False, 143 'variable': False, 144 'warning': True, 145 } 146 } 147 148section_id_mapping = \ 149 {'relatedtopics': 'RelatedTopics'} 150 151# text snippets that we need to convert 152replace_text_list = \ 153 [["$[officename]", "{{ProductName}}"], 154 ["%PRODUCTNAME", "{{ProductName}}"], 155 ["$PRODUCTNAME", "{{ProductName}}"], 156 ["font size", u"\u200dfont size"], 157 ["''","<nowiki>''</nowiki>"] 158 ] 159 160def get_link_filename(link, name): 161 text = link.strip() 162 fragment = '' 163 if text.find('http') == 0: 164 text = name 165 else: 166 f = text.find('#') 167 if f >= 0: 168 fragment = text[f:] 169 text = text[0:f] 170 171 for title in titles: 172 try: 173 if title[0].find(text) >= 0: 174 return (title[1].strip(), fragment) 175 except: 176 pass 177 return (link, '') 178 179def replace_text(text): 180 for i in replace_text_list: 181 if text.find(i[0]) >= 0: 182 text = text.replace(i[0],i[1]) 183 return text 184 185# modify the text so that in templates like {{Name|something}}, the 'something' 186# does not look like template params 187def escape_equals_sign(text): 188 depth = 0 189 t = '' 190 for i in text: 191 if i == '=': 192 if depth == 0: 193 t = t + '=' 194 else: 195 t = t + '=' 196 else: 197 t = t + i 198 if i == '{' or i == '[' or i == '<': 199 depth = depth + 1 200 elif i == '}' or i == ']' or i == '>': 201 depth = depth - 1 202 if depth < 0: 203 depth = 0 204 205 return t 206 207def xopen(path, mode, encoding): 208 """Wrapper around open() to support both python2 and python3.""" 209 if sys.version_info >= (3,): 210 return open(path, mode, encoding=encoding) 211 else: 212 return open(path, mode) 213 214# used by escape_help_text 215helptagre = re.compile('''<[/]??[a-z_\-]+?(?:| +[a-zA-Z]+?=[\\\\]??".*?") *[/]??>''') 216 217def escape_help_text(text): 218 """Escapes the help text as it would be in an SDF file.""" 219 220 for tag in helptagre.findall(text): 221 escapethistag = False 222 for escape_tag in ["ahelp", "link", "item", "emph", "defaultinline", "switchinline", "caseinline", "variable", "bookmark_value", "image", "embedvar", "alt"]: 223 if tag.startswith("<%s" % escape_tag) or tag == "</%s>" % escape_tag: 224 escapethistag = True 225 if tag in ["<br/>", "<help-id-missing/>"]: 226 escapethistag = True 227 if escapethistag: 228 escaped_tag = ("\\<" + tag[1:-1] + "\\>") 229 text = text.replace(tag, escaped_tag) 230 return text 231 232 233def load_localization_data(po_root): 234 global localization_data 235 localization_data = {} 236 for root, dirs, files in os.walk(po_root): 237 for file in files: 238 if re.search(r'\.po$', file) == None: 239 continue 240 path = "%s/%s" % (root, file) 241 sock = xopen(path, "r", encoding='utf-8') 242 hashKey = None 243 transCollecting = False 244 trans = "" 245 it = iter(sock) 246 line = next(it, None) 247 while line != None: 248 line=line.decode("utf-8") 249 if line.startswith('msgctxt ""'): # constructing the hashKey 250 key=[] 251 allGood = True 252 i=0 253 while i<2 and allGood: 254 msgctxt_line = next(it, None); 255 if msgctxt_line != None and msgctxt_line.strip().startswith('"'): 256 key.append( msgctxt_line[1:-4] ) #-4 cuts \\n"\n from the end of the line 257 i=i+1 258 else: 259 allGood = False 260 if i==2: #hash key is allowed to be constructed 261 hashKey = '#'.join( (re.sub(r'^.*helpcontent2/source/', r'source/', path[:-3]) + '/' + key[0] , key[1]) ) 262 else: 263 hashKey = None 264 elif hashKey != None: # constructing trans value for hashKey 265 if transCollecting: 266 if line.startswith('"'): 267 trans= trans + line.strip()[1:-1] 268 else: 269 transCollecting = False 270 localization_data[hashKey] = escape_help_text(trans) 271 hashKey = None 272 elif line.startswith('msgstr '): 273 trans = line.strip()[8:-1] 274 if trans == '': # possibly multiline 275 transCollecting = True 276 else: 277 localization_data[hashKey] = escape_help_text(trans) 278 hashKey = None 279 line = next(it, None) 280 return True 281 282def unescape(str): 283 unescape_map = {'<': {True:'<', False:'<'}, 284 '>': {True:'>', False:'>'}, 285 '&': {True:'&', False:'&'}, 286 '"': {True:'"', False:'"'}} 287 result = '' 288 escape = False 289 for c in str: 290 if c == '\\': 291 if escape: 292 result = result + '\\' 293 escape = False 294 else: 295 escape = True 296 else: 297 try: 298 replace = unescape_map[c] 299 result = result + replace[escape] 300 except: 301 result = result + c 302 escape = False 303 304 return result 305 306def get_localized_text(filename, id): 307 try: 308 str = localization_data['%s#%s'% (filename, id)] 309 except: 310 return '' 311 312 return unescape(str) 313 314def href_to_fname_id(href): 315 link = href.replace('"', '') 316 fname = link 317 id = '' 318 if link.find("#") >= 0: 319 fname = link[:link.find("#")] 320 id = link[link.find("#")+1:] 321 else: 322 sys.stderr.write('Reference without a "#" in "%s".'% link) 323 324 return [fname, id] 325 326# Exception classes 327class UnhandledItemType(Exception): 328 pass 329# Base class for all the elements 330# 331# self.name - name of the element, to drop the self.child_parsing flag 332# self.objects - collects the child objects that are constructed during 333# parsing of the child elements 334# self.child_parsing - flag whether we are parsing a child, or the object 335# itself 336# self.parent - parent object 337class ElementBase: 338 def __init__(self, name, parent): 339 self.name = name 340 self.objects = [] 341 self.child_parsing = False 342 self.parent = parent 343 344 def start_element(self, parser, name, attrs): 345 pass 346 347 def end_element(self, parser, name): 348 if name == self.name: 349 self.parent.child_parsing = False 350 351 def char_data(self, parser, data): 352 pass 353 354 def get_curobj(self): 355 if self.child_parsing: 356 return self.objects[len(self.objects)-1].get_curobj() 357 return self 358 359 # start parsing a child element 360 def parse_child(self, child): 361 self.child_parsing = True 362 self.objects.append(child) 363 364 # construct the wiki representation of this object, including the objects 365 # held in self.objects (here only the text of the objects) 366 def get_all(self): 367 text = u'' 368 for i in self.objects: 369 text = text + i.get_all() 370 return text 371 372 # for handling variables, and embedding in general 373 # id - the variable name we want to get 374 def get_variable(self, id): 375 for i in self.objects: 376 if i != None: 377 var = i.get_variable(id) 378 if var != None: 379 return var 380 return None 381 382 # embed part of another file into current structure 383 def embed_href(self, parent_parser, fname, id): 384 # parse another xhp 385 parser = XhpParser('source/' + fname, False, \ 386 parent_parser.current_app, parent_parser.wiki_page_name, \ 387 parent_parser.lang) 388 var = parser.get_variable(id) 389 390 if var != None: 391 try: 392 if var.role == 'variable': 393 var.role = 'paragraph' 394 except: 395 pass 396 self.objects.append(var) 397 elif parser.follow_embed: 398 sys.stderr.write('Cannot find reference "#%s" in "%s".\n'% \ 399 (id, fname)) 400 401 def unhandled_element(self, parser, name): 402 sys.stderr.write('Warning: Unhandled element "%s" in "%s" (%s)\n'% \ 403 (name, self.name, parser.filename)) 404 405# Base class for trivial elements that operate on char_data 406# 407# Like <comment>, or <title> 408class TextElementBase(ElementBase): 409 def __init__(self, attrs, parent, element_name, start, end, templ): 410 ElementBase.__init__(self, element_name, parent) 411 self.text = u'' 412 self.start = start 413 self.end = end 414 self.templ = templ 415 416 def char_data(self, parser, data): 417 self.text = self.text + data 418 419 def get_all(self): 420 if self.templ: 421 return self.start + escape_equals_sign(replace_text(self.text)) + self.end 422 else: 423 return self.start + replace_text(self.text) + self.end 424 425class XhpFile(ElementBase): 426 def __init__(self): 427 ElementBase.__init__(self, None, None) 428 429 def start_element(self, parser, name, attrs): 430 if name == 'body': 431 # ignored, we flatten the structure 432 pass 433 elif name == 'bookmark': 434 self.parse_child(Bookmark(attrs, self, 'div', parser)) 435 elif name == 'comment': 436 self.parse_child(Comment(attrs, self)) 437 elif name == 'embed' or name == 'embedvar': 438 if parser.follow_embed: 439 (fname, id) = href_to_fname_id(attrs['href']) 440 self.embed_href(parser, fname, id) 441 elif name == 'helpdocument': 442 # ignored, we flatten the structure 443 pass 444 elif name == 'list': 445 self.parse_child(List(attrs, self, False)) 446 elif name == 'meta': 447 self.parse_child(Meta(attrs, self)) 448 elif name == 'paragraph': 449 parser.parse_paragraph(attrs, self) 450 elif name == 'section': 451 self.parse_child(Section(attrs, self)) 452 elif name == 'sort': 453 self.parse_child(Sort(attrs, self)) 454 elif name == 'switch': 455 self.parse_child(Switch(attrs, self, parser.embedding_app)) 456 elif name == 'table': 457 self.parse_child(Table(attrs, self)) 458 elif name == 'bascode': 459 self.parse_child(BasicCode(attrs, self)) 460 else: 461 self.unhandled_element(parser, name) 462 463class Bookmark(ElementBase): 464 def __init__(self, attrs, parent, type, parser): 465 ElementBase.__init__(self, 'bookmark', parent) 466 467 self.type = type 468 469 self.id = attrs['id'] 470 self.app = '' 471 self.redirect = '' 472 self.target = '' 473 self.authoritative = False 474 475 # let's construct the name of the redirect, so that we can point 476 # to the wikihelp directly from the LO code; wiki then takes care of 477 # the correct redirect 478 branch = attrs['branch'] 479 if branch.find('hid/') == 0 and (parser.current_app_raw != '' or parser.follow_embed): 480 name = branch[branch.find('/') + 1:] 481 482 self.app = parser.current_app_raw 483 self.target = parser.wiki_page_name 484 self.authoritative = parser.follow_embed 485 self.redirect = name.replace("/", "%2F") 486 487 def get_all(self): 488 global redirects 489 # first of all, we need to create a redirect page for this one 490 if self.redirect != '' and self.target != '': 491 redirects.append([self.app, self.redirect, \ 492 '%s#%s'% (self.target, self.id), \ 493 self.authoritative]) 494 495 # then we also have to setup ID inside the page 496 if self.type == 'div': 497 return '<div id="%s"></div>\n'% self.id 498 elif self.type == 'span': 499 return '<span id="%s"></span>'% self.id 500 else: 501 sys.stderr.write('Unknown bookmark type "%s"'% self.type) 502 503 return '' 504 505class Image(ElementBase): 506 def __init__(self, attrs, parent): 507 ElementBase.__init__(self, 'image', parent) 508 self.src = attrs['src'] 509 self.align = 'left' 510 self.alt = False 511 self.alttext = "" 512 513 def start_element(self, parser, name, attrs): 514 if name == 'alt': 515 self.alt = True 516 else: 517 self.unhandled_element(parser, name) 518 519 def end_element(self, parser, name): 520 ElementBase.end_element(self, parser, name) 521 522 if name == 'alt': 523 self.alt = False 524 525 def char_data(self, parser, data): 526 if self.alt: 527 self.alttext = self.alttext + data 528 529 def get_all(self): 530 global images 531 images.add(self.src) 532 533 name = self.src[self.src.rfind('/') + 1:] 534 wikitext = "[[Image:"+name+"|border|"+self.align+"|" 535 wikitext = wikitext + self.alttext+"]]" 536 return wikitext 537 538 def get_curobj(self): 539 return self 540 541class Br(TextElementBase): 542 def __init__(self, attrs, parent): 543 TextElementBase.__init__(self, attrs, parent, 'br', '<br/>', '', False) 544 545class Comment(TextElementBase): 546 def __init__(self, attrs, parent): 547 TextElementBase.__init__(self, attrs, parent, 'comment', '<!-- ', ' -->', False) 548 549class HelpIdMissing(TextElementBase): 550 def __init__(self, attrs, parent): 551 TextElementBase.__init__(self, attrs, parent, 'help-id-missing', '{{MissingHelpId}}', '', False) 552 553class Text: 554 def __init__(self, text): 555 self.wikitext = replace_text(text) 556 557 def get_all(self): 558 return self.wikitext 559 560 def get_variable(self, id): 561 return None 562 563class TableCell(ElementBase): 564 def __init__(self, attrs, parent): 565 ElementBase.__init__(self, 'tablecell', parent) 566 self.cellHasChildElement = False 567 568 def start_element(self, parser, name, attrs): 569 self.cellHasChildElement = True 570 if name == 'bookmark': 571 self.parse_child(Bookmark(attrs, self, 'div', parser)) 572 elif name == 'comment': 573 self.parse_child(Comment(attrs, self)) 574 elif name == 'embed' or name == 'embedvar': 575 (fname, id) = href_to_fname_id(attrs['href']) 576 if parser.follow_embed: 577 self.embed_href(parser, fname, id) 578 elif name == 'paragraph': 579 parser.parse_localized_paragraph(TableContentParagraph, attrs, self) 580 elif name == 'section': 581 self.parse_child(Section(attrs, self)) 582 elif name == 'bascode': 583 # ignored, do not syntax highlight in table cells 584 pass 585 elif name == 'list': 586 self.parse_child(List(attrs, self, True)) 587 else: 588 self.unhandled_element(parser, name) 589 590 def get_all(self): 591 text = '' 592 if not self.cellHasChildElement: # an empty element 593 if self.parent.isTableHeader: # get from TableRow Element 594 role = 'tablehead' 595 else: 596 role = 'tablecontent' 597 text = text + replace_paragraph_role['start'][role] 598 text = text + replace_paragraph_role['end'][role] 599 text = text + ElementBase.get_all(self) 600 return text 601 602class TableRow(ElementBase): 603 def __init__(self, attrs, parent): 604 ElementBase.__init__(self, 'tablerow', parent) 605 606 def start_element(self, parser, name, attrs): 607 self.isTableHeader = False 608 if name == 'tablecell': 609 self.parse_child(TableCell(attrs, self)) 610 else: 611 self.unhandled_element(parser, name) 612 613 def get_all(self): 614 text = '|-\n' + ElementBase.get_all(self) 615 return text 616 617class BasicCode(ElementBase): 618 def __init__(self, attrs, parent): 619 ElementBase.__init__(self, 'bascode', parent) 620 621 def start_element(self, parser, name, attrs): 622 if name == 'paragraph': 623 parser.parse_localized_paragraph(BasicCodeParagraph, attrs, self) 624 else: 625 self.unhandled_element(parser, name) 626 627 def get_all(self): 628 text = '<source lang="oobas">\n' + ElementBase.get_all(self) + '</source>\n\n' 629 return text 630 631class Table(ElementBase): 632 def __init__(self, attrs, parent): 633 ElementBase.__init__(self, 'table', parent) 634 635 def start_element(self, parser, name, attrs): 636 if name == 'comment': 637 self.parse_child(Comment(attrs, self)) 638 elif name == 'tablerow': 639 self.parse_child(TableRow(attrs, self)) 640 else: 641 self.unhandled_element(parser, name) 642 643 def get_all(self): 644 # + ' align="left"' etc.? 645 text = '{| class="wikitable"\n' + \ 646 ElementBase.get_all(self) + \ 647 '|}\n\n' 648 return text 649 650class ListItem(ElementBase): 651 def __init__(self, attrs, parent): 652 ElementBase.__init__(self, 'listitem', parent) 653 654 def start_element(self, parser, name, attrs): 655 if name == 'bookmark': 656 self.parse_child(Bookmark(attrs, self, 'span', parser)) 657 elif name == 'embed' or name == 'embedvar': 658 (fname, id) = href_to_fname_id(attrs['href']) 659 if parser.follow_embed: 660 self.embed_href(parser, fname, id) 661 elif name == 'paragraph': 662 parser.parse_localized_paragraph(ListItemParagraph, attrs, self) 663 elif name == 'list': 664 self.parse_child(List(attrs, self, False)) 665 else: 666 self.unhandled_element(parser, name) 667 668 def get_all(self): 669 text = '*' 670 postfix = '\n' 671 if self.parent.startwith > 0: 672 text = '<li>' 673 postfix = '</li>' 674 elif self.parent.type == 'ordered': 675 text = '#' 676 677 # add the text itself 678 linebreak = False 679 for i in self.objects: 680 if linebreak: 681 text = text + '<br/>' 682 ti = i.get_all() 683 # when the object is another list (i.e. nested lists), only the first item 684 # gets the '#' sign in the front by the previous statement 685 # the below re.sub inserts the extra '#' for all additional items of the list 686 ti = re.sub(r'\n\s*#', '\n##', ti) 687 text = text + ti 688 linebreak = True 689 690 return text + postfix 691 692class List(ElementBase): 693 def __init__(self, attrs, parent, isInTable): 694 ElementBase.__init__(self, 'list', parent) 695 696 self.isInTable = isInTable 697 self.type = attrs['type'] 698 try: 699 self.startwith = int(attrs['startwith']) 700 except: 701 self.startwith = 0 702 703 def start_element(self, parser, name, attrs): 704 if name == 'listitem': 705 self.parse_child(ListItem(attrs, self)) 706 else: 707 self.unhandled_element(parser, name) 708 709 def get_all(self): 710 text = "" 711 if self.isInTable: 712 text = '| |\n' 713 if self.startwith > 0: 714 text = text + '<ol start="%d">\n'% self.startwith 715 716 text = text + ElementBase.get_all(self) 717 718 if self.startwith > 0: 719 text = text + '\n</ol>\n' 720 else: 721 text = text + '\n' 722 return text 723 724# To handle elements that should be completely ignored 725class Ignore(ElementBase): 726 def __init__(self, attrs, parent, element_name): 727 ElementBase.__init__(self, element_name, parent) 728 729class OrigTitle(TextElementBase): 730 def __init__(self, attrs, parent): 731 TextElementBase.__init__(self, attrs, parent, 'title', '{{OrigLang|', '}}\n', True) 732 733class Title(TextElementBase): 734 def __init__(self, attrs, parent, localized_title): 735 TextElementBase.__init__(self, attrs, parent, 'title', '{{Lang|', '}}\n', True) 736 self.localized_title = localized_title 737 738 def get_all(self): 739 if self.localized_title != '': 740 self.text = self.localized_title 741 return TextElementBase.get_all(self) 742 743class Topic(ElementBase): 744 def __init__(self, attrs, parent): 745 ElementBase.__init__(self, 'topic', parent) 746 747 def start_element(self, parser, name, attrs): 748 if name == 'title': 749 if parser.lang == '': 750 self.parse_child(OrigTitle(attrs, self)) 751 else: 752 self.parse_child(Title(attrs, self, get_localized_text(parser.filename, 'tit'))) 753 elif name == 'filename': 754 self.parse_child(Ignore(attrs, self, name)) 755 else: 756 self.unhandled_element(parser, name) 757 758class Meta(ElementBase): 759 def __init__(self, attrs, parent): 760 ElementBase.__init__(self, 'meta', parent) 761 762 def start_element(self, parser, name, attrs): 763 if name == 'topic': 764 self.parse_child(Topic(attrs, self)) 765 elif name == 'history': 766 self.parse_child(Ignore(attrs, self, name)) 767 else: 768 self.unhandled_element(parser, name) 769 770class Section(ElementBase): 771 def __init__(self, attrs, parent): 772 ElementBase.__init__(self, 'section', parent) 773 self.id = attrs[ 'id' ] 774 775 def start_element(self, parser, name, attrs): 776 if name == 'bookmark': 777 self.parse_child(Bookmark(attrs, self, 'div', parser)) 778 elif name == 'comment': 779 self.parse_child(Comment(attrs, self)) 780 elif name == 'embed' or name == 'embedvar': 781 (fname, id) = href_to_fname_id(attrs['href']) 782 if parser.follow_embed: 783 self.embed_href(parser, fname, id) 784 elif name == 'list': 785 self.parse_child(List(attrs, self, False)) 786 elif name == 'paragraph': 787 parser.parse_paragraph(attrs, self) 788 elif name == 'section': 789 # sections can be nested 790 self.parse_child(Section(attrs, self)) 791 elif name == 'switch': 792 self.parse_child(Switch(attrs, self, parser.embedding_app)) 793 elif name == 'table': 794 self.parse_child(Table(attrs, self)) 795 elif name == 'bascode': 796 self.parse_child(BasicCode(attrs, self)) 797 else: 798 self.unhandled_element(parser, name) 799 800 def get_all(self): 801 mapping = '' 802 try: 803 mapping = section_id_mapping[self.id] 804 except: 805 pass 806 807 # some of the section ids are used as real id's, some of them have 808 # function (like relatetopics), and have to be templatized 809 text = '' 810 if mapping != '': 811 text = '{{%s|%s}}\n\n'% (mapping, \ 812 escape_equals_sign(ElementBase.get_all(self))) 813 else: 814 text = ElementBase.get_all(self) 815 816 return text 817 818 def get_variable(self, id): 819 var = ElementBase.get_variable(self, id) 820 if var != None: 821 return var 822 if id == self.id: 823 return self 824 return None 825 826class Sort(ElementBase): 827 def __init__(self, attrs, parent): 828 ElementBase.__init__(self, 'sort', parent) 829 830 try: 831 self.order = attrs['order'] 832 except: 833 self.order = 'asc' 834 835 def start_element(self, parser, name, attrs): 836 if name == 'section': 837 self.parse_child(Section(attrs, self)) 838 else: 839 self.unhandled_element(parser, name) 840 841 def get_all(self): 842 rev = False 843 if self.order == 'asc': 844 rev = True 845 self.objects = sorted(self.objects, key=lambda obj: obj.id, reverse=rev) 846 847 return ElementBase.get_all(self) 848 849class Link(ElementBase): 850 def __init__(self, attrs, parent, lang): 851 ElementBase.__init__(self, 'link', parent) 852 853 self.link = attrs['href'] 854 try: 855 self.lname = attrs['name'] 856 except: 857 self.lname = self.link[self.link.rfind("/")+1:] 858 # Override lname 859 self.default_name = self.lname 860 (self.lname, self.fragment) = get_link_filename(self.link, self.lname) 861 self.wikitext = "" 862 self.lang = lang 863 864 def char_data(self, parser, data): 865 self.wikitext = self.wikitext + data 866 867 def get_all(self): 868 if self.wikitext == "": 869 self.wikitext = self.default_name 870 871 self.wikitext = replace_text(self.wikitext) 872 if self.link.find("http") == 0: 873 text = '[%s %s]'% (self.link, self.wikitext) 874 elif self.lang != '': 875 text = '[[%s/%s%s|%s]]'% (self.lname, self.lang, self.fragment, self.wikitext) 876 else: 877 text = '[[%s%s|%s]]'% (self.lname, self.fragment, self.wikitext) 878 return text 879 880class SwitchInline(ElementBase): 881 def __init__(self, attrs, parent, app): 882 ElementBase.__init__(self, 'switchinline', parent) 883 self.switch = attrs['select'] 884 self.embedding_app = app 885 886 def start_element(self, parser, name, attrs): 887 if name == 'caseinline': 888 self.parse_child(CaseInline(attrs, self, False)) 889 elif name == 'defaultinline': 890 self.parse_child(CaseInline(attrs, self, True)) 891 else: 892 self.unhandled_element(parser, name) 893 894 def get_all(self): 895 if len(self.objects) == 0: 896 return '' 897 elif self.switch == 'sys': 898 system = {'MAC':'', 'UNIX':'', 'WIN':'', 'default':''} 899 for i in self.objects: 900 if i.case == 'MAC' or i.case == 'UNIX' or \ 901 i.case == 'WIN' or i.case == 'default': 902 system[i.case] = i.get_all() 903 elif i.case == 'OS2': 904 # ignore, there is only one mention of OS2, which is a 905 # 'note to translators', and no meat 906 pass 907 elif i.case == 'HIDE_HERE': 908 # do what the name suggest ;-) 909 pass 910 else: 911 sys.stderr.write('Unhandled "%s" case in "sys" switchinline.\n'% \ 912 i.case ) 913 text = '{{System' 914 for i in [['default', 'default'], ['MAC', 'mac'], \ 915 ['UNIX', 'unx'], ['WIN', 'win']]: 916 if system[i[0]] != '': 917 text = '%s|%s=%s'% (text, i[1], system[i[0]]) 918 return text + '}}' 919 elif self.switch == 'appl': 920 # we want directly use the right text, when inlining something 921 # 'shared' into an 'app' 922 if self.embedding_app == '': 923 text = '' 924 default = '' 925 for i in self.objects: 926 appls = {'BASIC':'Basic', 'CALC':'Calc', \ 927 'CHART':'Chart', 'DRAW':'Draw', \ 928 'IMAGE':'Draw', 'IMPRESS': 'Impress', \ 929 'MATH':'Math', 'WRITER':'Writer', \ 930 'OFFICE':'', 'default':''} 931 try: 932 app = appls[i.case] 933 all = i.get_all() 934 if all == '': 935 pass 936 elif app == '': 937 default = all 938 else: 939 text = text + '{{WhenIn%s|%s}}'% (app, escape_equals_sign(all)) 940 except: 941 sys.stderr.write('Unhandled "%s" case in "appl" switchinline.\n'% \ 942 i.case) 943 944 if text == '': 945 text = default 946 elif default != '': 947 text = text + '{{WhenDefault|%s}}'% escape_equals_sign(default) 948 949 return text 950 else: 951 for i in self.objects: 952 if i.case == self.embedding_app: 953 return i.get_all() 954 955 return '' 956 957class Case(ElementBase): 958 def __init__(self, attrs, parent, is_default): 959 ElementBase.__init__(self, 'case', parent) 960 961 if is_default: 962 self.name = 'default' 963 self.case = 'default' 964 else: 965 self.case = attrs['select'] 966 967 def start_element(self, parser, name, attrs): 968 if name == 'bookmark': 969 self.parse_child(Bookmark(attrs, self, 'div', parser)) 970 elif name == 'comment': 971 self.parse_child(Comment(attrs, self)) 972 elif name == 'embed' or name == 'embedvar': 973 if parser.follow_embed: 974 (fname, id) = href_to_fname_id(attrs['href']) 975 self.embed_href(parser, fname, id) 976 elif name == 'list': 977 self.parse_child(List(attrs, self, False)) 978 elif name == 'paragraph': 979 parser.parse_paragraph(attrs, self) 980 elif name == 'section': 981 self.parse_child(Section(attrs, self)) 982 elif name == 'table': 983 self.parse_child(Table(attrs, self)) 984 elif name == 'bascode': 985 self.parse_child(BasicCode(attrs, self)) 986 else: 987 self.unhandled_element(parser, name) 988 989class Switch(SwitchInline): 990 def __init__(self, attrs, parent, app): 991 SwitchInline.__init__(self, attrs, parent, app) 992 self.name = 'switch' 993 994 def start_element(self, parser, name, attrs): 995 self.embedding_app = parser.embedding_app 996 if name == 'case': 997 self.parse_child(Case(attrs, self, False)) 998 elif name == 'default': 999 self.parse_child(Case(attrs, self, True)) 1000 else: 1001 self.unhandled_element(parser, name) 1002 1003class Item(ElementBase): 1004 replace_type = \ 1005 {'start':{'acronym' : '\'\'', 1006 'code': '<code>', 1007 'input': '<code>', 1008 'keycode': '{{KeyCode|', 1009 'tasto': '{{KeyCode|', 1010 'litera': '<code>', 1011 'literal': '<code>', 1012 'menuitem': '{{MenuItem|', 1013 'mwnuitem': '{{MenuItem|', 1014 'OpenOffice.org': '', 1015 'productname': '', 1016 'unknown': '<code>' 1017 }, 1018 'end':{'acronym' : '\'\'', 1019 'code': '</code>', 1020 'input': '</code>', 1021 'keycode': '}}', 1022 'tasto': '}}', 1023 'litera': '</code>', 1024 'literal': '</code>', 1025 'menuitem': '}}', 1026 'mwnuitem': '}}', 1027 'OpenOffice.org': '', 1028 'productname': '', 1029 'unknown': '</code>' 1030 }, 1031 'templ':{'acronym': False, 1032 'code': False, 1033 'input': False, 1034 'keycode': True, 1035 'tasto': True, 1036 'litera': False, 1037 'literal': False, 1038 'menuitem': True, 1039 'mwnuitem': True, 1040 'OpenOffice.org': False, 1041 'productname': False, 1042 'unknown': False 1043 }} 1044 1045 def __init__(self, attrs, parent): 1046 ElementBase.__init__(self, 'item', parent) 1047 1048 try: 1049 self.type = attrs['type'] 1050 except: 1051 self.type = 'unknown' 1052 self.text = '' 1053 1054 def char_data(self, parser, data): 1055 self.text = self.text + data 1056 1057 def get_all(self): 1058 try: 1059 text = '' 1060 if self.replace_type['templ'][self.type]: 1061 text = escape_equals_sign(replace_text(self.text)) 1062 else: 1063 text = replace_text(self.text) 1064 return self.replace_type['start'][self.type] + \ 1065 text + \ 1066 self.replace_type['end'][self.type] 1067 except: 1068 try: 1069 sys.stderr.write('Unhandled item type "%s".\n'% self.type) 1070 except: 1071 sys.stderr.write('Unhandled item type. Possibly type has been localized.\n') 1072 finally: 1073 raise UnhandledItemType 1074 1075class Paragraph(ElementBase): 1076 def __init__(self, attrs, parent): 1077 ElementBase.__init__(self, 'paragraph', parent) 1078 1079 try: 1080 self.role = attrs['role'] 1081 except: 1082 self.role = 'paragraph' 1083 1084 try: 1085 self.id = attrs['id'] 1086 except: 1087 self.id = "" 1088 1089 try: 1090 self.level = int(attrs['level']) 1091 except: 1092 self.level = 0 1093 1094 self.is_first = (len(self.parent.objects) == 0) 1095 1096 def start_element(self, parser, name, attrs): 1097 if name == 'ahelp': 1098 try: 1099 if attrs['visibility'] == 'hidden': 1100 self.parse_child(Ignore(attrs, self, name)) 1101 except: 1102 pass 1103 elif name == 'br': 1104 self.parse_child(Br(attrs, self)) 1105 elif name == 'comment': 1106 self.parse_child(Comment(attrs, self)) 1107 elif name == 'emph': 1108 self.parse_child(Emph(attrs, self)) 1109 elif name == 'sup': 1110 self.parse_child(Sup(attrs, self)) 1111 elif name == 'embedvar': 1112 if parser.follow_embed: 1113 (fname, id) = href_to_fname_id(attrs['href']) 1114 self.embed_href(parser, fname, id) 1115 elif name == 'help-id-missing': 1116 self.parse_child(HelpIdMissing(attrs, self)) 1117 elif name == 'image': 1118 self.parse_child(Image(attrs, self)) 1119 elif name == 'item': 1120 self.parse_child(Item(attrs, self)) 1121 elif name == 'link': 1122 self.parse_child(Link(attrs, self, parser.lang)) 1123 elif name == 'localized': 1124 # we ignore this tag, it is added arbitrary for the paragraphs 1125 # that come from .sdf files 1126 pass 1127 elif name == 'switchinline': 1128 self.parse_child(SwitchInline(attrs, self, parser.embedding_app)) 1129 elif name == 'variable': 1130 self.parse_child(Variable(attrs, self)) 1131 else: 1132 self.unhandled_element(parser, name) 1133 1134 def char_data(self, parser, data): 1135 if self.role == 'paragraph' or self.role == 'heading' or \ 1136 self.role == 'listitem' or self.role == 'variable': 1137 if data != '' and data[0] == ' ': 1138 data = ' ' + data.lstrip() 1139 data = data.replace('\n', ' ') 1140 1141 if len(data): 1142 self.objects.append(Text(data)) 1143 1144 def get_all(self): 1145 role = self.role 1146 if role == 'heading': 1147 if self.level <= 0: 1148 sys.stderr.write('Heading, but the level is %d.\n'% self.level) 1149 elif self.level < 6: 1150 role = 'heading%d'% self.level 1151 else: 1152 role = 'heading6' 1153 1154 # if we are not the first para in the table, we need special handling 1155 if not self.is_first and role.find('table') == 0: 1156 if role == 'tablecontentcode': 1157 role = 'tablenextparacode' 1158 elif role == 'tablecontentnote': 1159 role = 'tablenextnote' 1160 elif role == 'tablecontenttip': 1161 role = 'tablenexttip' 1162 elif role == 'tablecontentwarning': 1163 role = 'tablenextwarning' 1164 else: 1165 role = 'tablenextpara' 1166 1167 # the text itself 1168 try: 1169 children = ElementBase.get_all(self) 1170 except UnhandledItemType: 1171 raise UnhandledItemType('Paragraph id: '+str(self.id)) 1172 if self.role != 'emph' and self.role != 'bascode' and self.role != 'logocode': 1173 children = children.strip() 1174 1175 if len(children) == 0: 1176 return '' 1177 1178 # prepend the markup according to the role 1179 text = '' 1180 try: 1181 text = text + replace_paragraph_role['start'][role] 1182 except: 1183 sys.stderr.write( "Unknown paragraph role start: " + role + "\n" ) 1184 1185 if replace_paragraph_role['templ'][role]: 1186 text = text + escape_equals_sign(children) 1187 else: 1188 text = text + children 1189 1190 # append the markup according to the role 1191 try: 1192 text = text + replace_paragraph_role['end'][role] 1193 except: 1194 sys.stderr.write( "Unknown paragraph role end: " + role + "\n" ) 1195 1196 return text 1197 1198class Variable(Paragraph): 1199 def __init__(self, attrs, parent): 1200 Paragraph.__init__(self, attrs, parent) 1201 self.name = 'variable' 1202 self.role = 'variable' 1203 self.id = attrs['id'] 1204 1205 def get_variable(self, id): 1206 if id == self.id: 1207 return self 1208 return None 1209 1210class CaseInline(Paragraph): 1211 def __init__(self, attrs, parent, is_default): 1212 Paragraph.__init__(self, attrs, parent) 1213 1214 self.role = 'null' 1215 if is_default: 1216 self.name = 'defaultinline' 1217 self.case = 'default' 1218 else: 1219 self.name = 'caseinline' 1220 self.case = attrs['select'] 1221 1222class Emph(Paragraph): 1223 def __init__(self, attrs, parent): 1224 Paragraph.__init__(self, attrs, parent) 1225 self.name = 'emph' 1226 self.role = 'emph' 1227 1228 def get_all(self): 1229 text = Paragraph.get_all(self) 1230 if len(text): 1231 return "'''" + text + "'''" 1232 return '' 1233 1234class Sup(Paragraph): 1235 def __init__(self, attrs, parent): 1236 Paragraph.__init__(self, attrs, parent) 1237 self.name = 'sup' 1238 self.role = 'sup' 1239 1240 def get_all(self): 1241 text = Paragraph.get_all(self) 1242 if len(text): 1243 return "<sup>" + text + "</sup>" 1244 return '' 1245 1246class ListItemParagraph(Paragraph): 1247 def __init__(self, attrs, parent): 1248 Paragraph.__init__(self, attrs, parent) 1249 self.role = 'listitem' 1250 1251class BasicCodeParagraph(Paragraph): 1252 def __init__(self, attrs, parent): 1253 Paragraph.__init__(self, attrs, parent) 1254 self.role = 'bascode' 1255 1256class TableContentParagraph(Paragraph): 1257 def __init__(self, attrs, parent): 1258 Paragraph.__init__(self, attrs, parent) 1259 if self.role != 'tablehead' and self.role != 'tablecontent': 1260 if self.role == 'code': 1261 self.role = 'tablecontentcode' 1262 elif self.role == 'bascode': 1263 self.role = 'tablecontentcode' 1264 elif self.role == 'logocode': 1265 self.role = 'tablecontentcode' 1266 elif self.role == 'note': 1267 self.role = 'tablecontentnote' 1268 elif self.role == 'tip': 1269 self.role = 'tablecontenttip' 1270 elif self.role == 'warning': 1271 self.role = 'tablecontentwarning' 1272 else: 1273 self.role = 'tablecontent' 1274 if self.role == 'tablehead': 1275 self.parent.parent.isTableHeader = True # self.parent.parent is TableRow Element 1276 else: 1277 self.parent.parent.isTableHeader = False 1278 1279class ParserBase: 1280 def __init__(self, filename, follow_embed, embedding_app, current_app, wiki_page_name, lang, head_object, buffer): 1281 self.filename = filename 1282 self.follow_embed = follow_embed 1283 self.embedding_app = embedding_app 1284 self.current_app = current_app 1285 self.wiki_page_name = wiki_page_name 1286 self.lang = lang 1287 self.head_obj = head_object 1288 1289 p = xml.parsers.expat.ParserCreate() 1290 p.StartElementHandler = self.start_element 1291 p.EndElementHandler = self.end_element 1292 p.CharacterDataHandler = self.char_data 1293 1294 p.Parse(buffer) 1295 1296 def start_element(self, name, attrs): 1297 self.head_obj.get_curobj().start_element(self, name, attrs) 1298 1299 def end_element(self, name): 1300 self.head_obj.get_curobj().end_element(self, name) 1301 1302 def char_data(self, data): 1303 self.head_obj.get_curobj().char_data(self, data) 1304 1305 def get_all(self): 1306 return self.head_obj.get_all() 1307 1308 def get_variable(self, id): 1309 return self.head_obj.get_variable(id) 1310 1311 def parse_localized_paragraph(self, Paragraph_type, attrs, obj): 1312 localized_text = '' 1313 try: 1314 localized_text = get_localized_text(self.filename, attrs['id']) 1315 except: 1316 pass 1317 1318 paragraph = Paragraph_type(attrs, obj) 1319 if localized_text != '': 1320 # parse the localized text 1321 text = u'<?xml version="1.0" encoding="UTF-8"?><localized>' + localized_text + '</localized>' 1322 try: 1323 ParserBase(self.filename, self.follow_embed, self.embedding_app, \ 1324 self.current_app, self.wiki_page_name, self.lang, \ 1325 paragraph, text.encode('utf-8')) 1326 except xml.parsers.expat.ExpatError: 1327 sys.stderr.write( 'Invalid XML in translated text. Using the original text. Error location:\n'\ 1328 + 'Current xhp: ' + self.filename + '\nParagraph id: ' + attrs['id'] + '\n') 1329 obj.parse_child(Paragraph_type(attrs, obj)) # new paragraph must be created because "paragraph" is corrupted by "ParserBase" 1330 else: 1331 # add it to the overall structure 1332 obj.objects.append(paragraph) 1333 # and ignore the original text 1334 obj.parse_child(Ignore(attrs, obj, 'paragraph')) 1335 else: 1336 obj.parse_child(paragraph) 1337 1338 def parse_paragraph(self, attrs, obj): 1339 ignore_this = False 1340 try: 1341 if attrs['role'] == 'heading' and int(attrs['level']) == 1 \ 1342 and self.ignore_heading and self.follow_embed: 1343 self.ignore_heading = False 1344 ignore_this = True 1345 except: 1346 pass 1347 1348 if ignore_this: 1349 obj.parse_child(Ignore(attrs, obj, 'paragraph')) 1350 else: 1351 self.parse_localized_paragraph(Paragraph, attrs, obj) 1352 1353class XhpParser(ParserBase): 1354 def __init__(self, filename, follow_embed, embedding_app, wiki_page_name, lang): 1355 # we want to ignore the 1st level="1" heading, because in most of the 1356 # cases, it is the only level="1" heading in the file, and it is the 1357 # same as the page title 1358 self.ignore_heading = True 1359 1360 current_app = '' 1361 self.current_app_raw = '' 1362 for i in [['sbasic', 'BASIC'], ['scalc', 'CALC'], \ 1363 ['sdatabase', 'DATABASE'], ['sdraw', 'DRAW'], \ 1364 ['schart', 'CHART'], ['simpress', 'IMPRESS'], \ 1365 ['smath', 'MATH'], ['swriter', 'WRITER']]: 1366 if filename.find('/%s/'% i[0]) >= 0: 1367 self.current_app_raw = i[0] 1368 current_app = i[1] 1369 break 1370 1371 if embedding_app == '': 1372 embedding_app = current_app 1373 1374 file = codecs.open(filename, "r", "utf-8") 1375 buf = file.read() 1376 file.close() 1377 1378 ParserBase.__init__(self, filename, follow_embed, embedding_app, 1379 current_app, wiki_page_name, lang, XhpFile(), buf.encode('utf-8')) 1380 1381class WikiConverter(Thread): 1382 def __init__(self, inputfile, wiki_page_name, lang, outputfile): 1383 Thread.__init__(self) 1384 self.inputfile = inputfile 1385 self.wiki_page_name = wiki_page_name 1386 self.lang = lang 1387 self.outputfile = outputfile 1388 1389 def run(self): 1390 parser = XhpParser(self.inputfile, True, '', self.wiki_page_name, self.lang) 1391 file = codecs.open(self.outputfile, "wb", "utf-8") 1392 file.write(parser.get_all()) 1393 file.close() 1394 1395def write_link(r, target): 1396 fname = 'wiki/%s'% r 1397 try: 1398 file = open(fname, "w") 1399 file.write('#REDIRECT [[%s]]\n'% target) 1400 file.close() 1401 except: 1402 sys.stderr.write('Unable to write "%s".\n'%'wiki/%s'% fname) 1403 1404def write_redirects(): 1405 print 'Generating the redirects...' 1406 written = {} 1407 # in the first pass, immediately write the links that are embedded, so that 1408 # we can always point to that source versions 1409 for redir in redirects: 1410 app = redir[0] 1411 redirect = redir[1] 1412 target = redir[2] 1413 authoritative = redir[3] 1414 1415 if app != '': 1416 r = '%s/%s'% (app, redirect) 1417 if authoritative: 1418 write_link(r, target) 1419 written[r] = True 1420 else: 1421 try: 1422 written[r] 1423 except: 1424 written[r] = False 1425 1426 # in the second pass, output the wiki links 1427 for redir in redirects: 1428 app = redir[0] 1429 redirect = redir[1] 1430 target = redir[2] 1431 1432 if app == '': 1433 for i in ['swriter', 'scalc', 'simpress', 'sdraw', 'smath', \ 1434 'schart', 'sbasic', 'sdatabase']: 1435 write_link('%s/%s'% (i, redirect), target) 1436 else: 1437 r = '%s/%s'% (app, redirect) 1438 if not written[r]: 1439 write_link(r, target) 1440 1441# Main Function 1442def convert(title_data, generate_redirects, lang, po_root): 1443 if lang == '': 1444 print 'Generating the main wiki pages...' 1445 else: 1446 print 'Generating the wiki pages for language %s...'% lang 1447 1448 global titles 1449 titles = [t for t in title_data] 1450 global redirects 1451 redirects = [] 1452 global images 1453 images = set() 1454 1455 if lang != '': 1456 sys.stderr.write('Using localizations from "%s"\n'% po_root) 1457 if not load_localization_data(po_root): 1458 return 1459 1460 for title in titles: 1461 while threading.active_count() > max_threads: 1462 time.sleep(0.001) 1463 1464 infile = title[0].strip() 1465 wikiname = title[1].strip() 1466 articledir = 'wiki/' + wikiname 1467 try: 1468 os.mkdir(articledir) 1469 except: 1470 pass 1471 1472 outfile = '' 1473 if lang != '': 1474 wikiname = '%s/%s'% (wikiname, lang) 1475 outfile = '%s/%s'% (articledir, lang) 1476 else: 1477 outfile = '%s/MAIN'% articledir 1478 1479 try: 1480 file = open(outfile, 'r') 1481 except: 1482 try: 1483 wiki = WikiConverter(infile, wikiname, lang, outfile) 1484 wiki.start() 1485 continue 1486 except: 1487 print 'Failed to convert "%s" into "%s".\n'% \ 1488 (infile, outfile) 1489 sys.stderr.write('Warning: Skipping: %s > %s\n'% (infile, outfile)) 1490 file.close() 1491 1492 # wait for everyone to finish 1493 while threading.active_count() > 1: 1494 time.sleep(0.001) 1495 1496 if lang == '': 1497 # set of the images used here 1498 print 'Generating "images.txt", the list of used images...' 1499 file = open('images.txt', "w") 1500 for image in images: 1501 file.write('%s\n'% image) 1502 file.close() 1503 1504 # generate the redirects 1505 if generate_redirects: 1506 write_redirects() 1507 1508# vim:set shiftwidth=4 softtabstop=4 expandtab: 1509