|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2007 - 2022 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing an exporter for TeX. |
|
8 """ |
|
9 |
|
10 # This code is a port of the C++ code found in SciTE 1.74 |
|
11 # Original code: Copyright 1998-2006 by Neil Hodgson <neilh@scintilla.org> |
|
12 |
|
13 import os |
|
14 |
|
15 from PyQt6.Qsci import QsciScintilla |
|
16 |
|
17 from EricWidgets import EricMessageBox |
|
18 from EricGui.EricOverrideCursor import EricOverrideCursor |
|
19 |
|
20 from .ExporterBase import ExporterBase |
|
21 |
|
22 import Preferences |
|
23 |
|
24 |
|
25 class ExporterTEX(ExporterBase): |
|
26 """ |
|
27 Class implementing an exporter for TeX. |
|
28 """ |
|
29 CHARZ = 26 |
|
30 |
|
31 def __init__(self, editor, parent=None): |
|
32 """ |
|
33 Constructor |
|
34 |
|
35 @param editor reference to the editor object (QScintilla.Editor.Editor) |
|
36 @param parent parent object of the exporter (QObject) |
|
37 """ |
|
38 ExporterBase.__init__(self, editor, parent) |
|
39 |
|
40 def __getTexRGB(self, color): |
|
41 """ |
|
42 Private method to convert a color object to a TeX color string. |
|
43 |
|
44 @param color color object to convert (QColor) |
|
45 @return TeX color string (string) |
|
46 """ |
|
47 # texcolor[rgb]{0,0.5,0}{....} |
|
48 rf = color.red() / 256.0 |
|
49 gf = color.green() / 256.0 |
|
50 bf = color.blue() / 256.0 |
|
51 |
|
52 # avoid breakage due to locale setting |
|
53 r = int(rf * 10 + 0.5) |
|
54 g = int(gf * 10 + 0.5) |
|
55 b = int(bf * 10 + 0.5) |
|
56 |
|
57 return "{0:d}.{1:d}, {2:d}.{3:d}, {4:d}.{5:d}".format( |
|
58 r // 10, r % 10, g // 10, g % 10, b // 10, b % 10) |
|
59 |
|
60 def __texStyle(self, style): |
|
61 """ |
|
62 Private method to calculate a style name string for a given style |
|
63 number. |
|
64 |
|
65 @param style style number (integer) |
|
66 @return style name string (string) |
|
67 """ |
|
68 buf = "" |
|
69 if style >= 0: |
|
70 start = ord('a') |
|
71 else: |
|
72 start = ord('A') |
|
73 style = abs(style) |
|
74 |
|
75 if style == 0: |
|
76 buf = "a" |
|
77 else: |
|
78 while style > 0: |
|
79 buf += chr(start + (style % self.CHARZ)) |
|
80 style //= self.CHARZ |
|
81 return buf |
|
82 |
|
83 def __defineTexStyle(self, font, color, paper, file, istyle): |
|
84 """ |
|
85 Private method to define a new TeX style. |
|
86 |
|
87 @param font the font to be used (QFont) |
|
88 @param color the foreground color to be used (QColor) |
|
89 @param paper the background color to be used (QColor) |
|
90 @param file reference to the open file to write to (file object) |
|
91 @param istyle style number (integer) |
|
92 """ |
|
93 closing_brackets = 3 |
|
94 file.write( |
|
95 "\\newcommand{{\\eric{0}}}[1]{{\\noindent{{\\ttfamily{{".format( |
|
96 self.__texStyle(istyle))) |
|
97 if font.italic(): |
|
98 file.write("\\textit{") |
|
99 closing_brackets += 1 |
|
100 if font.bold(): |
|
101 file.write("\\textbf{") |
|
102 closing_brackets += 1 |
|
103 if color != self.defaultColor: |
|
104 file.write( |
|
105 "\\textcolor[rgb]{{{0}}}{{".format(self.__getTexRGB(color))) |
|
106 closing_brackets += 1 |
|
107 if paper != self.defaultPaper: |
|
108 file.write( |
|
109 "\\colorbox[rgb]{{{0}}}{{".format(self.__getTexRGB(paper))) |
|
110 closing_brackets += 1 |
|
111 file.write("#1{0}\n".format('}' * closing_brackets)) |
|
112 |
|
113 def exportSource(self): |
|
114 """ |
|
115 Public method performing the export. |
|
116 """ |
|
117 filename = self._getFileName(self.tr("TeX Files (*.tex)")) |
|
118 if not filename: |
|
119 return |
|
120 |
|
121 self.editor.recolor(0, -1) |
|
122 |
|
123 tabSize = self.editor.getEditorConfig("TabWidth") |
|
124 if tabSize == 0: |
|
125 tabSize = 4 |
|
126 |
|
127 onlyStylesUsed = Preferences.getEditorExporter( |
|
128 "TeX/OnlyStylesUsed") |
|
129 titleFullPath = Preferences.getEditorExporter( |
|
130 "TeX/FullPathAsTitle") |
|
131 |
|
132 lex = self.editor.getLexer() |
|
133 self.defaultPaper = ( |
|
134 lex and |
|
135 lex.paper(QsciScintilla.STYLE_DEFAULT) or |
|
136 self.editor.paper().name() |
|
137 ) |
|
138 self.defaultColor = ( |
|
139 lex and |
|
140 lex.color(QsciScintilla.STYLE_DEFAULT) or |
|
141 self.editor.color().name() |
|
142 ) |
|
143 self.defaultFont = ( |
|
144 lex and |
|
145 lex.color(QsciScintilla.STYLE_DEFAULT) or |
|
146 Preferences.getEditorOtherFonts("DefaultFont") |
|
147 ) |
|
148 |
|
149 lengthDoc = self.editor.length() |
|
150 styleIsUsed = {} |
|
151 if onlyStylesUsed: |
|
152 for index in range(QsciScintilla.STYLE_MAX + 1): |
|
153 styleIsUsed[index] = False |
|
154 # check the used styles |
|
155 pos = 0 |
|
156 while pos < lengthDoc: |
|
157 styleIsUsed[self.editor.styleAt(pos) & 0x7F] = True |
|
158 pos += 1 |
|
159 else: |
|
160 for index in range(QsciScintilla.STYLE_MAX + 1): |
|
161 styleIsUsed[index] = True |
|
162 styleIsUsed[QsciScintilla.STYLE_DEFAULT] = True |
|
163 |
|
164 with EricOverrideCursor(), open(filename, "w", encoding="utf-8") as f: |
|
165 try: |
|
166 f.write("\\documentclass[a4paper]{article}\n") |
|
167 f.write("\\usepackage[a4paper,margin=1.5cm]{geometry}\n") |
|
168 f.write("\\usepackage[T1]{fontenc}\n") |
|
169 f.write("\\usepackage{color}\n") |
|
170 f.write("\\usepackage{alltt}\n") |
|
171 f.write("\\usepackage{times}\n") |
|
172 if self.editor.isUtf8(): |
|
173 f.write("\\usepackage[utf8]{inputenc}\n") |
|
174 else: |
|
175 f.write("\\usepackage[latin1]{inputenc}\n") |
|
176 |
|
177 if lex: |
|
178 istyle = 0 |
|
179 while istyle <= QsciScintilla.STYLE_MAX: |
|
180 if ( |
|
181 (istyle <= QsciScintilla.STYLE_DEFAULT or |
|
182 istyle > QsciScintilla.STYLE_LASTPREDEFINED) and |
|
183 styleIsUsed[istyle] and |
|
184 (lex.description(istyle) or |
|
185 istyle == QsciScintilla.STYLE_DEFAULT) |
|
186 ): |
|
187 font = lex.font(istyle) |
|
188 colour = lex.color(istyle) |
|
189 paper = lex.paper(istyle) |
|
190 |
|
191 self.__defineTexStyle( |
|
192 font, colour, paper, f, istyle) |
|
193 # get substyles |
|
194 subs_start, subs_count = ( |
|
195 self.editor.getSubStyleRange(istyle) |
|
196 ) |
|
197 for subs_idx in range(subs_count): |
|
198 font = lex.font(subs_start + subs_idx) |
|
199 colour = lex.color( |
|
200 subs_start + subs_idx) |
|
201 paper = lex.paper( |
|
202 subs_start + subs_idx) |
|
203 |
|
204 self.__defineTexStyle( |
|
205 font, colour, paper, f, |
|
206 subs_idx - subs_start) |
|
207 |
|
208 istyle += 1 |
|
209 else: |
|
210 colour = self.editor.color() |
|
211 paper = self.editor.paper() |
|
212 font = Preferences.getEditorOtherFonts("DefaultFont") |
|
213 |
|
214 self.__defineTexStyle(font, colour, paper, f, 0) |
|
215 self.__defineTexStyle(font, colour, paper, f, |
|
216 QsciScintilla.STYLE_DEFAULT) |
|
217 |
|
218 f.write("\\begin{document}\n\n") |
|
219 title = ( |
|
220 self.editor.getFileName() |
|
221 if titleFullPath else |
|
222 os.path.basename(self.editor.getFileName()) |
|
223 ) |
|
224 f.write( |
|
225 "Source File: {0}\n\n\\noindent\n\\tiny{{\n" |
|
226 .format(title.replace('_', '\\_'))) |
|
227 |
|
228 styleCurrent = self.editor.styleAt(0) |
|
229 f.write("\\eric{0}{{" |
|
230 .format(self.__texStyle(styleCurrent))) |
|
231 |
|
232 lineIdx = 0 |
|
233 pos = 0 |
|
234 utf8 = self.editor.isUtf8() |
|
235 utf8Ch = b"" |
|
236 utf8Len = 0 |
|
237 |
|
238 while pos < lengthDoc: |
|
239 ch = self.editor.byteAt(pos) |
|
240 style = self.editor.styleAt(pos) |
|
241 if style != styleCurrent: |
|
242 # new style |
|
243 f.write( |
|
244 "}}\n\\eric{0}{{".format( |
|
245 self.__texStyle(style))) |
|
246 styleCurrent = style |
|
247 |
|
248 if ch == b'\t': |
|
249 ts = tabSize - (lineIdx % tabSize) |
|
250 lineIdx += ts - 1 |
|
251 f.write("\\hspace*{{{0:d}em}}".format(ts)) |
|
252 elif ch == b'\\': |
|
253 f.write("{\\textbackslash}") |
|
254 elif ch in [b'>', b'<', b'@']: |
|
255 f.write("${0}$".format(ch.decode())) |
|
256 elif ch in [b'{', b'}', b'^', b'_', b'&', b'$', b'#', |
|
257 b'%', b'~']: |
|
258 f.write("\\{0}".format(ch.decode())) |
|
259 elif ch in [b'\r', b'\n']: |
|
260 lineIdx = -1 # because incremented below |
|
261 if ( |
|
262 ch == b'\r' and |
|
263 self.editor.byteAt(pos + 1) == b'\n' |
|
264 ): |
|
265 pos += 1 # skip the LF |
|
266 styleCurrent = self.editor.styleAt(pos + 1) |
|
267 f.write("}} \\\\\n\\eric{0}{{".format( |
|
268 self.__texStyle(styleCurrent))) |
|
269 elif ch == b' ': |
|
270 if self.editor.byteAt(pos + 1) == b' ': |
|
271 f.write("{\\hspace*{1em}}") |
|
272 else: |
|
273 f.write(' ') |
|
274 else: |
|
275 if ord(ch) > 127 and utf8: |
|
276 utf8Ch += ch |
|
277 if utf8Len == 0: |
|
278 if (utf8Ch[0] & 0xF0) == 0xF0: |
|
279 utf8Len = 4 |
|
280 elif (utf8Ch[0] & 0xE0) == 0xE0: |
|
281 utf8Len = 3 |
|
282 elif (utf8Ch[0] & 0xC0) == 0xC0: |
|
283 utf8Len = 2 |
|
284 elif len(utf8Ch) == utf8Len: |
|
285 ch = utf8Ch.decode('utf8') |
|
286 f.write(ch) |
|
287 utf8Ch = b"" |
|
288 utf8Len = 0 |
|
289 else: |
|
290 f.write(ch.decode()) |
|
291 lineIdx += 1 |
|
292 pos += 1 |
|
293 |
|
294 # close last empty style macros and document too |
|
295 f.write("}\n} %end tiny\n\n\\end{document}\n") |
|
296 except OSError as err: |
|
297 EricMessageBox.critical( |
|
298 self.editor, |
|
299 self.tr("Export source"), |
|
300 self.tr( |
|
301 """<p>The source could not be exported to""" |
|
302 """ <b>{0}</b>.</p><p>Reason: {1}</p>""") |
|
303 .format(filename, str(err))) |