631517ad0f2acdaf08f7af9c4ad09ddebb09b4ce
[texml.git] / Texml / texmlwr.py
1 """ TeXML Writer and string services """
2 # $Id: texmlwr.py,v 1.9 2006-07-20 03:56:27 olpa Exp $
3
4 #
5 # Modes of processing of special characters
6 #
7 DEFAULT = 0
8 TEXT    = 1
9 MATH    = 2
10 ASIS    = 3
11 PDF     = 4
12 WEAK_WS_IS_NEWLINE = 2
13
14 import unimap
15 import specmap
16 import codecs
17 import os
18 import sys
19 import string
20
21 #
22 # Writer&Co class
23 #
24 class texmlwr:
25   
26   #
27   # Object variables
28   #
29   # Handling of '--', '---' and other ligatures
30   # last_char
31   #
32   # Modes of transformation can be tuned and nested
33   # mode
34   # mode_stack
35   # escape
36   # escape_stack
37   # ligatures
38   # ligatures_stack
39   # emptylines
40   # emptylines_stack
41   #
42   # Current length of a line that is being written. Value usually
43   # incorrect, but always correct to detect the start of a line (0)
44   # > approx_current_line_len
45   # If length of a current line is greater the value
46   # then writer converts weak whitespaces into newlines.
47   # And flag if it is possible
48   # > autonewline_after_len
49   # > allow_weak_ws_to_nl
50   # > is_after_weak_ws
51   # We usually don't allow empty lines in output because such lines
52   # cause a paragraph break in TeX.
53   # > line_is_blank
54   #
55   # always_ascii: If attempts to write a character to the output
56   # stream have failed, then the code converts the symbol to bytes,
57   # and these bytes are written in the ^^xx format.
58   #
59   # bad_enc_warned: TeXML issues warning if it fails to convert
60   # a symbol. This flag controls that warning was issued only once.
61   #
62   
63   def __init__(self, stream, encoding, autonl_width, use_context = 0, always_ascii = 0):
64     """ Remember output stream, initialize data structures """
65     # Tune output stream
66     self.always_ascii = always_ascii
67     self.encoding     = encoding
68     try:
69       if always_ascii:
70         encoding        = 'ascii'
71       self.stream     = stream_encoder(stream, encoding)
72     except Exception, e:
73       raise ValueError("Can't create encoder: '%s'" % e)
74     # Continue initialization
75     self.after_char0d     = 1
76     self.after_char0a     = 1
77     self.last_ch          = None
78     self.line_is_blank    = 1
79     self.mode             = TEXT
80     self.mode_stack       = []
81     self.escape           = 1
82     self.escape_stack     = []
83     self.ligatures        = 0
84     self.ligatures_stack  = []
85     self.emptylines       = 0
86     self.emptylines_stack = []
87     self.approx_current_line_len = 0
88     self.autonewline_after_len   = autonl_width
89     self.allow_weak_ws_to_nl     = 1
90     self.is_after_weak_ws        = 0
91     self.use_context      = use_context
92     self.bad_enc_warned   = 0
93
94   def stack_mode(self, mode):
95     """ Put new mode into the stack of modes """
96     self.mode_stack.append(self.mode)
97     if mode != DEFAULT:
98       self.mode = mode
99
100   def unstack_mode(self):
101     """ Restore mode """
102     self.mode = self.mode_stack.pop()
103
104   def stack_escape(self, ifdo):
105     """ Set if escaping is required. Remember old value. """
106     self.escape_stack.append(self.escape)
107     if ifdo != None:
108       self.escape = ifdo
109
110   def unstack_escape(self):
111     """ Restore old policy of escaping """
112     self.escape = self.escape_stack.pop()
113
114   def stack_ligatures(self, ifdo):
115     """ Set if breaking of ligatures is required. Remember old value. """
116     self.ligatures_stack.append(self.ligatures)
117     if ifdo != None:
118       self.ligatures = ifdo
119
120   def unstack_ligatures(self):
121     """ Restore old policy of breaking ligatures """
122     self.ligatures = self.ligatures_stack.pop()
123
124   def stack_emptylines(self, ifdo):
125     """ Set if empty lines are required. Remember old value. """
126     self.emptylines_stack.append(self.emptylines)
127     if ifdo != None:
128       self.emptylines = ifdo
129
130   def unstack_emptylines(self):
131     """ Restore old policy of handling of empty lines """
132     self.emptylines = self.emptylines_stack.pop()
133
134   def set_allow_weak_ws_to_nl(self, flag):
135     """ Set flag if weak spaces can be converted to new lines """
136     self.allow_weak_ws_to_nl = flag
137
138   def conditionalNewline(self):
139     """ Write a new line unless already at the start of a line """
140     if self.approx_current_line_len != 0:
141       self.writech('\n', 0)
142
143   def writeWeakWS(self, hint=1):
144     """ Write a whitespace instead of whitespaces deleted from source XML. Parameter 'hint' is a hack to make <opt/> and <parm/> in <env/> working good. hint=WEAK_WS_IS_NEWLINE if weak space should be converted to newline, not to a space """
145     # weak WS that is newline can not be converted to ws that is space
146     if hint <= self.is_after_weak_ws:
147       # return or avoid next if(). I prefer return.
148       return                                               # return
149     self.is_after_weak_ws = hint
150     #self.last_ch          = ' ' # no, setting so is an error: new lines are not corrected after it. Anyway, check for weak ws is the first action in writech, so it should not lead to errors
151     #
152     # Break line if it is too long
153     # We should not break lines if we regard spaces
154     # Check for WEAK_WS_IS_NEWLINE in order to avoid line break in
155     #   \begin{foo}[aa.....aa]<no line break here!>[bbb]
156     #
157     if (self.approx_current_line_len > self.autonewline_after_len) and self.allow_weak_ws_to_nl and (hint != WEAK_WS_IS_NEWLINE):
158       self.conditionalNewline()
159       return                                               # return
160
161   def ungetWeakWS(self):
162     """ Returns whitespace state and clears WS flag """
163     hint = self.is_after_weak_ws
164     self.is_after_weak_ws = 0
165     return hint
166
167   def writech(self, ch, esc_specials):
168     """ Write a char, (maybe) escaping specials """
169     #
170     # Write for PDF string
171     #
172     if  PDF == self.mode:
173       self.stack_mode(TEXT)
174       self.writepdfch(ch)
175       self.unstack_mode()
176       return                                               # return
177     #
178     # Write a suspended whitespace
179     #
180     if self.is_after_weak_ws:
181       hint = self.is_after_weak_ws
182       self.is_after_weak_ws = 0
183       if hint == WEAK_WS_IS_NEWLINE:
184         if ('\n' != ch) and ('\r' != ch):
185           self.conditionalNewline()
186       else:
187         if (self.approx_current_line_len != 0) and not(ch in string.whitespace):
188           self.writech(' ', 0)
189     #
190     # Update counter
191     #
192     self.approx_current_line_len = self.approx_current_line_len + 1
193     #
194     # Handle well-known standard TeX ligatures
195     #
196     if not(self.ligatures):
197       if '-' == ch:
198         if '-' == self.last_ch:
199           self.writech('{', 0)
200           self.writech('}', 0)
201       elif "'" == ch:
202         if "'" == self.last_ch:
203           self.writech('{', 0)
204           self.writech('}', 0)
205       elif '`' == ch:
206         if ('`' == self.last_ch) or ('!' == self.last_ch) or ('?' == self.last_ch):
207           self.writech('{', 0)
208           self.writech('}', 0)
209     #
210     # Handle end-of-line symbols.
211     # XML spec says: 2.11 End-of-Line Handling:
212     # ... contains either the literal two-character sequence "#xD#xA" or
213     # a standalone literal #xD, an XML processor must pass to the
214     # application the single character #xA.
215     #
216     if ('\n' == ch) or ('\r' == ch):
217       #
218       # We should never get '\r', but only '\n'.
219       # Anyway, someone will copy and paste this code, and code will
220       # get '\r'. In this case rewrite '\r' as '\n'.
221       #
222       if '\r' == ch:
223         ch = '\n'
224       #
225       # TeX interprets empty line as \par, fix this problem
226       #
227       if self.line_is_blank and (not self.emptylines):
228         self.writech('%', 0)
229       #
230       # Now create newline, update counters and return
231       #
232       self.stream.write(os.linesep)
233       self.approx_current_line_len = 0
234       self.last_ch                 = ch
235       self.line_is_blank           = 1
236       return                                               # return
237     #
238     # Remember the last character
239     #
240     self.last_ch = ch
241     #
242     # Reset the flag of a blank line
243     #
244     if not ch in ('\x20', '\x09'):
245       self.line_is_blank = 0
246     #
247     # Handle specials
248     #
249     if esc_specials:
250       try:
251         if self.mode == TEXT:
252             # Paul Tremblay changed this code on 2005-03-08
253           if self.use_context:
254             self.write(specmap.textescmap_context[ch], 0)
255           else:
256             self.write(specmap.textescmap[ch], 0)
257         else:
258           self.write(specmap.mathescmap[ch], 0)
259         return                                             # return
260       except:
261         pass
262     #
263     # First attempt to write symbol as-is
264     #
265     try:
266       self.stream.write(ch)
267       return                                               # return
268     except:
269       pass
270     #
271     # Try write the symbol in the ^^XX form
272     #
273     if self.always_ascii:
274       try:
275         bytes = ch.encode(self.encoding)
276         for by in bytes:
277           self.write('^^%02x' % ord(by), 0)
278         return
279       except Exception, e:
280         pass
281     #
282     # Symbol have to be rewritten. Let start with math mode.
283     #
284     chord = ord(ch)
285     if self.mode == TEXT:
286       #
287       # Text mode, lookup text map
288       #
289       try:
290         self.write(unimap.textmap[chord], 0)
291         return                                             # return
292       except:
293         #
294         # Text mode, lookup math map
295         #
296         tostr = unimap.mathmap.get(chord, None)
297     else: # self.mode == MATH:
298       #
299       # Math mode, lookup math map
300       #
301       try:
302         self.write(unimap.mathmap[chord], 0)
303         return                                             # return
304       except:
305         #
306         # Math mode, lookup text map
307         #
308         tostr = unimap.textmap.get(chord, None)
309     #
310     # If mapping in another mode table is found, use a wrapper
311     #
312     if tostr != None:
313       if self.mode == TEXT:
314         self.write('\\ensuremath{', 0)
315       else:
316         self.write('\\ensuretext{', 0)
317       self.write(tostr, 0)
318       self.writech('}', 0)
319       return                                               # return
320     #
321     # Finally, warn about bad symbol and write it in the &#xNNN; form
322     #
323     if not self.bad_enc_warned:
324       sys.stderr.write("texml: not all XML symbols are converted\n");
325       self.bad_enc_warned = 1
326     self.write('\\unicodechar{%d}' % chord, 0)
327
328   def write(self, str, escape = None):
329     """ Write symbols char-by-char in current mode of escaping """
330     if None == escape:
331       escape = self.escape
332     for ch in str:
333       self.writech(ch, escape)
334
335   def writepdfch(self, ch):
336     """ Write char in Acrobat utf16be encoding """
337     bytes = ch.encode('utf_16_be')
338     for by in bytes:
339       self.write('\\%03o' % ord(by), 0)
340       
341 #
342 # Wrapper over output stream to write is desired encoding
343 #
344 class stream_encoder:
345
346   def __init__(self, stream, encoding):
347     """ Construct a wrapper by stream and encoding """
348     self.stream = stream
349     self.encode = codecs.getencoder(encoding)
350
351   def write(self, str):
352     """ Write string encoded """
353     self.stream.write(self.encode(str)[0])
354
355   def close(self):
356     """ Close underlying stream """
357     self.stream.close()
358