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