removed obsolete issues (many of them fixed with AE)
[docutils.git] / sandbox / aahz / OO / OOwriter.py
blob6a0841040532871881fad90f55bea82850f22ee2
1 # Author: Aahz
2 # Contact: aahz@pythoncraft.com
3 # Revision:
4 # Date: $Date$
5 # Copyright: This module has been placed in the public domain.
7 """
8 OpenOffice writer
10 The output is an OpenOffice.org 1.0-compatible document.
11 """
13 __docformat__ = 'reStructuredText'
16 import sys
17 from warnings import warn
18 import re
20 import docutils
21 from docutils import nodes, utils, writers, languages
23 import OOtext
26 section_styles = [
27 '.ch title',
28 '.head 1',
29 '.head 2'
32 class Writer(writers.Writer):
34 supported = ('OpenOffice')
35 """Formats this writer supports."""
37 def __init__(self):
38 writers.Writer.__init__(self)
39 self.output = None
40 self.translator_class = Translator
42 def translate(self):
43 visitor = self.translator_class(self.document)
44 self.document.walkabout(visitor)
45 self.output = visitor.astext()
48 class Translator(nodes.NodeVisitor):
50 header = [OOtext.content_header]
51 footer = [OOtext.content_footer]
53 start_para = '\n<text:p text:style-name="%s">\n'
54 end_para = '\n</text:p>\n'
56 start_charstyle = '<text:span text:style-name="%s">'
57 end_charstyle = '</text:span>'
59 line_break = '\n<text:line-break/>'
60 re_spaces = re.compile(' +')
61 spaces = '<text:s text:c="%d"/>'
63 re_annotation = re.compile(r'#\d+(?:, #\d+)*$')
65 def __init__(self, document):
66 nodes.NodeVisitor.__init__(self, document)
67 self.settings = document.settings
68 self.body = []
69 self.section_level = 0
70 self.skip_para_tag = False
72 def astext(self):
73 return ''.join(self.header + self.body + self.footer)
75 def encode(self, text):
76 """Encode special characters in `text` & return."""
77 # @@@ A codec to do these and all other HTML entities would be nice.
78 text = text.replace("&", "&amp;")
79 text = text.replace("<", "&lt;")
80 text = text.replace('"', "&quot;")
81 text = text.replace(">", "&gt;")
82 return text
84 def compress_spaces(self, line):
85 while 1:
86 match = self.re_spaces.search(line)
87 if match:
88 start, end = match.span()
89 numspaces = end - start
90 line = line[:start] + (self.spaces % numspaces) + line[end:]
91 else:
92 break
93 return line
95 def fix_annotation(self, line):
96 match = self.re_annotation.search(line)
97 if match:
98 pos = match.start()
99 line = line[:pos] + '|' + line[pos:]
100 return line
102 def visit_Text(self, node):
103 self.body.append(self.encode(node.astext()))
105 def depart_Text(self, node):
106 pass
108 def visit_admonition(self, node, name):
109 self.skip_para_tag = True
110 self.body.append(self.start_para % '.CALLOUT')
112 def depart_admonition(self):
113 self.body.append(self.end_para)
114 self.skip_para_tag = False
116 def visit_attention(self, node):
117 self.visit_admonition(node, 'attention')
119 def depart_attention(self, node):
120 self.depart_admonition()
122 def visit_block_quote(self, node):
123 self.skip_para_tag = True
124 self.body.append(self.start_para % '.quotes')
126 def depart_block_quote(self, node):
127 self.body.append(self.end_para)
128 self.skip_para_tag = False
130 def visit_bullet_list(self, node):
131 self.body.append('\n<text:unordered-list text:style-name=".bullet">\n')
133 def depart_bullet_list(self, node):
134 self.body.append('</text:unordered-list>\n')
136 def visit_caption(self, node):
137 self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
139 def depart_caption(self, node):
140 self.body.append('</p>\n')
142 def visit_caution(self, node):
143 self.visit_admonition(node, 'caution')
145 def depart_caution(self, node):
146 self.depart_admonition()
148 def visit_citation(self, node):
149 self.body.append(self.starttag(node, 'table', CLASS='citation',
150 frame="void", rules="none"))
151 self.footnote_backrefs(node)
153 def depart_citation(self, node):
154 self.body.append('</td></tr>\n'
155 '</tbody>\n</table>\n')
157 def visit_citation_reference(self, node):
158 href = ''
159 if node.has_key('refid'):
160 href = '#' + node['refid']
161 elif node.has_key('refname'):
162 href = '#' + self.document.nameids[node['refname']]
163 self.body.append(self.starttag(node, 'a', '[', href=href,
164 CLASS='citation-reference'))
166 def depart_citation_reference(self, node):
167 self.body.append(']</a>')
169 def visit_classifier(self, node):
170 self.body.append(' <span class="classifier-delimiter">:</span> ')
171 self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
173 def depart_classifier(self, node):
174 self.body.append('</span>')
176 def visit_colspec(self, node):
177 self.colspecs.append(node)
179 def depart_colspec(self, node):
180 pass
182 def write_colspecs(self):
183 width = 0
184 for node in self.colspecs:
185 width += node['colwidth']
186 for node in self.colspecs:
187 colwidth = int(node['colwidth'] * 100.0 / width + 0.5)
188 self.body.append(self.emptytag(node, 'col',
189 colwidth='%i%%' % colwidth))
190 self.colspecs = []
192 def visit_comment(self, node):
193 raise nodes.SkipNode
195 def visit_decoration(self, node):
196 pass
198 def depart_decoration(self, node):
199 pass
201 def visit_definition(self, node):
202 self.body.append('</dt>\n')
203 self.body.append(self.starttag(node, 'dd', ''))
204 if len(node) and isinstance(node[0], nodes.paragraph):
205 node[0].set_class('first')
207 def depart_definition(self, node):
208 self.body.append('</dd>\n')
210 def visit_definition_list(self, node):
211 print node.astext()
212 self.body.append(self.starttag(node, 'dl'))
214 def depart_definition_list(self, node):
215 self.body.append('</dl>\n')
217 def visit_definition_list_item(self, node):
218 pass
220 def depart_definition_list_item(self, node):
221 pass
223 def visit_description(self, node):
224 self.body.append(self.starttag(node, 'td', ''))
225 if len(node) and isinstance(node[0], nodes.paragraph):
226 node[0].set_class('first')
228 def depart_description(self, node):
229 self.body.append('</td>')
231 def visit_doctest_block(self, node):
232 self.visit_literal_block(node)
234 def visit_document(self, node):
235 pass
237 def depart_document(self, node):
238 pass
240 def visit_emphasis(self, node):
241 self.body.append(self.start_charstyle % 'italic')
243 def depart_emphasis(self, node):
244 self.body.append(self.end_charstyle)
246 def visit_entry(self, node):
247 if isinstance(node.parent.parent, nodes.thead):
248 tagname = 'th'
249 else:
250 tagname = 'td'
251 atts = {}
252 if node.has_key('morerows'):
253 atts['rowspan'] = node['morerows'] + 1
254 if node.has_key('morecols'):
255 atts['colspan'] = node['morecols'] + 1
256 self.body.append(self.starttag(node, tagname, '', **atts))
257 self.context.append('</%s>\n' % tagname.lower())
258 if len(node) == 0: # empty cell
259 self.body.append('&nbsp;')
260 elif isinstance(node[0], nodes.paragraph):
261 node[0].set_class('first')
263 def depart_entry(self, node):
264 self.body.append(self.context.pop())
266 def visit_enumerated_list(self, node):
268 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
269 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be
270 usable.
272 atts = {}
273 if node.has_key('start'):
274 atts['start'] = node['start']
275 if node.has_key('enumtype'):
276 atts['class'] = node['enumtype']
277 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
278 # single "format" attribute? Use CSS2?
279 old_compact_simple = self.compact_simple
280 self.context.append((self.compact_simple, self.compact_p))
281 self.compact_p = None
282 self.compact_simple = (self.options.compact_lists and
283 (self.compact_simple
284 or self.topic_class == 'contents'
285 or self.check_simple_list(node)))
286 if self.compact_simple and not old_compact_simple:
287 atts['class'] = (atts.get('class', '') + ' simple').strip()
288 self.body.append(self.starttag(node, 'ol', **atts))
290 def depart_enumerated_list(self, node):
291 self.compact_simple, self.compact_p = self.context.pop()
292 self.body.append('</ol>\n')
294 def visit_error(self, node):
295 self.visit_admonition(node, 'error')
297 def depart_error(self, node):
298 self.depart_admonition()
300 def visit_field(self, node):
301 self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
303 def depart_field(self, node):
304 self.body.append('</tr>\n')
306 def visit_field_body(self, node):
307 self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
308 if len(node) and isinstance(node[0], nodes.paragraph):
309 node[0].set_class('first')
311 def depart_field_body(self, node):
312 self.body.append('</td>\n')
314 def visit_field_list(self, node):
315 self.body.append(self.starttag(node, 'table', frame='void',
316 rules='none', CLASS='field-list'))
317 self.body.append('<col class="field-name" />\n'
318 '<col class="field-body" />\n'
319 '<tbody valign="top">\n')
321 def depart_field_list(self, node):
322 self.body.append('</tbody>\n</table>\n')
324 def visit_field_name(self, node):
325 atts = {}
326 if self.in_docinfo:
327 atts['class'] = 'docinfo-name'
328 else:
329 atts['class'] = 'field-name'
330 if len(node.astext()) > 14:
331 atts['colspan'] = 2
332 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
333 else:
334 self.context.append('')
335 self.body.append(self.starttag(node, 'th', '', **atts))
337 def depart_field_name(self, node):
338 self.body.append(':</th>')
339 self.body.append(self.context.pop())
341 def visit_figure(self, node):
342 self.body.append(self.start_para % '.figure')
344 def depart_figure(self, node):
345 self.body.append(self.end_para)
347 def visit_footnote(self, node):
348 raise nodes.SkipNode
350 def footnote_backrefs(self, node):
351 warn("footnote backrefs not available")
353 def depart_footnote(self, node):
354 pass
356 def visit_footnote_reference(self, node):
357 name = node['refid']
358 id = node['id']
359 number = node['auto']
360 for footnote in self.document.autofootnotes:
361 if name == footnote['name']:
362 break
363 self.body.append('<text:footnote text:id="%s">\n' % id)
364 self.body.append('<text:footnote-citation text:string-value="%s"/>\n' % number)
365 self.body.append('<text:footnote-body>\n')
366 self.body.append(self.start_para % '.body')
367 for child in footnote.children:
368 if isinstance(child, nodes.paragraph):
369 self.body.append(child.astext())
370 self.body.append(self.end_para)
371 self.body.append('</text:footnote-body>\n')
372 self.body.append('</text:footnote>')
373 raise nodes.SkipNode
375 def depart_footnote_reference(self, node):
376 pass
378 def visit_generated(self, node):
379 pass
381 def depart_generated(self, node):
382 pass
384 def visit_header(self, node):
385 self.context.append(len(self.body))
387 def depart_header(self, node):
388 start = self.context.pop()
389 self.body_prefix.append(self.starttag(node, 'div', CLASS='header'))
390 self.body_prefix.extend(self.body[start:])
391 self.body_prefix.append('<hr />\n</div>\n')
392 del self.body[start:]
394 def visit_hint(self, node):
395 self.visit_admonition(node, 'hint')
397 def depart_hint(self, node):
398 self.depart_admonition()
400 def visit_image(self, node):
401 name = "Figure: %s\n" % node.attributes['uri']
402 self.body.append(name)
404 def depart_image(self, node):
405 pass
407 def visit_important(self, node):
408 self.visit_admonition(node, 'important')
410 def depart_important(self, node):
411 self.depart_admonition()
413 def visit_index_entry(self, node):
414 index_format = '<text:alphabetical-index-mark text:string-value="%s"/>\n'
415 self.body.append(self.start_para % '.body')
416 entries = node.astext().split('\n')
417 for entry in entries:
418 self.body.append(index_format % self.encode(entry))
419 self.body.append(self.end_para)
420 raise nodes.SkipNode
422 def visit_interpreted(self, node):
423 # @@@ Incomplete, pending a proper implementation on the
424 # Parser/Reader end.
425 #self.body.append(node['role'] + ':')
426 self.body.append(node.astext())
427 raise nodes.SkipNode
429 def depart_interpreted(self, node):
430 pass
432 # Don't need footnote labels/numbers
433 def visit_label(self, node):
434 print "!"
435 raise nodes.SkipNode
437 def visit_legend(self, node):
438 self.body.append(self.starttag(node, 'div', CLASS='legend'))
440 def depart_legend(self, node):
441 self.body.append('</div>\n')
443 def visit_line_block(self, node):
444 self.body.append(self.start_para % '.quotes')
445 lines = node.astext()
446 lines = lines.split('\n')
447 lines = self.line_break.join(lines)
448 self.body.append(lines)
449 self.body.append(self.end_para)
450 raise nodes.SkipNode
452 def visit_list_item(self, node):
453 self.body.append('<text:list-item>')
455 def depart_list_item(self, node):
456 self.body.append('</text:list-item>\n')
458 def visit_literal(self, node):
459 self.body.append(self.start_charstyle % 'code')
461 def depart_literal(self, node):
462 self.body.append(self.end_charstyle)
464 def visit_literal_block(self, node):
465 self.body.append(self.start_para % '.code first')
466 self.body.append(self.end_para)
467 lines = self.encode(node.astext())
468 lines = lines.split('\n')
469 while lines[-1] == '':
470 lines.pop()
471 for line in lines:
472 self.body.append(self.start_para % '.code')
473 line = self.fix_annotation(line)
474 line = self.compress_spaces(line)
475 self.body.append(line)
476 self.body.append(self.end_para)
477 self.body.append(self.start_para % '.code last')
478 self.body.append(self.end_para)
479 raise nodes.SkipNode
481 def visit_note(self, node):
482 self.visit_admonition(node, '.note')
484 def depart_note(self, node):
485 self.depart_admonition()
487 def visit_option(self, node):
488 if self.context[-1]:
489 self.body.append(', ')
491 def depart_option(self, node):
492 self.context[-1] += 1
494 def visit_option_argument(self, node):
495 self.body.append(node.get('delimiter', ' '))
496 self.body.append(self.starttag(node, 'var', ''))
498 def depart_option_argument(self, node):
499 self.body.append('</var>')
501 def visit_option_group(self, node):
502 atts = {}
503 if len(node.astext()) > 14:
504 atts['colspan'] = 2
505 self.context.append('</tr>\n<tr><td>&nbsp;</td>')
506 else:
507 self.context.append('')
508 self.body.append(self.starttag(node, 'td', **atts))
509 self.body.append('<kbd>')
510 self.context.append(0) # count number of options
512 def depart_option_group(self, node):
513 self.context.pop()
514 self.body.append('</kbd></td>\n')
515 self.body.append(self.context.pop())
517 def visit_option_list(self, node):
518 self.body.append(
519 self.starttag(node, 'table', CLASS='option-list',
520 frame="void", rules="none"))
521 self.body.append('<col class="option" />\n'
522 '<col class="description" />\n'
523 '<tbody valign="top">\n')
525 def depart_option_list(self, node):
526 self.body.append('</tbody>\n</table>\n')
528 def visit_option_list_item(self, node):
529 self.body.append(self.starttag(node, 'tr', ''))
531 def depart_option_list_item(self, node):
532 self.body.append('</tr>\n')
534 def visit_option_string(self, node):
535 self.body.append(self.starttag(node, 'span', '', CLASS='option'))
537 def depart_option_string(self, node):
538 self.body.append('</span>')
540 def visit_paragraph(self, node):
541 if not self.skip_para_tag:
542 self.body.append(self.start_para % '.body')
544 def depart_paragraph(self, node):
545 if not self.skip_para_tag:
546 self.body.append(self.end_para)
548 def visit_problematic(self, node):
549 if node.hasattr('refid'):
550 self.body.append('<a href="#%s" name="%s">' % (node['refid'],
551 node['id']))
552 self.context.append('</a>')
553 else:
554 self.context.append('')
555 self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
557 def depart_problematic(self, node):
558 self.body.append('</span>')
559 self.body.append(self.context.pop())
561 def visit_raw(self, node):
562 if node.has_key('format') and node['format'] == 'html':
563 self.body.append(node.astext())
564 raise nodes.SkipNode
566 def visit_reference(self, node):
567 pass
569 def depart_reference(self, node):
570 pass
572 def visit_row(self, node):
573 self.body.append(self.starttag(node, 'tr', ''))
575 def depart_row(self, node):
576 self.body.append('</tr>\n')
578 def visit_section(self, node):
579 self.section_level += 1
581 def depart_section(self, node):
582 self.section_level -= 1
584 def visit_strong(self, node):
585 self.body.append('<strong>')
587 def depart_strong(self, node):
588 self.body.append('</strong>')
590 def visit_table(self, node):
591 self.body.append(
592 self.starttag(node, 'table', CLASS="table",
593 frame='border', rules='all'))
595 def depart_table(self, node):
596 self.body.append('</table>\n')
598 def visit_target(self, node):
599 if not (node.has_key('refuri') or node.has_key('refid')
600 or node.has_key('refname')):
601 self.body.append(self.starttag(node, 'a', '', CLASS='target'))
602 self.context.append('</a>')
603 else:
604 self.context.append('')
606 def depart_target(self, node):
607 self.body.append(self.context.pop())
609 def visit_tbody(self, node):
610 self.write_colspecs()
611 self.body.append(self.context.pop()) # '</colgroup>\n' or ''
612 self.body.append(self.starttag(node, 'tbody', valign='top'))
614 def depart_tbody(self, node):
615 self.body.append('</tbody>\n')
617 def visit_term(self, node):
618 self.body.append(self.starttag(node, 'dt', ''))
620 def depart_term(self, node):
622 Leave the end tag to `self.visit_definition()`, in case there's a
623 classifier.
625 pass
627 def visit_tgroup(self, node):
628 # Mozilla needs <colgroup>:
629 self.body.append(self.starttag(node, 'colgroup'))
630 # Appended by thead or tbody:
631 self.context.append('</colgroup>\n')
633 def depart_tgroup(self, node):
634 pass
636 def visit_thead(self, node):
637 self.write_colspecs()
638 self.body.append(self.context.pop()) # '</colgroup>\n'
639 # There may or may not be a <thead>; this is for <tbody> to use:
640 self.context.append('')
641 self.body.append(self.starttag(node, 'thead', valign='bottom'))
643 def depart_thead(self, node):
644 self.body.append('</thead>\n')
646 def visit_tip(self, node):
647 self.visit_admonition(node, 'tip')
649 def depart_tip(self, node):
650 self.depart_admonition()
652 def visit_title(self, node):
653 """Only 3 section levels are supported by this writer."""
654 title_tag = self.start_para % section_styles[self.section_level]
655 self.body.append(title_tag)
657 def depart_title(self, node):
658 self.body.append(self.end_para)
660 def visit_warning(self, node):
661 self.visit_admonition(node, 'warning')
663 def depart_warning(self, node):
664 self.depart_admonition()
666 def visit_system_message(self, node):
667 print node.astext()
669 def depart_system_message(self, node):
670 pass
672 def unknown_visit(self, node):
673 print "Failure processing at line", node.line
674 print "Failure is", node.astext()
675 raise NotImplementedError('visiting unimplemented node type: %s'
676 % node.__class__.__name__)