pdf-merge.py: Bump to version 0.3
[mw2txt.git] / posts / Maple / mw2txt.py
1 #!/usr/bin/env python
2
3 # Copyright (C) 2011-2012 W. Trevor King <wking@drexel.edu>
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Lesser General Public License as
7 # published by the Free Software Foundation, either version 3 of the
8 # License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful, but
11 # WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 # Lesser General Public License for more details.
14 #
15 # You should have received a copy of the GNU Lesser General Public
16 # License along with this program.  If not, see
17 # <http://www.gnu.org/licenses/>.
18
19 """View Maple worksheets (.mw) from the command line (without X).
20
21 ./mw2txt.py -c input.mw | less
22 ./mw2txt.py -m input.mw | /opt/maple15/bin/maple | less
23 """
24
25 import sys as _sys
26
27 import lxml.etree as _lxml_etree
28
29 try:
30     from pygments.console import colorize as _colorize
31 except ImportError, e:
32     _sys.stderr.write(str(e) + '\n')
33     def _write_color(string, color=None, stream=None):
34         if stream is None:
35             stream = _sys.stdout
36         stream.write(string)
37 else:
38     def _write_color(string, color=None, stream=None):
39         if color is None:
40             color = 'reset'
41         if stream is None:
42             stream = _sys.stdout
43         stream.write(_colorize(color_key=color, text=string))
44
45
46 __version__ = '0.1'
47
48
49 class Writer (object):
50     def __init__(self, color=None, stream=None, use_color=False):
51         self.color = color
52         self.stream = stream
53         self.use_color = use_color
54         self.last_char = None
55
56     def __call__(self, text, color=None):
57         if not self.use_color:
58             color = None
59         elif color is None:
60             color = self.color
61         if text == '\n' and self.last_char == '\n':
62             return  # don't add lots of blank lines
63         _write_color(string=text, color=color, stream=self.stream)
64         self.last_char = text[-1]
65
66
67 def mw2txt(path, writer, filter_math=False):
68     xml = _lxml_etree.parse(path)
69     pruned_iteration(
70         root=xml.getroot(),
71         match=lambda node: node.tag == 'Text-field',
72         match_action=lambda node: top_text_node2txt(
73             node=node, writer=writer, filter_math=filter_math),
74         match_tail=lambda node:writer(text='\n'))
75
76 def top_text_node2txt(node, writer, filter_math=False):
77     if filter_math:
78         match_action = None
79     else:
80         match_action = lambda node: other_in_text_node2txt(
81             node=node, writer=writer)
82     pruned_iteration(
83         root=node,
84         match=lambda node: node.tag not in ['Text-field', 'Font', 'Hyperlink'],
85         match_action=match_action,
86         other_action=lambda node: text_node2txt(
87             node=node, writer=writer, filter_math=filter_math),
88         match_tail=lambda node:tail_node2txt(
89             node=node, writer=writer, filter_math=filter_math),
90         other_tail=lambda node:tail_node2txt(
91             node=node, writer=writer, filter_math=filter_math))
92
93 def other_in_text_node2txt(node, writer):
94     if node.tag in ['Drawing-Root']:
95         # ignore missing content
96         pass
97     elif node.tag in ['Equation', 'Image', 'Plot']:
98         # warn about missing content
99         writer(text=node.tag, color='yellow')
100     else:
101         # warn about wierd tag
102         writer(text=node.tag, color='magenta')
103
104 def text_node2txt(node, writer, filter_math=False):
105     if node.tag not in ['Text-field', 'Font', 'Hyperlink'] and not filter_math:
106         # warn about wierd tag
107         writer(text=node.tag, color='magenta')
108     write_text(
109         node=node, text=node.text, writer=writer, filter_math=filter_math)
110
111 def tail_node2txt(node, writer, filter_math=False):
112     if node.tag != 'Text-field':
113         write_text(
114             node=node.getparent(), text=node.tail, writer=writer,
115             filter_math=filter_math)
116
117 def write_text(node, text, writer, filter_math=False):
118     if not text:
119         return
120     style = node_style(node)
121     if filter_math:
122         if style == 'Maple Input':
123             writer(text=text)
124         return
125     prompt = node.get('prompt', None)
126     if prompt:
127         t = '\n'.join(prompt+line for line in text.splitlines())
128         if text.endswith('\n'):
129             t += '\n'  # '\n'.join('a\nb\n'.splitlines()) == 'a\nb'
130         if writer.last_char not in [None, '\n']:
131             t = t[len(prompt):]  # no initial prompt
132         text = t
133     if style == 'Maple Input':
134         color = 'red'
135     else:
136         color = None
137     writer(text=text, color=color)
138
139 def node_style(node):
140     p = node
141     while p is not None:
142         style = p.get('style', None)
143         if style:
144             return style
145         p = p.getparent()
146     return None
147
148 def pruned_iteration(root, match, match_action=None, match_tail=None,
149                      other_action=None, other_tail=None, debug=False):
150     if debug:
151         _write_color('start pruned iteration from %s\n' % root, color='blue')
152     line = [None]
153     stack = [root]
154     while len(stack) > 0:
155         node = stack.pop(0)
156         p = node.getparent()
157         while line[-1] != p:
158             n = line.pop()
159             if n is None:
160                 break
161             _pruned_iteration_handle_tail(
162                 node=n, match=match, match_tail=match_tail,
163                 other_tail=other_tail, debug=debug)
164         line.append(node)
165         if debug:
166             color_node(node, color='cyan')
167         if match(node):
168             if match_action:
169                 match_action(node)
170         else:
171             if other_action:
172                 other_action(node)
173             stack = list(node.getchildren()) + stack
174     while len(line) > 0:
175         n = line.pop()
176         if n is None:
177             break
178         _pruned_iteration_handle_tail(
179             node=n, match=match, match_tail=match_tail, other_tail=other_tail,
180             debug=debug)
181     if debug:
182         _write_color('end pruned iteration from %s\n' % root, color='blue')
183
184 def _pruned_iteration_handle_tail(node, match, match_tail, other_tail,
185                                   debug=False):
186     if debug:
187         color_node(node, color='magenta', tail=True)
188     if match(node):
189         if match_tail:
190             match_tail(node)
191     else:
192         if other_tail:
193             other_tail(node)
194
195 def node_depth(node):
196     depth = 0
197     p = node.getparent()
198     while p is not None:
199         depth += 1
200         p = p.getparent()
201     return depth
202
203 def color_node(node, color=None, tail=False):
204     depth = node_depth(node)
205     string = ' '*depth + node.tag
206     if tail:
207         string += ' tail'
208     _write_color(string + '\n', color)
209
210
211 if __name__ == '__main__':
212     from optparse import OptionParser as _OptionParser
213
214     # don't wrap epilog paragraphs
215     class OptionParser (_OptionParser):
216         def format_epilog(self, formatter):
217             return self.epilog
218
219     parser = OptionParser(
220         usage='%prog [options] input.mw', epilog='\n'+__doc__)
221     parser.add_option(
222         '-c', '--color', dest='color', action='store_true',
223         help='Use ANSI escape sequences to color output')
224     parser.add_option(
225         '-m', '--maple', dest='maple', action='store_true',
226         help='output text suitable for piping into `maple`')
227
228     options,args = parser.parse_args()
229     path = args[0]
230
231     filter_math = options.maple
232     writer = Writer(use_color=options.color)
233     if options.maple:
234         if options.color:
235             raise ValueError("maple doesn't understand ANSI color")
236     mw2txt(path=path, writer=writer, filter_math=filter_math)