2 # Contact: aahz@pythoncraft.com
5 # Copyright: This module has been placed in the public domain.
10 The output is an OpenOffice.org 1.0-compatible document.
13 __docformat__
= 'reStructuredText'
17 from warnings
import warn
21 from docutils
import nodes
, utils
, writers
, languages
32 class Writer(writers
.Writer
):
34 supported
= ('OpenOffice')
35 """Formats this writer supports."""
38 writers
.Writer
.__init
__(self
)
40 self
.translator_class
= Translator
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
69 self
.section_level
= 0
70 self
.skip_para_tag
= False
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("&", "&")
79 text
= text
.replace("<", "<")
80 text
= text
.replace('"', """)
81 text
= text
.replace(">", ">")
84 def compress_spaces(self
, line
):
86 match
= self
.re_spaces
.search(line
)
88 start
, end
= match
.span()
89 numspaces
= end
- start
90 line
= line
[:start
] + (self
.spaces
% numspaces
) + line
[end
:]
95 def fix_annotation(self
, line
):
96 match
= self
.re_annotation
.search(line
)
99 line
= line
[:pos
] + '|' + line
[pos
:]
102 def visit_Text(self
, node
):
103 self
.body
.append(self
.encode(node
.astext()))
105 def depart_Text(self
, node
):
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
):
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
):
182 def write_colspecs(self
):
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
))
192 def visit_comment(self
, node
):
195 def visit_decoration(self
, node
):
198 def depart_decoration(self
, node
):
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
):
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
):
220 def depart_definition_list_item(self
, node
):
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
):
237 def depart_document(self
, node
):
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
):
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(' ')
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
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
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
):
327 atts
['class'] = 'docinfo-name'
329 atts
['class'] = 'field-name'
330 if len(node
.astext()) > 14:
332 self
.context
.append('</tr>\n<tr><td> </td>')
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
):
350 def footnote_backrefs(self
, node
):
351 warn("footnote backrefs not available")
353 def depart_footnote(self
, node
):
356 def visit_footnote_reference(self
, node
):
359 number
= node
['auto']
360 for footnote
in self
.document
.autofootnotes
:
361 if name
== footnote
['name']:
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>')
375 def depart_footnote_reference(self
, node
):
378 def visit_generated(self
, node
):
381 def depart_generated(self
, node
):
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
):
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
)
422 def visit_interpreted(self
, node
):
423 # @@@ Incomplete, pending a proper implementation on the
425 #self.body.append(node['role'] + ':')
426 self
.body
.append(node
.astext())
429 def depart_interpreted(self
, node
):
432 # Don't need footnote labels/numbers
433 def visit_label(self
, node
):
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
)
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] == '':
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
)
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
):
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
):
503 if len(node
.astext()) > 14:
505 self
.context
.append('</tr>\n<tr><td> </td>')
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
):
514 self
.body
.append('</kbd></td>\n')
515 self
.body
.append(self
.context
.pop())
517 def visit_option_list(self
, node
):
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'],
552 self
.context
.append('</a>')
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())
566 def visit_reference(self
, node
):
569 def depart_reference(self
, node
):
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
):
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>')
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
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
):
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
):
669 def depart_system_message(self
, node
):
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
__)