eric6/QScintilla/Exporters/ExporterHTML.py

changeset 6942
2602857055c5
parent 6645
ad476851d7e0
child 7192
a22eee00b052
equal deleted inserted replaced
6941:f99d60d6b59b 6942:2602857055c5
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing an exporter for HTML.
8 """
9
10 from __future__ import unicode_literals
11
12 try: # Only for Py2
13 import StringIO as io # __IGNORE_EXCEPTION__
14 except (ImportError, NameError):
15 import io # __IGNORE_WARNING__
16
17 # This code is a port of the C++ code found in SciTE 1.74
18 # Original code: Copyright 1998-2006 by Neil Hodgson <neilh@scintilla.org>
19
20 import os
21 import sys
22
23 from PyQt5.QtCore import Qt
24 from PyQt5.QtGui import QCursor, QFontInfo
25 from PyQt5.QtWidgets import QApplication
26 from PyQt5.Qsci import QsciScintilla
27
28 from E5Gui import E5MessageBox
29
30 from .ExporterBase import ExporterBase
31
32 import Preferences
33 import Utilities
34
35
36 class HTMLGenerator(object):
37 """
38 Class implementing an HTML generator for exporting source code.
39 """
40 def __init__(self, editor):
41 """
42 Constructor
43
44 @param editor reference to the editor object (QScintilla.Editor.Editor)
45 """
46 self.editor = editor
47
48 def generate(self, tabSize=4, useTabs=False, wysiwyg=True, folding=False,
49 onlyStylesUsed=False, titleFullPath=False):
50 """
51 Public method to generate HTML for the source editor.
52
53 @keyparam tabSize size of tabs (integer)
54 @keyparam useTabs flag indicating the use of tab characters (boolean)
55 @keyparam wysiwyg flag indicating colorization (boolean)
56 @keyparam folding flag indicating usage of fold markers
57 @keyparam onlyStylesUsed flag indicating to include only style
58 definitions for styles used in the source (boolean)
59 @keyparam titleFullPath flag indicating to include the full file path
60 in the title tag (boolean)
61 @return generated HTML text (string)
62 """
63 self.editor.recolor(0, -1)
64
65 lengthDoc = self.editor.length()
66 styleIsUsed = {}
67 if onlyStylesUsed:
68 for index in range(QsciScintilla.STYLE_MAX + 1):
69 styleIsUsed[index] = False
70 # check the used styles
71 pos = 0
72 while pos < lengthDoc:
73 styleIsUsed[self.editor.styleAt(pos) & 0x7F] = True
74 pos += 1
75 else:
76 for index in range(QsciScintilla.STYLE_MAX + 1):
77 styleIsUsed[index] = True
78 styleIsUsed[QsciScintilla.STYLE_DEFAULT] = True
79
80 html = \
81 '''<!DOCTYPE html PUBLIC "-//W3C//DTD''' \
82 ''' XHTML 1.0 Transitional//EN"\n''' \
83 ''' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">''' \
84 '''\n''' \
85 '''<html xmlns="http://www.w3.org/1999/xhtml">\n''' \
86 '''<head>\n'''
87 if titleFullPath:
88 html += '''<title>{0}</title>\n'''.format(
89 self.editor.getFileName())
90 else:
91 html += '''<title>{0}</title>\n'''.format(
92 os.path.basename(self.editor.getFileName()))
93 html += '''<meta name="Generator" content="eric6" />\n''' \
94 '''<meta http-equiv="Content-Type" ''' \
95 '''content="text/html; charset=utf-8" />\n'''
96 if folding:
97 html += \
98 '''<script language="JavaScript" type="text/javascript">\n''' \
99 '''<!--\n''' \
100 '''function symbol(id, sym) {\n''' \
101 ''' if (id.textContent == undefined) {\n''' \
102 ''' id.innerText = sym;\n''' \
103 ''' } else {\n''' \
104 ''' id.textContent = sym;\n''' \
105 ''' }\n''' \
106 '''}\n''' \
107 '''function toggle(id) {\n''' \
108 ''' var thislayer = document.getElementById('ln' + id);\n''' \
109 ''' id -= 1;\n''' \
110 ''' var togline = document.getElementById('hd' + id);\n''' \
111 ''' var togsym = document.getElementById('bt' + id);\n''' \
112 ''' if (thislayer.style.display == 'none') {\n''' \
113 ''' thislayer.style.display = 'block';\n''' \
114 ''' togline.style.textDecoration = 'none';\n''' \
115 ''' symbol(togsym, '- ');\n''' \
116 ''' } else {\n''' \
117 ''' thislayer.style.display = 'none';\n''' \
118 ''' togline.style.textDecoration = 'underline';\n''' \
119 ''' symbol(togsym, '+ ');\n''' \
120 ''' }\n''' \
121 '''}\n''' \
122 '''//-->\n''' \
123 '''</script>\n'''
124
125 lex = self.editor.getLexer()
126 if lex:
127 bgColour = lex.paper(QsciScintilla.STYLE_DEFAULT).name()
128 else:
129 bgColour = self.editor.paper().name()
130
131 html += '''<style type="text/css">\n'''
132 if lex:
133 istyle = 0
134 while istyle <= QsciScintilla.STYLE_MAX:
135 if (istyle <= QsciScintilla.STYLE_DEFAULT or
136 istyle > QsciScintilla.STYLE_LASTPREDEFINED) and \
137 styleIsUsed[istyle]:
138 if lex.description(istyle) or \
139 istyle == QsciScintilla.STYLE_DEFAULT:
140 font = lex.font(istyle)
141 colour = lex.color(istyle)
142 paper = lex.paper(istyle)
143 if istyle == QsciScintilla.STYLE_DEFAULT:
144 html += '''span {\n'''
145 else:
146 html += '''.S{0:d} {{\n'''.format(istyle)
147 if font.italic():
148 html += ''' font-style: italic;\n'''
149 if font.bold():
150 html += ''' font-weight: bold;\n'''
151 if wysiwyg:
152 html += ''' font-family: '{0}';\n'''.format(
153 font.family())
154 html += ''' color: {0};\n'''.format(colour.name())
155 if istyle != QsciScintilla.STYLE_DEFAULT and \
156 bgColour != paper.name():
157 html += ''' background: {0};\n'''.format(
158 paper.name())
159 html += ''' text-decoration: inherit;\n'''
160 if wysiwyg:
161 html += ''' font-size: {0:d}pt;\n'''.format(
162 QFontInfo(font).pointSize())
163 html += '''}\n'''
164 else:
165 styleIsUsed[istyle] = False
166 istyle += 1
167 else:
168 colour = self.editor.color()
169 paper = self.editor.paper()
170 font = Preferences.getEditorOtherFonts("DefaultFont")
171 html += '''.S0 {\n'''
172 if font.italic():
173 html += ''' font-style: italic;\n'''
174 if font.bold():
175 html += ''' font-weight: bold;\n'''
176 if wysiwyg:
177 html += ''' font-family: '{0}';\n'''.format(font.family())
178 html += ''' color: {0};\n'''.format(colour.name())
179 if bgColour != paper.name():
180 html += ''' background: {0};\n'''.format(paper.name())
181 html += ''' text-decoration: inherit;\n'''
182 if wysiwyg:
183 html += ''' font-size: {0:d}pt;\n'''.format(
184 QFontInfo(font).pointSize())
185 html += '''}\n'''
186 html += '''</style>\n'''
187 html += '''</head>\n'''
188
189 html += '''<body bgcolor="{0}">\n'''.format(bgColour)
190 line = self.editor.lineAt(0)
191 level = self.editor.foldLevelAt(line) - QsciScintilla.SC_FOLDLEVELBASE
192 levelStack = [level]
193 styleCurrent = self.editor.styleAt(0)
194 inStyleSpan = False
195 inFoldSpan = False
196 # Global span for default attributes
197 if wysiwyg:
198 html += '''<span>'''
199 else:
200 html += '''<pre>'''
201
202 if folding:
203 if self.editor.foldFlagsAt(line) & \
204 QsciScintilla.SC_FOLDLEVELHEADERFLAG:
205 html += '''<span id="hd{0:d}" onclick="toggle('{1:d}')">'''\
206 .format(line, line + 1)
207 html += '''<span id="bt{0:d}">- </span>'''.format(line)
208 inFoldSpan = True
209 else:
210 html += '''&nbsp; '''
211
212 if styleIsUsed[styleCurrent]:
213 html += '''<span class="S{0:0d}">'''.format(styleCurrent)
214 inStyleSpan = True
215
216 column = 0
217 pos = 0
218 utf8 = self.editor.isUtf8()
219 utf8Ch = b""
220 utf8Len = 0
221
222 while pos < lengthDoc:
223 ch = self.editor.byteAt(pos)
224 style = self.editor.styleAt(pos)
225 if style != styleCurrent:
226 if inStyleSpan:
227 html += '''</span>'''
228 inStyleSpan = False
229 if ch not in [b'\r', b'\n']: # no need of a span for the EOL
230 if styleIsUsed[style]:
231 html += '''<span class="S{0:d}">'''.format(style)
232 inStyleSpan = True
233 styleCurrent = style
234
235 if ch == b' ':
236 if wysiwyg:
237 prevCh = b''
238 if column == 0:
239 # at start of line, must put a &nbsp;
240 # because regular space will be collapsed
241 prevCh = b' '
242 while pos < lengthDoc and self.editor.byteAt(pos) == b' ':
243 if prevCh != b' ':
244 html += ' '
245 else:
246 html += '''&nbsp;'''
247 prevCh = self.editor.byteAt(pos)
248 pos += 1
249 column += 1
250 pos -= 1
251 # the last incrementation will be done by the outer loop
252 else:
253 html += ' '
254 column += 1
255 elif ch == b'\t':
256 ts = tabSize - (column % tabSize)
257 if wysiwyg:
258 html += '''&nbsp;''' * ts
259 column += ts
260 else:
261 if useTabs:
262 html += '\t'
263 column += 1
264 else:
265 html += ' ' * ts
266 column += ts
267 elif ch in [b'\r', b'\n']:
268 if inStyleSpan:
269 html += '''</span>'''
270 inStyleSpan = False
271 if inFoldSpan:
272 html += '''</span>'''
273 inFoldSpan = False
274 if ch == b'\r' and self.editor.byteAt(pos + 1) == b'\n':
275 pos += 1 # CR+LF line ending, skip the "extra" EOL char
276 column = 0
277 if wysiwyg:
278 html += '''<br />'''
279
280 styleCurrent = self.editor.styleAt(pos + 1)
281 if folding:
282 line = self.editor.lineAt(pos + 1)
283 newLevel = self.editor.foldLevelAt(line)
284
285 if newLevel < level:
286 while levelStack[-1] > newLevel:
287 html += '''</span>'''
288 levelStack.pop()
289 html += '\n' # here to get clean code
290 if newLevel > level:
291 html += '''<span id="ln{0:d}">'''.format(line)
292 levelStack.append(newLevel)
293 if self.editor.foldFlagsAt(line) & \
294 QsciScintilla.SC_FOLDLEVELHEADERFLAG:
295 html += \
296 '''<span id="hd{0:d}"''' \
297 ''' onclick="toggle('{1:d}')">''' \
298 .format(line, line + 1)
299 html += '''<span id="bt{0:d}">- </span>'''.format(line)
300 inFoldSpan = True
301 else:
302 html += '''&nbsp; '''
303 level = newLevel
304 else:
305 html += '\n'
306
307 if styleIsUsed[styleCurrent] and \
308 self.editor.byteAt(pos + 1) not in [b'\r', b'\n']:
309 # We know it's the correct next style,
310 # but no (empty) span for an empty line
311 html += '''<span class="S{0:0d}">'''.format(styleCurrent)
312 inStyleSpan = True
313 else:
314 if ch == b'<':
315 html += '''&lt;'''
316 elif ch == b'>':
317 html += '''&gt'''
318 elif ch == b'&':
319 html += '''&amp;'''
320 else:
321 if ord(ch) > 127 and utf8:
322 utf8Ch += ch
323 if utf8Len == 0:
324 if (utf8Ch[0] & 0xF0) == 0xF0:
325 utf8Len = 4
326 elif (utf8Ch[0] & 0xE0) == 0xE0:
327 utf8Len = 3
328 elif (utf8Ch[0] & 0xC0) == 0xC0:
329 utf8Len = 2
330 column -= 1 # will be incremented again later
331 elif len(utf8Ch) == utf8Len:
332 ch = utf8Ch.decode('utf8')
333 html += Utilities.html_encode(ch)
334 utf8Ch = b""
335 utf8Len = 0
336 else:
337 column -= 1 # will be incremented again later
338 else:
339 html += ch.decode()
340 column += 1
341
342 pos += 1
343
344 if inStyleSpan:
345 html += '''</span>'''
346
347 if folding:
348 while levelStack:
349 html += '''</span>'''
350 levelStack.pop()
351
352 if wysiwyg:
353 html += '''</span>'''
354 else:
355 html += '''</pre>'''
356
357 html += '''</body>\n</html>\n'''
358
359 return html
360
361
362 class ExporterHTML(ExporterBase):
363 """
364 Class implementing an exporter for HTML.
365 """
366 def __init__(self, editor, parent=None):
367 """
368 Constructor
369
370 @param editor reference to the editor object (QScintilla.Editor.Editor)
371 @param parent parent object of the exporter (QObject)
372 """
373 ExporterBase.__init__(self, editor, parent)
374
375 def exportSource(self):
376 """
377 Public method performing the export.
378 """
379 filename = self._getFileName(self.tr("HTML Files (*.html)"))
380 if not filename:
381 return
382
383 try:
384 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
385 QApplication.processEvents()
386
387 fn = self.editor.getFileName()
388 if fn:
389 extension = os.path.normcase(os.path.splitext(fn)[1][1:])
390 else:
391 extension = ""
392
393 if extension in \
394 Preferences.getEditor("PreviewMarkdownFileNameExtensions") or \
395 self.editor.getLanguage().lower() == "markdown":
396 # export markdown to HTML
397 html = self.__generateFromMarkdown()
398 elif extension in \
399 Preferences.getEditor("PreviewRestFileNameExtensions") or \
400 self.editor.getLanguage().lower() == "restructuredtext":
401 # export ReST to HTML
402 html = self.__generateFromReSTDocutils()
403 else:
404 tabSize = self.editor.getEditorConfig("TabWidth")
405 if tabSize == 0:
406 tabSize = 4
407 wysiwyg = Preferences.getEditorExporter("HTML/WYSIWYG")
408 folding = Preferences.getEditorExporter("HTML/Folding")
409 onlyStylesUsed = Preferences.getEditorExporter(
410 "HTML/OnlyStylesUsed")
411 titleFullPath = Preferences.getEditorExporter(
412 "HTML/FullPathAsTitle")
413 tabs = Preferences.getEditorExporter("HTML/UseTabs")
414
415 generator = HTMLGenerator(self.editor)
416 html = generator.generate(
417 tabSize=tabSize,
418 useTabs=tabs,
419 wysiwyg=wysiwyg,
420 folding=folding,
421 onlyStylesUsed=onlyStylesUsed,
422 titleFullPath=titleFullPath
423 )
424
425 if html:
426 try:
427 f = open(filename, "w", encoding="utf-8")
428 f.write(html)
429 f.close()
430 except IOError as err:
431 QApplication.restoreOverrideCursor()
432 E5MessageBox.critical(
433 self.editor,
434 self.tr("Export source"),
435 self.tr(
436 """<p>The source could not be exported to"""
437 """ <b>{0}</b>.</p><p>Reason: {1}</p>""")
438 .format(filename, str(err)))
439 else:
440 QApplication.restoreOverrideCursor()
441 E5MessageBox.critical(
442 self.editor,
443 self.tr("Export source"),
444 self.tr(
445 """<p>The source could not be exported to"""
446 """ <b>{0}</b>.</p><p>Reason: No HTML code"""
447 """ generated.</p>""")
448 .format(filename))
449 finally:
450 QApplication.restoreOverrideCursor()
451
452 def __generateFromReSTDocutils(self):
453 """
454 Private method to convert ReST text into HTML using 'docutils'.
455
456 @return processed HTML (string)
457 """
458 if 'sphinx' in sys.modules:
459 # Make sure any Sphinx polution of docutils has been removed.
460 unloadKeys = [k for k in sys.modules.keys()
461 if k.startswith(('docutils', 'sphinx'))]
462 for key in unloadKeys:
463 sys.modules.pop(key)
464
465 try:
466 import docutils.core # __IGNORE_EXCEPTION__
467 except ImportError:
468 E5MessageBox.critical(
469 self.editor,
470 self.tr("Export source"),
471 self.tr(
472 """<p>ReStructuredText export requires the"""
473 """ <b>python-docutils</b> package.<br/>Install it with"""
474 """ your package manager, 'pip install docutils' or see"""
475 """ <a href="http://pypi.python.org/pypi/docutils">"""
476 """this page.</a></p>""")
477 )
478 return ""
479
480 htmlFormat = Preferences.getEditor(
481 "PreviewRestDocutilsHTMLFormat").lower()
482 # redirect sys.stderr because we are not interested in it here
483 origStderr = sys.stderr
484 sys.stderr = io.StringIO()
485 html = docutils.core.publish_string(
486 self.editor.text(), writer_name=htmlFormat).decode("utf-8")
487 sys.stderr = origStderr
488 return html
489
490 def __generateFromMarkdown(self):
491 """
492 Private method to convert Markdown text into HTML.
493
494 @return processed HTML
495 @rtype str
496 """
497 try:
498 import markdown # __IGNORE_EXCEPTION__
499 except ImportError:
500 E5MessageBox.critical(
501 self.editor,
502 self.tr("Export source"),
503 self.tr(
504 """<p>Markdown export requires the <b>python-markdown"""
505 """</b> package.<br/>Install it with your package"""
506 """ manager, 'pip install docutils' or see """
507 """<a href="http://pythonhosted.org/Markdown/install"""
508 """.html"> installation instructions.</a></p>""")
509 )
510 return ""
511
512 try:
513 import mdx_mathjax # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
514 except ImportError:
515 # mathjax doesn't require import statement if installed as
516 # extension
517 pass
518
519 if Preferences.getEditor("PreviewMarkdownNLtoBR"):
520 extensions = ['fenced_code', 'nl2br', 'extra']
521 else:
522 extensions = ['fenced_code', 'extra']
523
524 # version 2.0 supports only extension names, not instances
525 if markdown.version_info[0] > 2 or \
526 (markdown.version_info[0] == 2 and
527 markdown.version_info[1] > 0):
528 class _StrikeThroughExtension(markdown.Extension):
529 """
530 Class is placed here, because it depends on imported markdown,
531 and markdown import is lazy.
532
533 (see http://achinghead.com/
534 python-markdown-adding-insert-delete.html this page for
535 details)
536 """
537 DEL_RE = r'(~~)(.*?)~~'
538
539 def extendMarkdown(self, md, md_globals):
540 # Create the del pattern
541 del_tag = markdown.inlinepatterns.SimpleTagPattern(
542 self.DEL_RE, 'del')
543 # Insert del pattern into markdown parser
544 md.inlinePatterns.add('del', del_tag, '>not_strong')
545
546 extensions.append(_StrikeThroughExtension())
547
548 htmlFormat = Preferences.getEditor("PreviewMarkdownHTMLFormat").lower()
549 try:
550 body = markdown.markdown(self.editor.text(),
551 extensions=extensions + ['mathjax'],
552 output_format=htmlFormat)
553 except (ImportError, ValueError):
554 # markdown raises ValueError or ImportError, depends on version
555 # It is not clear, how to distinguish missing mathjax from other
556 # errors. So keep going without mathjax.
557 body = markdown.markdown(self.editor.text(),
558 extensions=extensions,
559 output_format=htmlFormat)
560
561 if htmlFormat == "xhtml1":
562 head = \
563 '''<!DOCTYPE html PUBLIC "-//W3C//DTD''' \
564 ''' XHTML 1.0 Transitional//EN"\n''' \
565 ''' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional''' \
566 '''.dtd">\n''' \
567 '''<html xmlns="http://www.w3.org/1999/xhtml">\n'''
568 elif htmlFormat == "html5":
569 head = \
570 '''<!DOCTYPE html>\n''' \
571 '''<html lang="EN">\n'''
572 else:
573 head = '<html lang="EN">\n'
574 head += '''<head>\n'''
575 if Preferences.getEditorExporter("HTML/FullPathAsTitle"):
576 head += '''<title>{0}</title>\n'''.format(
577 self.editor.getFileName())
578 else:
579 head += '''<title>{0}</title>\n'''.format(
580 os.path.basename(self.editor.getFileName()))
581 head += '''<meta name="Generator" content="eric6" />\n''' \
582 '''<meta http-equiv="Content-Type" ''' \
583 '''content="text/html; charset=utf-8" />\n''' \
584 '''</head>\n''' \
585 '''<body>\n'''
586
587 foot = '''\n</body>\n</html>\n'''
588
589 return head + body + foot

eric ide

mercurial