odf2moinmoin.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579
  1. # -*- coding: utf-8 -*-
  2. # Copyright (C) 2006-2008 Søren Roug, European Environment Agency
  3. #
  4. # This library is free software; you can redistribute it and/or
  5. # modify it under the terms of the GNU Lesser General Public
  6. # License as published by the Free Software Foundation; either
  7. # version 2.1 of the License, or (at your option) any later version.
  8. #
  9. # This library is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  12. # Lesser General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Lesser General Public
  15. # License along with this library; if not, write to the Free Software
  16. # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
  17. #
  18. # See http://trac.edgewall.org/wiki/WikiFormatting
  19. #
  20. # Contributor(s):
  21. #
  22. import sys, zipfile, xml.dom.minidom
  23. from odf.namespaces import nsdict
  24. from odf.elementtypes import *
  25. IGNORED_TAGS = [
  26. 'draw:a'
  27. 'draw:g',
  28. 'draw:line',
  29. 'draw:object-ole',
  30. 'office:annotation',
  31. 'presentation:notes',
  32. 'svg:desc',
  33. ] + [ nsdict[item[0]]+":"+item[1] for item in empty_elements]
  34. INLINE_TAGS = [ nsdict[item[0]]+":"+item[1] for item in inline_elements]
  35. class TextProps:
  36. """ Holds properties for a text style. """
  37. def __init__(self):
  38. self.italic = False
  39. self.bold = False
  40. self.fixed = False
  41. self.underlined = False
  42. self.strikethrough = False
  43. self.superscript = False
  44. self.subscript = False
  45. def setItalic(self, value):
  46. if value == "italic":
  47. self.italic = True
  48. elif value == "normal":
  49. self.italic = False
  50. def setBold(self, value):
  51. if value == "bold":
  52. self.bold = True
  53. elif value == "normal":
  54. self.bold = False
  55. def setFixed(self, value):
  56. self.fixed = value
  57. def setUnderlined(self, value):
  58. if value and value != "none":
  59. self.underlined = True
  60. def setStrikethrough(self, value):
  61. if value and value != "none":
  62. self.strikethrough = True
  63. def setPosition(self, value):
  64. if value is None or value == '':
  65. return
  66. posisize = value.split(' ')
  67. textpos = posisize[0]
  68. if textpos.find('%') == -1:
  69. if textpos == "sub":
  70. self.superscript = False
  71. self.subscript = True
  72. elif textpos == "super":
  73. self.superscript = True
  74. self.subscript = False
  75. else:
  76. itextpos = int(textpos[:textpos.find('%')])
  77. if itextpos > 10:
  78. self.superscript = False
  79. self.subscript = True
  80. elif itextpos < -10:
  81. self.superscript = True
  82. self.subscript = False
  83. def __str__(self):
  84. return "[italic=%s, bold=i%s, fixed=%s]" % (str(self.italic),
  85. str(self.bold),
  86. str(self.fixed))
  87. class ParagraphProps:
  88. """ Holds properties of a paragraph style. """
  89. def __init__(self):
  90. self.blockquote = False
  91. self.headingLevel = 0
  92. self.code = False
  93. self.title = False
  94. self.indented = 0
  95. def setIndented(self, value):
  96. self.indented = value
  97. def setHeading(self, level):
  98. self.headingLevel = level
  99. def setTitle(self, value):
  100. self.title = value
  101. def setCode(self, value):
  102. self.code = value
  103. def __str__(self):
  104. return "[bq=%s, h=%d, code=%s]" % (str(self.blockquote),
  105. self.headingLevel,
  106. str(self.code))
  107. class ListProperties:
  108. """ Holds properties for a list style. """
  109. def __init__(self):
  110. self.ordered = False
  111. def setOrdered(self, value):
  112. self.ordered = value
  113. class ODF2MoinMoin(object):
  114. def __init__(self, filepath):
  115. self.footnotes = []
  116. self.footnoteCounter = 0
  117. self.textStyles = {"Standard": TextProps()}
  118. self.paragraphStyles = {"Standard": ParagraphProps()}
  119. self.listStyles = {}
  120. self.fixedFonts = []
  121. self.hasTitle = 0
  122. self.lastsegment = None
  123. # Tags
  124. self.elements = {
  125. 'draw:page': self.textToString,
  126. 'draw:frame': self.textToString,
  127. 'draw:image': self.draw_image,
  128. 'draw:text-box': self.textToString,
  129. 'text:a': self.text_a,
  130. 'text:note': self.text_note,
  131. }
  132. for tag in IGNORED_TAGS:
  133. self.elements[tag] = self.do_nothing
  134. for tag in INLINE_TAGS:
  135. self.elements[tag] = self.inline_markup
  136. self.elements['text:line-break'] = self.text_line_break
  137. self.elements['text:s'] = self.text_s
  138. self.elements['text:tab'] = self.text_tab
  139. self.load(filepath)
  140. def processFontDeclarations(self, fontDecl):
  141. """ Extracts necessary font information from a font-declaration
  142. element.
  143. """
  144. for fontFace in fontDecl.getElementsByTagName("style:font-face"):
  145. if fontFace.getAttribute("style:font-pitch") == "fixed":
  146. self.fixedFonts.append(fontFace.getAttribute("style:name"))
  147. def extractTextProperties(self, style, parent=None):
  148. """ Extracts text properties from a style element. """
  149. textProps = TextProps()
  150. if parent:
  151. parentProp = self.textStyles.get(parent, None)
  152. if parentProp:
  153. textProp = parentProp
  154. textPropEl = style.getElementsByTagName("style:text-properties")
  155. if not textPropEl: return textProps
  156. textPropEl = textPropEl[0]
  157. textProps.setItalic(textPropEl.getAttribute("fo:font-style"))
  158. textProps.setBold(textPropEl.getAttribute("fo:font-weight"))
  159. textProps.setUnderlined(textPropEl.getAttribute("style:text-underline-style"))
  160. textProps.setStrikethrough(textPropEl.getAttribute("style:text-line-through-style"))
  161. textProps.setPosition(textPropEl.getAttribute("style:text-position"))
  162. if textPropEl.getAttribute("style:font-name") in self.fixedFonts:
  163. textProps.setFixed(True)
  164. return textProps
  165. def extractParagraphProperties(self, style, parent=None):
  166. """ Extracts paragraph properties from a style element. """
  167. paraProps = ParagraphProps()
  168. name = style.getAttribute("style:name")
  169. if name.startswith("Heading_20_"):
  170. level = name[11:]
  171. try:
  172. level = int(level)
  173. paraProps.setHeading(level)
  174. except:
  175. level = 0
  176. if name == "Title":
  177. paraProps.setTitle(True)
  178. paraPropEl = style.getElementsByTagName("style:paragraph-properties")
  179. if paraPropEl:
  180. paraPropEl = paraPropEl[0]
  181. leftMargin = paraPropEl.getAttribute("fo:margin-left")
  182. if leftMargin:
  183. try:
  184. leftMargin = float(leftMargin[:-2])
  185. if leftMargin > 0.01:
  186. paraProps.setIndented(True)
  187. except:
  188. pass
  189. textProps = self.extractTextProperties(style)
  190. if textProps.fixed:
  191. paraProps.setCode(True)
  192. return paraProps
  193. def processStyles(self, styleElements):
  194. """ Runs through "style" elements extracting necessary information.
  195. """
  196. for style in styleElements:
  197. name = style.getAttribute("style:name")
  198. if name == "Standard": continue
  199. family = style.getAttribute("style:family")
  200. parent = style.getAttribute("style:parent-style-name")
  201. if family == "text":
  202. self.textStyles[name] = self.extractTextProperties(style, parent)
  203. elif family == "paragraph":
  204. self.paragraphStyles[name] = \
  205. self.extractParagraphProperties(style, parent)
  206. self.textStyles[name] = self.extractTextProperties(style, parent)
  207. def processListStyles(self, listStyleElements):
  208. for style in listStyleElements:
  209. name = style.getAttribute("style:name")
  210. prop = ListProperties()
  211. if style.hasChildNodes():
  212. subitems = [el for el in style.childNodes
  213. if el.nodeType == xml.dom.Node.ELEMENT_NODE
  214. and el.tagName == "text:list-level-style-number"]
  215. if len(subitems) > 0:
  216. prop.setOrdered(True)
  217. self.listStyles[name] = prop
  218. def load(self, filepath):
  219. """ Loads an ODT file. """
  220. zip = zipfile.ZipFile(filepath)
  221. styles_doc = xml.dom.minidom.parseString(zip.read("styles.xml"))
  222. fontfacedecls = styles_doc.getElementsByTagName("office:font-face-decls")
  223. if fontfacedecls:
  224. self.processFontDeclarations(fontfacedecls[0])
  225. self.processStyles(styles_doc.getElementsByTagName("style:style"))
  226. self.processListStyles(styles_doc.getElementsByTagName("text:list-style"))
  227. self.content = xml.dom.minidom.parseString(zip.read("content.xml"))
  228. fontfacedecls = self.content.getElementsByTagName("office:font-face-decls")
  229. if fontfacedecls:
  230. self.processFontDeclarations(fontfacedecls[0])
  231. self.processStyles(self.content.getElementsByTagName("style:style"))
  232. self.processListStyles(self.content.getElementsByTagName("text:list-style"))
  233. def compressCodeBlocks(self, text):
  234. """ Removes extra blank lines from code blocks. """
  235. return text
  236. lines = text.split("\n")
  237. buffer = []
  238. numLines = len(lines)
  239. for i in range(numLines):
  240. if (lines[i].strip() or i == numLines-1 or i == 0 or
  241. not ( lines[i-1].startswith(" ")
  242. and lines[i+1].startswith(" ") ) ):
  243. buffer.append("\n" + lines[i])
  244. return ''.join(buffer)
  245. #-----------------------------------
  246. def do_nothing(self, node):
  247. return ''
  248. def draw_image(self, node):
  249. """
  250. """
  251. link = node.getAttribute("xlink:href")
  252. if link and link[:2] == './': # Indicates a sub-object, which isn't supported
  253. return "%s\n" % link
  254. if link and link[:9] == 'Pictures/':
  255. link = link[9:]
  256. return "[[Image(%s)]]\n" % link
  257. def text_a(self, node):
  258. text = self.textToString(node)
  259. link = node.getAttribute("xlink:href")
  260. if link.strip() == text.strip():
  261. return "[%s] " % link.strip()
  262. else:
  263. return "[%s %s] " % (link.strip(), text.strip())
  264. def text_line_break(self, node):
  265. return "[[BR]]"
  266. def text_note(self, node):
  267. cite = (node.getElementsByTagName("text:note-citation")[0]
  268. .childNodes[0].nodeValue)
  269. body = (node.getElementsByTagName("text:note-body")[0]
  270. .childNodes[0])
  271. self.footnotes.append((cite, self.textToString(body)))
  272. return "^%s^" % cite
  273. def text_s(self, node):
  274. try:
  275. num = int(node.getAttribute("text:c"))
  276. return " "*num
  277. except:
  278. return " "
  279. def text_tab(self, node):
  280. return " "
  281. def inline_markup(self, node):
  282. text = self.textToString(node)
  283. if not text.strip():
  284. return '' # don't apply styles to white space
  285. styleName = node.getAttribute("text:style-name")
  286. style = self.textStyles.get(styleName, TextProps())
  287. if style.fixed:
  288. return "`" + text + "`"
  289. mark = []
  290. if style:
  291. if style.italic:
  292. mark.append("''")
  293. if style.bold:
  294. mark.append("'''")
  295. if style.underlined:
  296. mark.append("__")
  297. if style.strikethrough:
  298. mark.append("~~")
  299. if style.superscript:
  300. mark.append("^")
  301. if style.subscript:
  302. mark.append(",,")
  303. revmark = mark[:]
  304. revmark.reverse()
  305. return "%s%s%s" % (''.join(mark), text, ''.join(revmark))
  306. #-----------------------------------
  307. def listToString(self, listElement, indent = 0):
  308. self.lastsegment = listElement.tagName
  309. buffer = []
  310. styleName = listElement.getAttribute("text:style-name")
  311. props = self.listStyles.get(styleName, ListProperties())
  312. i = 0
  313. for item in listElement.childNodes:
  314. buffer.append(" "*indent)
  315. i += 1
  316. if props.ordered:
  317. number = str(i)
  318. number = " " + number + ". "
  319. buffer.append(" 1. ")
  320. else:
  321. buffer.append(" * ")
  322. subitems = [el for el in item.childNodes
  323. if el.tagName in ["text:p", "text:h", "text:list"]]
  324. for subitem in subitems:
  325. if subitem.tagName == "text:list":
  326. buffer.append("\n")
  327. buffer.append(self.listToString(subitem, indent+3))
  328. else:
  329. buffer.append(self.paragraphToString(subitem, indent+3))
  330. self.lastsegment = subitem.tagName
  331. self.lastsegment = item.tagName
  332. buffer.append("\n")
  333. return ''.join(buffer)
  334. def tableToString(self, tableElement):
  335. """ MoinMoin uses || to delimit table cells
  336. """
  337. self.lastsegment = tableElement.tagName
  338. buffer = []
  339. for item in tableElement.childNodes:
  340. self.lastsegment = item.tagName
  341. if item.tagName == "table:table-header-rows":
  342. buffer.append(self.tableToString(item))
  343. if item.tagName == "table:table-row":
  344. buffer.append("\n||")
  345. for cell in item.childNodes:
  346. buffer.append(self.inline_markup(cell))
  347. buffer.append("||")
  348. self.lastsegment = cell.tagName
  349. return ''.join(buffer)
  350. def toString(self):
  351. """ Converts the document to a string.
  352. FIXME: Result from second call differs from first call
  353. """
  354. body = self.content.getElementsByTagName("office:body")[0]
  355. text = body.childNodes[0]
  356. buffer = []
  357. paragraphs = [el for el in text.childNodes
  358. if el.tagName in ["draw:page", "text:p", "text:h","text:section",
  359. "text:list", "table:table"]]
  360. for paragraph in paragraphs:
  361. if paragraph.tagName == "text:list":
  362. text = self.listToString(paragraph)
  363. elif paragraph.tagName == "text:section":
  364. text = self.textToString(paragraph)
  365. elif paragraph.tagName == "table:table":
  366. text = self.tableToString(paragraph)
  367. else:
  368. text = self.paragraphToString(paragraph)
  369. if text:
  370. buffer.append(text)
  371. if self.footnotes:
  372. buffer.append("----")
  373. for cite, body in self.footnotes:
  374. buffer.append("%s: %s" % (cite, body))
  375. buffer.append("")
  376. return self.compressCodeBlocks('\n'.join(buffer))
  377. def textToString(self, element):
  378. buffer = []
  379. for node in element.childNodes:
  380. if node.nodeType == xml.dom.Node.TEXT_NODE:
  381. buffer.append(node.nodeValue)
  382. elif node.nodeType == xml.dom.Node.ELEMENT_NODE:
  383. tag = node.tagName
  384. if tag in ("draw:text-box", "draw:frame"):
  385. buffer.append(self.textToString(node))
  386. elif tag in ("text:p", "text:h"):
  387. text = self.paragraphToString(node)
  388. if text:
  389. buffer.append(text)
  390. elif tag == "text:list":
  391. buffer.append(self.listToString(node))
  392. else:
  393. method = self.elements.get(tag)
  394. if method:
  395. buffer.append(method(node))
  396. else:
  397. buffer.append(" {" + tag + "} ")
  398. return ''.join(buffer)
  399. def paragraphToString(self, paragraph, indent = 0):
  400. dummyParaProps = ParagraphProps()
  401. style_name = paragraph.getAttribute("text:style-name")
  402. paraProps = self.paragraphStyles.get(style_name, dummyParaProps)
  403. text = self.inline_markup(paragraph)
  404. if paraProps and not paraProps.code:
  405. text = text.strip()
  406. if paragraph.tagName == "text:p" and self.lastsegment == "text:p":
  407. text = "\n" + text
  408. self.lastsegment = paragraph.tagName
  409. if paraProps.title:
  410. self.hasTitle = 1
  411. return "= " + text + " =\n"
  412. outlinelevel = paragraph.getAttribute("text:outline-level")
  413. if outlinelevel:
  414. level = int(outlinelevel)
  415. if self.hasTitle: level += 1
  416. if level >= 1:
  417. return "=" * level + " " + text + " " + "=" * level + "\n"
  418. elif paraProps.code:
  419. return "{{{\n" + text + "\n}}}\n"
  420. if paraProps.indented:
  421. return self.wrapParagraph(text, indent = indent, blockquote = True)
  422. else:
  423. return self.wrapParagraph(text, indent = indent)
  424. def wrapParagraph(self, text, indent = 0, blockquote=False):
  425. counter = 0
  426. buffer = []
  427. LIMIT = 50
  428. if blockquote:
  429. buffer.append(" ")
  430. return ''.join(buffer) + text
  431. # Unused from here
  432. for token in text.split():
  433. if counter > LIMIT - indent:
  434. buffer.append("\n" + " "*indent)
  435. if blockquote:
  436. buffer.append(" ")
  437. counter = 0
  438. buffer.append(token + " ")
  439. counter += len(token)
  440. return ''.join(buffer)