eric7/QScintilla/Exporters/ExporterHTML.py

branch
eric7
changeset 8312
800c432b34c8
parent 8259
2bbec88047dd
child 8318
962bce857696
equal deleted inserted replaced
8311:4e8b98454baa 8312:800c432b34c8
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2007 - 2021 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing an exporter for HTML.
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 import sys
15 import io
16 import contextlib
17
18 from PyQt5.QtGui import QFontInfo
19 from PyQt5.QtWidgets import QInputDialog
20 from PyQt5.Qsci import QsciScintilla
21
22 from E5Gui import E5MessageBox
23 from E5Gui.E5OverrideCursor import E5OverrideCursor
24
25 from .ExporterBase import ExporterBase
26
27 import Preferences
28 import Utilities
29
30
31 class HTMLGenerator:
32 """
33 Class implementing an HTML generator for exporting source code.
34 """
35 def __init__(self, editor):
36 """
37 Constructor
38
39 @param editor reference to the editor object (QScintilla.Editor.Editor)
40 """
41 self.editor = editor
42
43 def generate(self, tabSize=4, useTabs=False, wysiwyg=True, folding=False,
44 onlyStylesUsed=False, titleFullPath=False):
45 """
46 Public method to generate HTML for the source editor.
47
48 @param tabSize size of tabs (integer)
49 @param useTabs flag indicating the use of tab characters (boolean)
50 @param wysiwyg flag indicating colorization (boolean)
51 @param folding flag indicating usage of fold markers
52 @param onlyStylesUsed flag indicating to include only style
53 definitions for styles used in the source (boolean)
54 @param titleFullPath flag indicating to include the full file path
55 in the title tag (boolean)
56 @return generated HTML text (string)
57 """
58 self.editor.recolor(0, -1)
59
60 lengthDoc = self.editor.length()
61 styleIsUsed = {}
62 if onlyStylesUsed:
63 for index in range(QsciScintilla.STYLE_MAX + 1):
64 styleIsUsed[index] = False
65 # check the used styles
66 pos = 0
67 while pos < lengthDoc:
68 styleIsUsed[self.editor.styleAt(pos) & 0x7F] = True
69 pos += 1
70 else:
71 for index in range(QsciScintilla.STYLE_MAX + 1):
72 styleIsUsed[index] = True
73 styleIsUsed[QsciScintilla.STYLE_DEFAULT] = True
74
75 html = (
76 '''<!DOCTYPE html PUBLIC "-//W3C//DTD'''
77 ''' XHTML 1.0 Transitional//EN"\n'''
78 ''' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">'''
79 '''\n'''
80 '''<html xmlns="http://www.w3.org/1999/xhtml">\n'''
81 '''<head>\n'''
82 )
83 if titleFullPath:
84 html += '''<title>{0}</title>\n'''.format(
85 self.editor.getFileName())
86 else:
87 html += '''<title>{0}</title>\n'''.format(
88 os.path.basename(self.editor.getFileName()))
89 html += (
90 '''<meta name="Generator" content="eric" />\n'''
91 '''<meta http-equiv="Content-Type" '''
92 '''content="text/html; charset=utf-8" />\n'''
93 )
94 if folding:
95 html += (
96 '''<script language="JavaScript" type="text/javascript">\n'''
97 '''<!--\n'''
98 '''function symbol(id, sym) {\n'''
99 ''' if (id.textContent == undefined) {\n'''
100 ''' id.innerText = sym;\n'''
101 ''' } else {\n'''
102 ''' id.textContent = sym;\n'''
103 ''' }\n'''
104 '''}\n'''
105 '''function toggle(id) {\n'''
106 ''' var thislayer = document.getElementById('ln' + id);\n'''
107 ''' id -= 1;\n'''
108 ''' var togline = document.getElementById('hd' + id);\n'''
109 ''' var togsym = document.getElementById('bt' + id);\n'''
110 ''' if (thislayer.style.display == 'none') {\n'''
111 ''' thislayer.style.display = 'block';\n'''
112 ''' togline.style.textDecoration = 'none';\n'''
113 ''' symbol(togsym, '- ');\n'''
114 ''' } else {\n'''
115 ''' thislayer.style.display = 'none';\n'''
116 ''' togline.style.textDecoration = 'underline';\n'''
117 ''' symbol(togsym, '+ ');\n'''
118 ''' }\n'''
119 '''}\n'''
120 '''//-->\n'''
121 '''</script>\n'''
122 )
123
124 lex = self.editor.getLexer()
125 bgColour = (
126 lex.paper(QsciScintilla.STYLE_DEFAULT).name()
127 if lex else
128 self.editor.paper().name()
129 )
130
131 html += '''<style type="text/css">\n'''
132 if lex:
133 istyle = 0
134 while istyle <= QsciScintilla.STYLE_MAX:
135 if (
136 (istyle <= QsciScintilla.STYLE_DEFAULT or
137 istyle > QsciScintilla.STYLE_LASTPREDEFINED) and
138 styleIsUsed[istyle]
139 ):
140 if (
141 lex.description(istyle) or
142 istyle == QsciScintilla.STYLE_DEFAULT
143 ):
144 font = lex.font(istyle)
145 colour = lex.color(istyle)
146 paper = lex.paper(istyle)
147 if istyle == QsciScintilla.STYLE_DEFAULT:
148 html += '''span {\n'''
149 else:
150 html += '''.S{0:d} {{\n'''.format(istyle)
151 if font.italic():
152 html += ''' font-style: italic;\n'''
153 if font.bold():
154 html += ''' font-weight: bold;\n'''
155 if wysiwyg:
156 html += ''' font-family: '{0}';\n'''.format(
157 font.family())
158 html += ''' color: {0};\n'''.format(colour.name())
159 if (
160 istyle != QsciScintilla.STYLE_DEFAULT and
161 bgColour != paper.name()
162 ):
163 html += ''' background: {0};\n'''.format(
164 paper.name())
165 html += ''' text-decoration: inherit;\n'''
166 if wysiwyg:
167 html += ''' font-size: {0:d}pt;\n'''.format(
168 QFontInfo(font).pointSize())
169 html += '''}\n'''
170
171 # get substyles
172 subs_start, subs_count = self.editor.getSubStyleRange(
173 istyle)
174 for subs_idx in range(subs_count):
175 styleIsUsed[subs_idx - subs_start] = True
176 font = lex.font(subs_start + subs_idx)
177 colour = lex.color(subs_start + subs_idx)
178 paper = lex.paper(subs_start + subs_idx)
179 html += '.S{0:d} {{\n'.format(
180 subs_idx - subs_start)
181 if font.italic():
182 html += ' font-style: italic;\n'
183 if font.bold():
184 html += ' font-weight: bold;\n'
185 if wysiwyg:
186 html += " font-family: '{0}';\n".format(
187 font.family())
188 html += ' color: {0};\n'.format(colour.name())
189 if wysiwyg:
190 html += ' font-size: {0:d}pt;\n'.format(
191 QFontInfo(font).pointSize())
192 html += '}\n'
193 # __IGNORE_WARNING_Y113__
194 else:
195 styleIsUsed[istyle] = False
196 istyle += 1
197 else:
198 colour = self.editor.color()
199 paper = self.editor.paper()
200 font = Preferences.getEditorOtherFonts("DefaultFont")
201 html += '''.S0 {\n'''
202 if font.italic():
203 html += ''' font-style: italic;\n'''
204 if font.bold():
205 html += ''' font-weight: bold;\n'''
206 if wysiwyg:
207 html += ''' font-family: '{0}';\n'''.format(font.family())
208 html += ''' color: {0};\n'''.format(colour.name())
209 if bgColour != paper.name():
210 html += ''' background: {0};\n'''.format(paper.name())
211 html += ''' text-decoration: inherit;\n'''
212 if wysiwyg:
213 html += ''' font-size: {0:d}pt;\n'''.format(
214 QFontInfo(font).pointSize())
215 html += '''}\n'''
216 html += '''</style>\n'''
217 html += '''</head>\n'''
218
219 html += '''<body bgcolor="{0}">\n'''.format(bgColour)
220 line = self.editor.lineAt(0)
221 level = self.editor.foldLevelAt(line) - QsciScintilla.SC_FOLDLEVELBASE
222 levelStack = [level]
223 styleCurrent = self.editor.styleAt(0)
224 inStyleSpan = False
225 inFoldSpan = False
226 # Global span for default attributes
227 if wysiwyg:
228 html += '''<span>'''
229 else:
230 html += '''<pre>'''
231
232 if folding:
233 if (
234 self.editor.foldFlagsAt(line) &
235 QsciScintilla.SC_FOLDLEVELHEADERFLAG
236 ):
237 html += (
238 '''<span id="hd{0:d}" onclick="toggle('{1:d}')">'''
239 ).format(line, line + 1)
240 html += '''<span id="bt{0:d}">- </span>'''.format(line)
241 inFoldSpan = True
242 else:
243 html += '''&nbsp; '''
244
245 if styleIsUsed[styleCurrent]:
246 html += '''<span class="S{0:0d}">'''.format(styleCurrent)
247 inStyleSpan = True
248
249 column = 0
250 pos = 0
251 utf8 = self.editor.isUtf8()
252 utf8Ch = b""
253 utf8Len = 0
254
255 while pos < lengthDoc:
256 ch = self.editor.byteAt(pos)
257 style = self.editor.styleAt(pos)
258 if style != styleCurrent:
259 if inStyleSpan:
260 html += '''</span>'''
261 inStyleSpan = False
262 if ch not in [b'\r', b'\n']: # no need of a span for the EOL
263 if styleIsUsed[style]:
264 html += '''<span class="S{0:d}">'''.format(style)
265 inStyleSpan = True
266 styleCurrent = style
267
268 if ch == b' ':
269 if wysiwyg:
270 prevCh = b''
271 if column == 0:
272 # at start of line, must put a &nbsp;
273 # because regular space will be collapsed
274 prevCh = b' '
275 while pos < lengthDoc and self.editor.byteAt(pos) == b' ':
276 if prevCh != b' ':
277 html += ' '
278 else:
279 html += '''&nbsp;'''
280 prevCh = self.editor.byteAt(pos)
281 pos += 1
282 column += 1
283 pos -= 1
284 # the last incrementation will be done by the outer loop
285 else:
286 html += ' '
287 column += 1
288 elif ch == b'\t':
289 ts = tabSize - (column % tabSize)
290 if wysiwyg:
291 html += '''&nbsp;''' * ts
292 column += ts
293 else:
294 if useTabs:
295 html += '\t'
296 column += 1
297 else:
298 html += ' ' * ts
299 column += ts
300 elif ch in [b'\r', b'\n']:
301 if inStyleSpan:
302 html += '''</span>'''
303 inStyleSpan = False
304 if inFoldSpan:
305 html += '''</span>'''
306 inFoldSpan = False
307 if ch == b'\r' and self.editor.byteAt(pos + 1) == b'\n':
308 pos += 1 # CR+LF line ending, skip the "extra" EOL char
309 column = 0
310 if wysiwyg:
311 html += '''<br />'''
312
313 styleCurrent = self.editor.styleAt(pos + 1)
314 if folding:
315 line = self.editor.lineAt(pos + 1)
316 newLevel = self.editor.foldLevelAt(line)
317
318 if newLevel < level:
319 while levelStack[-1] > newLevel:
320 html += '''</span>'''
321 levelStack.pop()
322 html += '\n' # here to get clean code
323 if newLevel > level:
324 html += '''<span id="ln{0:d}">'''.format(line)
325 levelStack.append(newLevel)
326 if (
327 self.editor.foldFlagsAt(line) &
328 QsciScintilla.SC_FOLDLEVELHEADERFLAG
329 ):
330 html += (
331 '''<span id="hd{0:d}"'''
332 ''' onclick="toggle('{1:d}')">'''
333 ).format(line, line + 1)
334 html += '''<span id="bt{0:d}">- </span>'''.format(line)
335 inFoldSpan = True
336 else:
337 html += '''&nbsp; '''
338 level = newLevel
339 else:
340 html += '\n'
341
342 if (
343 styleIsUsed[styleCurrent] and
344 self.editor.byteAt(pos + 1) not in [b'\r', b'\n']
345 ):
346 # We know it's the correct next style,
347 # but no (empty) span for an empty line
348 html += '''<span class="S{0:0d}">'''.format(styleCurrent)
349 inStyleSpan = True
350 else:
351 if ch == b'<':
352 html += '''&lt;'''
353 elif ch == b'>':
354 html += '''&gt'''
355 elif ch == b'&':
356 html += '''&amp;'''
357 else:
358 if ord(ch) > 127 and utf8:
359 utf8Ch += ch
360 if utf8Len == 0:
361 if (utf8Ch[0] & 0xF0) == 0xF0:
362 utf8Len = 4
363 elif (utf8Ch[0] & 0xE0) == 0xE0:
364 utf8Len = 3
365 elif (utf8Ch[0] & 0xC0) == 0xC0:
366 utf8Len = 2
367 column -= 1 # will be incremented again later
368 elif len(utf8Ch) == utf8Len:
369 ch = utf8Ch.decode('utf8')
370 html += Utilities.html_encode(ch)
371 utf8Ch = b""
372 utf8Len = 0
373 else:
374 column -= 1 # will be incremented again later
375 else:
376 html += ch.decode()
377 column += 1
378
379 pos += 1
380
381 if inStyleSpan:
382 html += '''</span>'''
383
384 if folding:
385 while levelStack:
386 html += '''</span>'''
387 levelStack.pop()
388
389 if wysiwyg:
390 html += '''</span>'''
391 else:
392 html += '''</pre>'''
393
394 html += '''</body>\n</html>\n'''
395
396 return html
397
398
399 class ExporterHTML(ExporterBase):
400 """
401 Class implementing an exporter for HTML.
402 """
403 def __init__(self, editor, parent=None):
404 """
405 Constructor
406
407 @param editor reference to the editor object (QScintilla.Editor.Editor)
408 @param parent parent object of the exporter (QObject)
409 """
410 ExporterBase.__init__(self, editor, parent)
411
412 def exportSource(self):
413 """
414 Public method performing the export.
415 """
416 filename = self._getFileName(self.tr("HTML Files (*.html)"))
417 if not filename:
418 return
419
420 fn = self.editor.getFileName()
421 extension = os.path.normcase(os.path.splitext(fn)[1][1:]) if fn else ""
422
423 if (
424 extension in Preferences.getEditor(
425 "PreviewMarkdownFileNameExtensions") or
426 self.editor.getLanguage().lower() == "markdown"
427 ):
428 # export markdown to HTML
429 colorSchemes = [
430 self.tr("Light Background Color"),
431 self.tr("Dark Background Color"),
432 ]
433 colorScheme, ok = QInputDialog.getItem(
434 None,
435 self.tr("Markdown Export"),
436 self.tr("Select color scheme:"),
437 colorSchemes,
438 0, False)
439 if ok:
440 colorSchemeIndex = colorSchemes.index(colorScheme)
441 else:
442 # light background as default
443 colorSchemeIndex = 0
444 with E5OverrideCursor():
445 html = self.__generateFromMarkdown(colorSchemeIndex == 1)
446 elif (
447 extension in Preferences.getEditor(
448 "PreviewRestFileNameExtensions") or
449 self.editor.getLanguage().lower() == "restructuredtext"
450 ):
451 # export ReST to HTML
452 with E5OverrideCursor():
453 html = self.__generateFromReSTDocutils()
454 else:
455 tabSize = self.editor.getEditorConfig("TabWidth")
456 if tabSize == 0:
457 tabSize = 4
458 wysiwyg = Preferences.getEditorExporter("HTML/WYSIWYG")
459 folding = Preferences.getEditorExporter("HTML/Folding")
460 onlyStylesUsed = Preferences.getEditorExporter(
461 "HTML/OnlyStylesUsed")
462 titleFullPath = Preferences.getEditorExporter(
463 "HTML/FullPathAsTitle")
464 tabs = Preferences.getEditorExporter("HTML/UseTabs")
465
466 with E5OverrideCursor():
467 generator = HTMLGenerator(self.editor)
468 html = generator.generate(
469 tabSize=tabSize,
470 useTabs=tabs,
471 wysiwyg=wysiwyg,
472 folding=folding,
473 onlyStylesUsed=onlyStylesUsed,
474 titleFullPath=titleFullPath
475 )
476
477 if html:
478 with E5OverrideCursor(), open(filename, "w", encoding="utf-8"
479 ) as f:
480 try:
481 f.write(html)
482 except OSError as err:
483 E5MessageBox.critical(
484 self.editor,
485 self.tr("Export source"),
486 self.tr(
487 """<p>The source could not be exported to"""
488 """ <b>{0}</b>.</p><p>Reason: {1}</p>""")
489 .format(filename, str(err)))
490 else:
491 E5MessageBox.critical(
492 self.editor,
493 self.tr("Export source"),
494 self.tr(
495 """<p>The source could not be exported to"""
496 """ <b>{0}</b>.</p><p>Reason: No HTML code"""
497 """ generated.</p>""")
498 .format(filename))
499
500 def __generateFromReSTDocutils(self):
501 """
502 Private method to convert ReST text into HTML using 'docutils'.
503
504 @return processed HTML (string)
505 """
506 if 'sphinx' in sys.modules:
507 # Make sure any Sphinx polution of docutils has been removed.
508 unloadKeys = [k for k in sys.modules.keys()
509 if k.startswith(('docutils', 'sphinx'))]
510 for key in unloadKeys:
511 sys.modules.pop(key)
512
513 try:
514 import docutils.core # __IGNORE_EXCEPTION__
515 except ImportError:
516 E5MessageBox.critical(
517 self.editor,
518 self.tr("Export source"),
519 self.tr(
520 """<p>ReStructuredText export requires the"""
521 """ <b>python-docutils</b> package.<br/>Install it with"""
522 """ your package manager, 'pip install docutils' or see"""
523 """ <a href="http://pypi.python.org/pypi/docutils">"""
524 """this page.</a></p>""")
525 )
526 return ""
527
528 htmlFormat = Preferences.getEditor(
529 "PreviewRestDocutilsHTMLFormat").lower()
530 # redirect sys.stderr because we are not interested in it here
531 origStderr = sys.stderr
532 sys.stderr = io.StringIO()
533 html = docutils.core.publish_string(
534 self.editor.text(), writer_name=htmlFormat).decode("utf-8")
535 sys.stderr = origStderr
536 return html
537
538 def __generateFromMarkdown(self, useDarkScheme):
539 """
540 Private method to convert Markdown text into HTML.
541
542 @param useDarkScheme flag indicating to export using a dark color
543 scheme
544 @type bool
545 @return processed HTML
546 @rtype str
547 """
548 try:
549 import markdown # __IGNORE_EXCEPTION__
550 except ImportError:
551 E5MessageBox.critical(
552 self.editor,
553 self.tr("Export source"),
554 self.tr(
555 """<p>Markdown export requires the <b>python-markdown"""
556 """</b> package.<br/>Install it with your package"""
557 """ manager, 'pip install docutils' or see """
558 """<a href="http://pythonhosted.org/Markdown/install"""
559 """.html"> installation instructions.</a></p>""")
560 )
561 return ""
562
563 from UI.Previewers import PreviewerHTMLStyles
564 from UI.Previewers import MarkdownExtensions
565
566 extensions = []
567
568 text = self.editor.text()
569
570 mermaidNeeded = False
571 if (
572 Preferences.getEditor("PreviewMarkdownMermaid") and
573 MarkdownExtensions.MermaidRegexFullText.search(text)
574 ):
575 extensions.append(MarkdownExtensions.MermaidExtension())
576 mermaidNeeded = True
577
578 if Preferences.getEditor("PreviewMarkdownNLtoBR"):
579 extensions.append('nl2br')
580
581 pyMdown = False
582 if Preferences.getEditor("PreviewMarkdownUsePyMdownExtensions"):
583 with contextlib.suppress(ImportError):
584 import pymdownx # __IGNORE_EXCEPTION__ __IGNORE_WARNING__
585 # PyPI package is 'pymdown-extensions'
586
587 extensions.extend([
588 'toc',
589 'pymdownx.extra', 'pymdownx.caret', 'pymdownx.emoji',
590 'pymdownx.mark', 'pymdownx.tilde', 'pymdownx.keys',
591 'pymdownx.tasklist', 'pymdownx.smartsymbols',
592 ])
593 pyMdown = True
594
595 if not pyMdown:
596 extensions.extend(['extra', 'toc'])
597
598 # version 2.0 supports only extension names, not instances
599 if (
600 markdown.version_info[0] > 2 or
601 (markdown.version_info[0] == 2 and
602 markdown.version_info[1] > 0)
603 ):
604 extensions.append(MarkdownExtensions.SimplePatternExtension())
605
606 if Preferences.getEditor("PreviewMarkdownMathJax"):
607 mathjax = (
608 "<script type='text/javascript' id='MathJax-script' async"
609 " src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/"
610 "tex-chtml.js'>\n"
611 "</script>\n"
612 )
613 # prepare text for mathjax
614 text = (
615 text
616 .replace(r"\(", r"\\(")
617 .replace(r"\)", r"\\)")
618 .replace(r"\[", r"\\[")
619 .replace(r"\]", r"\\]")
620 )
621 else:
622 mathjax = ""
623
624 if mermaidNeeded:
625 mermaid = (
626 "<script type='text/javascript' id='Mermaid-script'"
627 " src='https://unpkg.com/mermaid@8/dist/mermaid.min.js'>\n"
628 "</script>\n"
629 )
630 if useDarkScheme:
631 mermaid_initialize = (
632 "<script>mermaid.initialize({"
633 "theme: 'dark', "
634 "startOnLoad:true"
635 "});</script>"
636 )
637 else:
638 mermaid_initialize = (
639 "<script>mermaid.initialize({"
640 "theme: 'default', "
641 "startOnLoad:true"
642 "});</script>"
643 )
644 else:
645 mermaid = ""
646 mermaid_initialize = ""
647
648 htmlFormat = Preferences.getEditor("PreviewMarkdownHTMLFormat").lower()
649 body = markdown.markdown(text, extensions=extensions,
650 output_format=htmlFormat)
651 style = (
652 (PreviewerHTMLStyles.css_markdown_dark +
653 PreviewerHTMLStyles.css_pygments_dark)
654 if useDarkScheme else
655 (PreviewerHTMLStyles.css_markdown_light +
656 PreviewerHTMLStyles.css_pygments_light)
657 )
658
659 if htmlFormat == "xhtml1":
660 head = (
661 '''<!DOCTYPE html PUBLIC "-//W3C//DTD'''
662 ''' XHTML 1.0 Transitional//EN"\n'''
663 ''' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional'''
664 '''.dtd">\n'''
665 '''<html xmlns="http://www.w3.org/1999/xhtml">\n'''
666 )
667 elif htmlFormat == "html5":
668 head = (
669 '''<!DOCTYPE html>\n'''
670 '''<html lang="EN">\n'''
671 )
672 else:
673 head = '<html lang="EN">\n'
674 head += '''<head>\n'''
675 if Preferences.getEditorExporter("HTML/FullPathAsTitle"):
676 head += '''<title>{0}</title>\n'''.format(
677 self.editor.getFileName())
678 else:
679 head += '''<title>{0}</title>\n'''.format(
680 os.path.basename(self.editor.getFileName()))
681 head += (
682 '''<meta name="Generator" content="eric" />\n'''
683 '''<meta http-equiv="Content-Type" '''
684 '''content="text/html; charset=utf-8" />\n'''
685 '''{0}'''
686 '''{1}'''
687 '''<style type="text/css">'''
688 '''{2}'''
689 '''</style>\n'''
690 '''</head>\n'''
691 '''<body>\n'''
692 ).format(mathjax, mermaid, style)
693
694 foot = '''\n</body>\n</html>\n'''
695
696 return head + body + mermaid_initialize + foot

eric ide

mercurial