novelWriter 2.7.4__py3-none-any.whl → 2.8b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- novelwriter/__init__.py +8 -7
 - novelwriter/assets/icons/font_awesome.icons +22 -4
 - novelwriter/assets/icons/material_filled_normal.icons +20 -2
 - novelwriter/assets/icons/material_filled_thin.icons +20 -2
 - novelwriter/assets/icons/material_rounded_normal.icons +20 -2
 - novelwriter/assets/icons/material_rounded_thin.icons +20 -2
 - novelwriter/assets/icons/material_sharp_normal.icons +20 -2
 - novelwriter/assets/icons/material_sharp_thin.icons +20 -2
 - novelwriter/assets/icons/remix_filled.icons +20 -2
 - novelwriter/assets/icons/remix_outline.icons +20 -2
 - novelwriter/assets/images/welcome.webp +0 -0
 - novelwriter/assets/manual.pdf +0 -0
 - novelwriter/assets/manual_fr.pdf +0 -0
 - novelwriter/assets/sample.zip +0 -0
 - novelwriter/assets/text/credits_en.htm +61 -11
 - novelwriter/assets/themes/aura.conf +97 -0
 - novelwriter/assets/themes/aura_bright.conf +95 -0
 - novelwriter/assets/themes/aura_soft.conf +97 -0
 - novelwriter/assets/themes/b2t_garden_dark.conf +97 -0
 - novelwriter/assets/themes/b2t_garden_light.conf +97 -0
 - novelwriter/assets/themes/b2t_suburb_dark.conf +97 -0
 - novelwriter/assets/themes/b2t_suburb_light.conf +97 -0
 - novelwriter/assets/themes/b4t_classic_o_dark.conf +97 -0
 - novelwriter/assets/themes/b4t_classic_o_light.conf +97 -0
 - novelwriter/assets/themes/b4t_modern_c_dark.conf +97 -0
 - novelwriter/assets/themes/b4t_modern_c_light.conf +97 -0
 - novelwriter/assets/themes/blue_streak_dark.conf +97 -0
 - novelwriter/assets/themes/blue_streak_light.conf +97 -0
 - novelwriter/assets/themes/castle_day.conf +95 -0
 - novelwriter/assets/themes/castle_night.conf +95 -0
 - novelwriter/assets/themes/catppuccin_latte.conf +97 -0
 - novelwriter/assets/themes/catppuccin_mocha.conf +97 -0
 - novelwriter/assets/themes/chalky_soil.conf +95 -0
 - novelwriter/assets/themes/chernozem.conf +95 -0
 - novelwriter/assets/themes/cyberpunk_night.conf +88 -40
 - novelwriter/assets/themes/default_dark.conf +89 -41
 - novelwriter/assets/themes/default_light.conf +89 -41
 - novelwriter/assets/themes/dracula.conf +91 -42
 - novelwriter/assets/themes/espresso.conf +97 -0
 - novelwriter/assets/themes/everforest_dark.conf +97 -0
 - novelwriter/assets/themes/everforest_light.conf +97 -0
 - novelwriter/assets/themes/floral_daydream.conf +95 -0
 - novelwriter/assets/themes/floral_midnight.conf +95 -0
 - novelwriter/assets/themes/full_moon.conf +95 -0
 - novelwriter/assets/themes/grey_dark.conf +97 -0
 - novelwriter/assets/themes/grey_light.conf +97 -0
 - novelwriter/assets/themes/horizon_dark.conf +97 -0
 - novelwriter/assets/themes/horizon_light.conf +97 -0
 - novelwriter/assets/themes/jewel_case_dark.conf +95 -0
 - novelwriter/assets/themes/jewel_case_light.conf +95 -0
 - novelwriter/assets/themes/lcars.conf +97 -0
 - novelwriter/assets/themes/light_owl.conf +117 -0
 - novelwriter/assets/themes/new_moon.conf +97 -0
 - novelwriter/assets/themes/night_owl.conf +117 -0
 - novelwriter/assets/themes/noctis.conf +129 -0
 - novelwriter/assets/themes/noctis_lux.conf +129 -0
 - novelwriter/assets/themes/nord.conf +97 -0
 - novelwriter/assets/themes/nordlicht.conf +95 -0
 - novelwriter/assets/themes/otium_dark.conf +95 -0
 - novelwriter/assets/themes/otium_light.conf +95 -0
 - novelwriter/assets/themes/paragon.conf +96 -0
 - novelwriter/assets/themes/primer_light.conf +97 -0
 - novelwriter/assets/themes/primer_night.conf +97 -0
 - novelwriter/assets/themes/rose_pine.conf +97 -0
 - novelwriter/assets/themes/rose_pine_dawn.conf +97 -0
 - novelwriter/assets/themes/ruby_day.conf +95 -0
 - novelwriter/assets/themes/ruby_night.conf +95 -0
 - novelwriter/assets/themes/selenium_dark.conf +95 -0
 - novelwriter/assets/themes/selenium_light.conf +95 -0
 - novelwriter/assets/themes/sepia_dark.conf +95 -0
 - novelwriter/assets/themes/sepia_light.conf +95 -0
 - novelwriter/assets/themes/snazzy.conf +102 -40
 - novelwriter/assets/themes/solarized_dark.conf +108 -40
 - novelwriter/assets/themes/solarized_light.conf +108 -40
 - novelwriter/assets/themes/sultana_light.conf +95 -0
 - novelwriter/assets/themes/sultana_night.conf +95 -0
 - novelwriter/assets/themes/tango_dark.conf +111 -0
 - novelwriter/assets/themes/tango_light.conf +111 -0
 - novelwriter/assets/themes/tomorrow.conf +117 -0
 - novelwriter/assets/themes/tomorrow_night.conf +117 -0
 - novelwriter/assets/themes/tomorrow_night_blue.conf +117 -0
 - novelwriter/assets/themes/tomorrow_night_bright.conf +117 -0
 - novelwriter/assets/themes/tomorrow_night_eighties.conf +117 -0
 - novelwriter/assets/themes/vivid_black_green.conf +97 -0
 - novelwriter/assets/themes/vivid_black_red.conf +97 -0
 - novelwriter/assets/themes/vivid_white_green.conf +97 -0
 - novelwriter/assets/themes/vivid_white_red.conf +97 -0
 - novelwriter/assets/themes/warpgate.conf +96 -0
 - novelwriter/assets/themes/waterlily_dark.conf +95 -0
 - novelwriter/assets/themes/waterlily_light.conf +95 -0
 - novelwriter/common.py +47 -17
 - novelwriter/config.py +57 -62
 - novelwriter/constants.py +32 -6
 - novelwriter/core/buildsettings.py +3 -23
 - novelwriter/core/coretools.py +21 -25
 - novelwriter/core/docbuild.py +4 -9
 - novelwriter/core/document.py +2 -6
 - novelwriter/core/index.py +33 -53
 - novelwriter/core/indexdata.py +17 -22
 - novelwriter/core/item.py +11 -35
 - novelwriter/core/itemmodel.py +5 -21
 - novelwriter/core/novelmodel.py +3 -7
 - novelwriter/core/options.py +3 -4
 - novelwriter/core/project.py +31 -21
 - novelwriter/core/projectdata.py +2 -21
 - novelwriter/core/projectxml.py +13 -21
 - novelwriter/core/sessions.py +2 -4
 - novelwriter/core/spellcheck.py +12 -13
 - novelwriter/core/status.py +27 -20
 - novelwriter/core/storage.py +5 -10
 - novelwriter/core/tree.py +6 -15
 - novelwriter/dialogs/about.py +9 -10
 - novelwriter/dialogs/docmerge.py +17 -14
 - novelwriter/dialogs/docsplit.py +18 -14
 - novelwriter/dialogs/editlabel.py +15 -9
 - novelwriter/dialogs/preferences.py +69 -68
 - novelwriter/dialogs/projectsettings.py +88 -67
 - novelwriter/dialogs/quotes.py +15 -10
 - novelwriter/dialogs/wordlist.py +18 -21
 - novelwriter/enum.py +75 -30
 - novelwriter/error.py +6 -11
 - novelwriter/extensions/configlayout.py +8 -34
 - novelwriter/extensions/eventfilters.py +3 -3
 - novelwriter/extensions/modified.py +87 -32
 - novelwriter/extensions/novelselector.py +13 -12
 - novelwriter/extensions/pagedsidebar.py +10 -18
 - novelwriter/extensions/progressbars.py +5 -11
 - novelwriter/extensions/statusled.py +3 -6
 - novelwriter/extensions/switch.py +8 -11
 - novelwriter/extensions/switchbox.py +2 -11
 - novelwriter/extensions/versioninfo.py +6 -7
 - novelwriter/formats/shared.py +10 -2
 - novelwriter/formats/todocx.py +15 -37
 - novelwriter/formats/tohtml.py +52 -61
 - novelwriter/formats/tokenizer.py +33 -64
 - novelwriter/formats/tomarkdown.py +4 -11
 - novelwriter/formats/toodt.py +12 -71
 - novelwriter/formats/toqdoc.py +11 -21
 - novelwriter/formats/toraw.py +2 -6
 - novelwriter/gui/doceditor.py +207 -245
 - novelwriter/gui/dochighlight.py +142 -101
 - novelwriter/gui/docviewer.py +53 -84
 - novelwriter/gui/docviewerpanel.py +18 -41
 - novelwriter/gui/editordocument.py +12 -17
 - novelwriter/gui/itemdetails.py +5 -14
 - novelwriter/gui/mainmenu.py +24 -32
 - novelwriter/gui/noveltree.py +13 -51
 - novelwriter/gui/outline.py +20 -61
 - novelwriter/gui/projtree.py +40 -96
 - novelwriter/gui/search.py +9 -24
 - novelwriter/gui/sidebar.py +54 -22
 - novelwriter/gui/statusbar.py +7 -22
 - novelwriter/gui/theme.py +482 -368
 - novelwriter/guimain.py +87 -101
 - novelwriter/shared.py +79 -48
 - novelwriter/splash.py +9 -5
 - novelwriter/text/comments.py +1 -1
 - novelwriter/text/counting.py +9 -5
 - novelwriter/text/patterns.py +20 -15
 - novelwriter/tools/dictionaries.py +18 -16
 - novelwriter/tools/lipsum.py +15 -17
 - novelwriter/tools/manusbuild.py +25 -45
 - novelwriter/tools/manuscript.py +94 -95
 - novelwriter/tools/manussettings.py +149 -104
 - novelwriter/tools/noveldetails.py +10 -24
 - novelwriter/tools/welcome.py +24 -72
 - novelwriter/tools/writingstats.py +17 -26
 - novelwriter/types.py +25 -13
 - {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/METADATA +7 -7
 - novelwriter-2.8b1.dist-info/RECORD +212 -0
 - novelwriter/assets/images/welcome-dark.jpg +0 -0
 - novelwriter/assets/images/welcome-light.jpg +0 -0
 - novelwriter/assets/syntax/cyberpunk_night.conf +0 -28
 - novelwriter/assets/syntax/default_dark.conf +0 -42
 - novelwriter/assets/syntax/default_light.conf +0 -42
 - novelwriter/assets/syntax/dracula.conf +0 -44
 - novelwriter/assets/syntax/grey_dark.conf +0 -29
 - novelwriter/assets/syntax/grey_light.conf +0 -29
 - novelwriter/assets/syntax/light_owl.conf +0 -49
 - novelwriter/assets/syntax/night_owl.conf +0 -49
 - novelwriter/assets/syntax/snazzy.conf +0 -42
 - novelwriter/assets/syntax/solarized_dark.conf +0 -29
 - novelwriter/assets/syntax/solarized_light.conf +0 -29
 - novelwriter/assets/syntax/tango.conf +0 -39
 - novelwriter/assets/syntax/tomorrow.conf +0 -49
 - novelwriter/assets/syntax/tomorrow_night.conf +0 -49
 - novelwriter/assets/syntax/tomorrow_night_blue.conf +0 -49
 - novelwriter/assets/syntax/tomorrow_night_bright.conf +0 -49
 - novelwriter/assets/syntax/tomorrow_night_eighties.conf +0 -49
 - novelwriter/assets/themes/default.conf +0 -3
 - novelwriter-2.7.4.dist-info/RECORD +0 -163
 - {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/WHEEL +0 -0
 - {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/entry_points.txt +0 -0
 - {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/LICENSE.md +0 -0
 - {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/licenses/setup/LICENSE-Apache-2.0.txt +0 -0
 - {novelwriter-2.7.4.dist-info → novelwriter-2.8b1.dist-info}/top_level.txt +0 -0
 
| 
         @@ -20,7 +20,7 @@ General Public License for more details. 
     | 
|
| 
       20 
20 
     | 
    
         | 
| 
       21 
21 
     | 
    
         
             
            You should have received a copy of the GNU General Public License
         
     | 
| 
       22 
22 
     | 
    
         
             
            along with this program. If not, see <https://www.gnu.org/licenses/>.
         
     | 
| 
       23 
     | 
    
         
            -
            """
         
     | 
| 
      
 23 
     | 
    
         
            +
            """  # noqa
         
     | 
| 
       24 
24 
     | 
    
         
             
            from __future__ import annotations
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
            from typing import TYPE_CHECKING
         
     | 
| 
         @@ -39,7 +39,7 @@ if TYPE_CHECKING: 
     | 
|
| 
       39 
39 
     | 
    
         | 
| 
       40 
40 
     | 
    
         | 
| 
       41 
41 
     | 
    
         
             
            class NSwitchBox(QScrollArea):
         
     | 
| 
       42 
     | 
    
         
            -
                """Extension: Switch Box Widget
         
     | 
| 
      
 42 
     | 
    
         
            +
                """Extension: Switch Box Widget.
         
     | 
| 
       43 
43 
     | 
    
         | 
| 
       44 
44 
     | 
    
         
             
                A widget that can hold a list of switches with labels and optional
         
     | 
| 
       45 
45 
     | 
    
         
             
                icons. The switch toggles emits a common signal with a switch key.
         
     | 
| 
         @@ -55,7 +55,6 @@ class NSwitchBox(QScrollArea): 
     | 
|
| 
       55 
55 
     | 
    
         
             
                    self._sIcon = baseSize
         
     | 
| 
       56 
56 
     | 
    
         
             
                    self._widgets = []
         
     | 
| 
       57 
57 
     | 
    
         
             
                    self.clear()
         
     | 
| 
       58 
     | 
    
         
            -
                    return
         
     | 
| 
       59 
58 
     | 
    
         | 
| 
       60 
59 
     | 
    
         
             
                def clear(self) -> None:
         
     | 
| 
       61 
60 
     | 
    
         
             
                    """Rebuild the content of the core widget."""
         
     | 
| 
         @@ -72,8 +71,6 @@ class NSwitchBox(QScrollArea): 
     | 
|
| 
       72 
71 
     | 
    
         
             
                    self.setWidgetResizable(True)
         
     | 
| 
       73 
72 
     | 
    
         
             
                    self.setWidget(self._widget)
         
     | 
| 
       74 
73 
     | 
    
         | 
| 
       75 
     | 
    
         
            -
                    return
         
     | 
| 
       76 
     | 
    
         
            -
             
     | 
| 
       77 
74 
     | 
    
         
             
                def addLabel(self, text: str) -> None:
         
     | 
| 
       78 
75 
     | 
    
         
             
                    """Add a header label to the content box."""
         
     | 
| 
       79 
76 
     | 
    
         
             
                    label = QLabel(text, self)
         
     | 
| 
         @@ -83,7 +80,6 @@ class NSwitchBox(QScrollArea): 
     | 
|
| 
       83 
80 
     | 
    
         
             
                    self._content.addWidget(label, self._index, 0, 1, 3, QtAlignLeft)
         
     | 
| 
       84 
81 
     | 
    
         
             
                    self._widgets.append(label)
         
     | 
| 
       85 
82 
     | 
    
         
             
                    self._bumpIndex()
         
     | 
| 
       86 
     | 
    
         
            -
                    return
         
     | 
| 
       87 
83 
     | 
    
         | 
| 
       88 
84 
     | 
    
         
             
                def addItem(self, qIcon: QIcon, text: str, identifier: str, default: bool = False) -> None:
         
     | 
| 
       89 
85 
     | 
    
         
             
                    """Add an item to the content box."""
         
     | 
| 
         @@ -104,8 +100,6 @@ class NSwitchBox(QScrollArea): 
     | 
|
| 
       104 
100 
     | 
    
         
             
                    self._widgets.append(switch)
         
     | 
| 
       105 
101 
     | 
    
         
             
                    self._bumpIndex()
         
     | 
| 
       106 
102 
     | 
    
         | 
| 
       107 
     | 
    
         
            -
                    return
         
     | 
| 
       108 
     | 
    
         
            -
             
     | 
| 
       109 
103 
     | 
    
         
             
                def addSeparator(self) -> None:
         
     | 
| 
       110 
104 
     | 
    
         
             
                    """Add a blank entry in the content box."""
         
     | 
| 
       111 
105 
     | 
    
         
             
                    spacer = QWidget(self)
         
     | 
| 
         @@ -113,7 +107,6 @@ class NSwitchBox(QScrollArea): 
     | 
|
| 
       113 
107 
     | 
    
         
             
                    self._content.addWidget(spacer, self._index, 0, 1, 3, QtAlignLeft)
         
     | 
| 
       114 
108 
     | 
    
         
             
                    self._widgets.append(spacer)
         
     | 
| 
       115 
109 
     | 
    
         
             
                    self._bumpIndex()
         
     | 
| 
       116 
     | 
    
         
            -
                    return
         
     | 
| 
       117 
110 
     | 
    
         | 
| 
       118 
111 
     | 
    
         
             
                ##
         
     | 
| 
       119 
112 
     | 
    
         
             
                #  Internal Functions
         
     | 
| 
         @@ -122,7 +115,6 @@ class NSwitchBox(QScrollArea): 
     | 
|
| 
       122 
115 
     | 
    
         
             
                def _emitSwitchSignal(self, identifier: str, state: bool) -> None:
         
     | 
| 
       123 
116 
     | 
    
         
             
                    """Emit a signal for a switch toggle."""
         
     | 
| 
       124 
117 
     | 
    
         
             
                    self.switchToggled.emit(identifier, state)
         
     | 
| 
       125 
     | 
    
         
            -
                    return
         
     | 
| 
       126 
118 
     | 
    
         | 
| 
       127 
119 
     | 
    
         
             
                def _bumpIndex(self) -> None:
         
     | 
| 
       128 
120 
     | 
    
         
             
                    """Increase the index counter and make sure only the last
         
     | 
| 
         @@ -131,4 +123,3 @@ class NSwitchBox(QScrollArea): 
     | 
|
| 
       131 
123 
     | 
    
         
             
                    self._content.setRowStretch(self._index, 0)
         
     | 
| 
       132 
124 
     | 
    
         
             
                    self._content.setRowStretch(self._index + 1, 1)
         
     | 
| 
       133 
125 
     | 
    
         
             
                    self._index += 1
         
     | 
| 
       134 
     | 
    
         
            -
                    return
         
     | 
| 
         @@ -20,7 +20,7 @@ General Public License for more details. 
     | 
|
| 
       20 
20 
     | 
    
         | 
| 
       21 
21 
     | 
    
         
             
            You should have received a copy of the GNU General Public License
         
     | 
| 
       22 
22 
     | 
    
         
             
            along with this program. If not, see <https://www.gnu.org/licenses/>.
         
     | 
| 
       23 
     | 
    
         
            -
            """
         
     | 
| 
      
 23 
     | 
    
         
            +
            """  # noqa
         
     | 
| 
       24 
24 
     | 
    
         
             
            from __future__ import annotations
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
            import json
         
     | 
| 
         @@ -45,6 +45,11 @@ API_URL = "https://api.github.com/repos/vkbo/novelwriter/releases/latest" 
     | 
|
| 
       45 
45 
     | 
    
         | 
| 
       46 
46 
     | 
    
         | 
| 
       47 
47 
     | 
    
         
             
            class VersionInfoWidget(QWidget):
         
     | 
| 
      
 48 
     | 
    
         
            +
                """Custom: version Info Label.
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                A custom widget that will show a clickable area for contacting
         
     | 
| 
      
 51 
     | 
    
         
            +
                GitHub and pulling the latest release version info.
         
     | 
| 
      
 52 
     | 
    
         
            +
                """
         
     | 
| 
       48 
53 
     | 
    
         | 
| 
       49 
54 
     | 
    
         
             
                def __init__(self, parent: QWidget) -> None:
         
     | 
| 
       50 
55 
     | 
    
         
             
                    super().__init__(parent=parent)
         
     | 
| 
         @@ -75,8 +80,6 @@ class VersionInfoWidget(QWidget): 
     | 
|
| 
       75 
80 
     | 
    
         | 
| 
       76 
81 
     | 
    
         
             
                    self.setLayout(self._layout)
         
     | 
| 
       77 
82 
     | 
    
         | 
| 
       78 
     | 
    
         
            -
                    return
         
     | 
| 
       79 
     | 
    
         
            -
             
     | 
| 
       80 
83 
     | 
    
         
             
                ##
         
     | 
| 
       81 
84 
     | 
    
         
             
                #  Private Slots
         
     | 
| 
       82 
85 
     | 
    
         
             
                ##
         
     | 
| 
         @@ -93,7 +96,6 @@ class VersionInfoWidget(QWidget): 
     | 
|
| 
       93 
96 
     | 
    
         
             
                        lookup = _Retriever()
         
     | 
| 
       94 
97 
     | 
    
         
             
                        lookup.signals.dataReady.connect(self._updateReleaseInfo)
         
     | 
| 
       95 
98 
     | 
    
         
             
                        SHARED.runInThreadPool(lookup)
         
     | 
| 
       96 
     | 
    
         
            -
                    return
         
     | 
| 
       97 
99 
     | 
    
         | 
| 
       98 
100 
     | 
    
         
             
                ##
         
     | 
| 
       99 
101 
     | 
    
         
             
                #  Private Slots
         
     | 
| 
         @@ -109,7 +111,6 @@ class VersionInfoWidget(QWidget): 
     | 
|
| 
       109 
111 
     | 
    
         
             
                        ))
         
     | 
| 
       110 
112 
     | 
    
         
             
                    else:
         
     | 
| 
       111 
113 
     | 
    
         
             
                        self._lblRelease.setText(self._trLatest.format(reason or self.tr("Failed")))
         
     | 
| 
       112 
     | 
    
         
            -
                    return
         
     | 
| 
       113 
114 
     | 
    
         | 
| 
       114 
115 
     | 
    
         | 
| 
       115 
116 
     | 
    
         
             
            class _Retriever(QRunnable):
         
     | 
| 
         @@ -117,7 +118,6 @@ class _Retriever(QRunnable): 
     | 
|
| 
       117 
118 
     | 
    
         
             
                def __init__(self) -> None:
         
     | 
| 
       118 
119 
     | 
    
         
             
                    super().__init__()
         
     | 
| 
       119 
120 
     | 
    
         
             
                    self.signals = _RetrieverSignal()
         
     | 
| 
       120 
     | 
    
         
            -
                    return
         
     | 
| 
       121 
121 
     | 
    
         | 
| 
       122 
122 
     | 
    
         
             
                @pyqtSlot()
         
     | 
| 
       123 
123 
     | 
    
         
             
                def run(self) -> None:
         
     | 
| 
         @@ -140,7 +140,6 @@ class _Retriever(QRunnable): 
     | 
|
| 
       140 
140 
     | 
    
         
             
                    except Exception as e:
         
     | 
| 
       141 
141 
     | 
    
         
             
                        logger.error("Failed to retrieve release info")
         
     | 
| 
       142 
142 
     | 
    
         
             
                        self.signals.dataReady.emit("", str(e))
         
     | 
| 
       143 
     | 
    
         
            -
                    return
         
     | 
| 
       144 
143 
     | 
    
         | 
| 
       145 
144 
     | 
    
         | 
| 
       146 
145 
     | 
    
         
             
            class _RetrieverSignal(QObject):
         
     | 
    
        novelwriter/formats/shared.py
    CHANGED
    
    | 
         @@ -20,7 +20,7 @@ General Public License for more details. 
     | 
|
| 
       20 
20 
     | 
    
         | 
| 
       21 
21 
     | 
    
         
             
            You should have received a copy of the GNU General Public License
         
     | 
| 
       22 
22 
     | 
    
         
             
            along with this program. If not, see <https://www.gnu.org/licenses/>.
         
     | 
| 
       23 
     | 
    
         
            -
            """
         
     | 
| 
      
 23 
     | 
    
         
            +
            """  # noqa
         
     | 
| 
       24 
24 
     | 
    
         
             
            from __future__ import annotations
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
            import re
         
     | 
| 
         @@ -29,7 +29,15 @@ from enum import Flag, IntEnum 
     | 
|
| 
       29 
29 
     | 
    
         | 
| 
       30 
30 
     | 
    
         
             
            from PyQt6.QtGui import QColor
         
     | 
| 
       31 
31 
     | 
    
         | 
| 
       32 
     | 
    
         
            -
            ESCAPES = { 
     | 
| 
      
 32 
     | 
    
         
            +
            ESCAPES = {
         
     | 
| 
      
 33 
     | 
    
         
            +
                r"\*": "*",
         
     | 
| 
      
 34 
     | 
    
         
            +
                r"\~": "~",
         
     | 
| 
      
 35 
     | 
    
         
            +
                r"\=": "=",
         
     | 
| 
      
 36 
     | 
    
         
            +
                r"\_": "_",
         
     | 
| 
      
 37 
     | 
    
         
            +
                r"\[": "[",
         
     | 
| 
      
 38 
     | 
    
         
            +
                r"\]": "]",
         
     | 
| 
      
 39 
     | 
    
         
            +
                r"\ ": "",
         
     | 
| 
      
 40 
     | 
    
         
            +
            }
         
     | 
| 
       33 
41 
     | 
    
         
             
            RX_ESC = re.compile("|".join([re.escape(k) for k in ESCAPES.keys()]), flags=re.DOTALL)
         
     | 
| 
       34 
42 
     | 
    
         | 
| 
       35 
43 
     | 
    
         | 
    
        novelwriter/formats/todocx.py
    CHANGED
    
    | 
         @@ -21,7 +21,7 @@ General Public License for more details. 
     | 
|
| 
       21 
21 
     | 
    
         | 
| 
       22 
22 
     | 
    
         
             
            You should have received a copy of the GNU General Public License
         
     | 
| 
       23 
23 
     | 
    
         
             
            along with this program. If not, see <https://www.gnu.org/licenses/>.
         
     | 
| 
       24 
     | 
    
         
            -
            """
         
     | 
| 
      
 24 
     | 
    
         
            +
            """  # noqa
         
     | 
| 
       25 
25 
     | 
    
         
             
            from __future__ import annotations
         
     | 
| 
       26 
26 
     | 
    
         | 
| 
       27 
27 
     | 
    
         
             
            import logging
         
     | 
| 
         @@ -38,7 +38,7 @@ from novelwriter import __version__ 
     | 
|
| 
       38 
38 
     | 
    
         
             
            from novelwriter.common import firstFloat, xmlElement, xmlSubElem
         
     | 
| 
       39 
39 
     | 
    
         
             
            from novelwriter.constants import nwHeadFmt, nwStyles
         
     | 
| 
       40 
40 
     | 
    
         
             
            from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
         
     | 
| 
       41 
     | 
    
         
            -
            from novelwriter.formats.tokenizer import Tokenizer
         
     | 
| 
      
 41 
     | 
    
         
            +
            from novelwriter.formats.tokenizer import COMMENT_BLOCKS, Tokenizer
         
     | 
| 
       42 
42 
     | 
    
         
             
            from novelwriter.types import QtHexRgb
         
     | 
| 
       43 
43 
     | 
    
         | 
| 
       44 
44 
     | 
    
         
             
            if TYPE_CHECKING:
         
     | 
| 
         @@ -51,7 +51,7 @@ if TYPE_CHECKING: 
     | 
|
| 
       51 
51 
     | 
    
         
             
            logger = logging.getLogger(__name__)
         
     | 
| 
       52 
52 
     | 
    
         | 
| 
       53 
53 
     | 
    
         
             
            # RegEx
         
     | 
| 
       54 
     | 
    
         
            -
            RX_TEXT = re.compile(r"([\n\t])" 
     | 
| 
      
 54 
     | 
    
         
            +
            RX_TEXT = re.compile(r"([\n\t])")
         
     | 
| 
       55 
55 
     | 
    
         | 
| 
       56 
56 
     | 
    
         
             
            # Types and Relationships
         
     | 
| 
       57 
57 
     | 
    
         
             
            OOXML_SCM = "http://schemas.openxmlformats.org"
         
     | 
| 
         @@ -100,7 +100,7 @@ def _wText(parent: ET.Element, text: str) -> ET.Element: 
     | 
|
| 
       100 
100 
     | 
    
         | 
| 
       101 
101 
     | 
    
         | 
| 
       102 
102 
     | 
    
         
             
            def _mmToSz(value: float) -> int:
         
     | 
| 
       103 
     | 
    
         
            -
                """Convert millimetres to internal margin size units"""
         
     | 
| 
      
 103 
     | 
    
         
            +
                """Convert millimetres to internal margin size units."""
         
     | 
| 
       104 
104 
     | 
    
         
             
                return int(value*20.0*72.0/25.4)
         
     | 
| 
       105 
105 
     | 
    
         | 
| 
       106 
106 
     | 
    
         | 
| 
         @@ -143,6 +143,7 @@ S_FNOTE = "FootnoteText" 
     | 
|
| 
       143 
143 
     | 
    
         | 
| 
       144 
144 
     | 
    
         | 
| 
       145 
145 
     | 
    
         
             
            class DocXXmlRel(NamedTuple):
         
     | 
| 
      
 146 
     | 
    
         
            +
                """DocX XML Rel Data."""
         
     | 
| 
       146 
147 
     | 
    
         | 
| 
       147 
148 
     | 
    
         
             
                rId: str
         
     | 
| 
       148 
149 
     | 
    
         
             
                relType: str
         
     | 
| 
         @@ -150,6 +151,7 @@ class DocXXmlRel(NamedTuple): 
     | 
|
| 
       150 
151 
     | 
    
         | 
| 
       151 
152 
     | 
    
         | 
| 
       152 
153 
     | 
    
         
             
            class DocXXmlFile(NamedTuple):
         
     | 
| 
      
 154 
     | 
    
         
            +
                """DocX XML File Data."""
         
     | 
| 
       153 
155 
     | 
    
         | 
| 
       154 
156 
     | 
    
         
             
                xml: ET.Element
         
     | 
| 
       155 
157 
     | 
    
         
             
                path: str
         
     | 
| 
         @@ -157,6 +159,7 @@ class DocXXmlFile(NamedTuple): 
     | 
|
| 
       157 
159 
     | 
    
         | 
| 
       158 
160 
     | 
    
         | 
| 
       159 
161 
     | 
    
         
             
            class DocXParStyle(NamedTuple):
         
     | 
| 
      
 162 
     | 
    
         
            +
                """DocX XML Paragraph Style Data."""
         
     | 
| 
       160 
163 
     | 
    
         | 
| 
       161 
164 
     | 
    
         
             
                name: str
         
     | 
| 
       162 
165 
     | 
    
         
             
                styleId: str
         
     | 
| 
         @@ -176,7 +179,7 @@ class DocXParStyle(NamedTuple): 
     | 
|
| 
       176 
179 
     | 
    
         | 
| 
       177 
180 
     | 
    
         | 
| 
       178 
181 
     | 
    
         
             
            class ToDocX(Tokenizer):
         
     | 
| 
       179 
     | 
    
         
            -
                """Core: DocX Document Writer
         
     | 
| 
      
 182 
     | 
    
         
            +
                """Core: DocX Document Writer.
         
     | 
| 
       180 
183 
     | 
    
         | 
| 
       181 
184 
     | 
    
         
             
                Extend the Tokenizer class to writer DocX Document files.
         
     | 
| 
       182 
185 
     | 
    
         
             
                """
         
     | 
| 
         @@ -202,8 +205,6 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       202 
205 
     | 
    
         
             
                    self._usedNotes: dict[str, int] = {}
         
     | 
| 
       203 
206 
     | 
    
         
             
                    self._usedFields: list[tuple[ET.Element, str]] = []
         
     | 
| 
       204 
207 
     | 
    
         | 
| 
       205 
     | 
    
         
            -
                    return
         
     | 
| 
       206 
     | 
    
         
            -
             
     | 
| 
       207 
208 
     | 
    
         
             
                ##
         
     | 
| 
       208 
209 
     | 
    
         
             
                #  Setters
         
     | 
| 
       209 
210 
     | 
    
         
             
                ##
         
     | 
| 
         @@ -214,25 +215,22 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       214 
215 
     | 
    
         
             
                    """Set the document page size and margins in millimetres."""
         
     | 
| 
       215 
216 
     | 
    
         
             
                    self._pageSize = QSize(_mmToSz(width), _mmToSz(height))
         
     | 
| 
       216 
217 
     | 
    
         
             
                    self._pageMargins = QMargins(_mmToSz(left), _mmToSz(top), _mmToSz(right), _mmToSz(bottom))
         
     | 
| 
       217 
     | 
    
         
            -
                    return
         
     | 
| 
       218 
218 
     | 
    
         | 
| 
       219 
219 
     | 
    
         
             
                def setHeaderFormat(self, value: str, offset: int) -> None:
         
     | 
| 
       220 
220 
     | 
    
         
             
                    """Set the document header format."""
         
     | 
| 
       221 
221 
     | 
    
         
             
                    self._headerFormat = value.strip()
         
     | 
| 
       222 
222 
     | 
    
         
             
                    self._pageOffset = offset
         
     | 
| 
       223 
     | 
    
         
            -
                    return
         
     | 
| 
       224 
223 
     | 
    
         | 
| 
       225 
224 
     | 
    
         
             
                ##
         
     | 
| 
       226 
225 
     | 
    
         
             
                #  Class Methods
         
     | 
| 
       227 
226 
     | 
    
         
             
                ##
         
     | 
| 
       228 
227 
     | 
    
         | 
| 
       229 
228 
     | 
    
         
             
                def initDocument(self) -> None:
         
     | 
| 
       230 
     | 
    
         
            -
                    """ 
     | 
| 
      
 229 
     | 
    
         
            +
                    """Initialise the DocX document structure."""
         
     | 
| 
       231 
230 
     | 
    
         
             
                    super().initDocument()
         
     | 
| 
       232 
231 
     | 
    
         
             
                    self._fontFamily = self._textFont.family()
         
     | 
| 
       233 
232 
     | 
    
         
             
                    self._fontSize = self._textFont.pointSizeF()
         
     | 
| 
       234 
233 
     | 
    
         
             
                    self._generateStyles()
         
     | 
| 
       235 
     | 
    
         
            -
                    return
         
     | 
| 
       236 
234 
     | 
    
         | 
| 
       237 
235 
     | 
    
         
             
                def doConvert(self) -> None:
         
     | 
| 
       238 
236 
     | 
    
         
             
                    """Convert the list of text tokens into XML elements."""
         
     | 
| 
         @@ -296,14 +294,12 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       296 
294 
     | 
    
         
             
                        elif tType == BlockTyp.SKIP:
         
     | 
| 
       297 
295 
     | 
    
         
             
                            self._processFragments(par, S_NORM, "")
         
     | 
| 
       298 
296 
     | 
    
         | 
| 
       299 
     | 
    
         
            -
                        elif tType  
     | 
| 
      
 297 
     | 
    
         
            +
                        elif tType in COMMENT_BLOCKS:
         
     | 
| 
       300 
298 
     | 
    
         
             
                            self._processFragments(par, S_META, tText, tFormat)
         
     | 
| 
       301 
299 
     | 
    
         | 
| 
       302 
300 
     | 
    
         
             
                        elif tType == BlockTyp.KEYWORD:
         
     | 
| 
       303 
301 
     | 
    
         
             
                            self._processFragments(par, S_META, tText, tFormat)
         
     | 
| 
       304 
302 
     | 
    
         | 
| 
       305 
     | 
    
         
            -
                    return
         
     | 
| 
       306 
     | 
    
         
            -
             
     | 
| 
       307 
303 
     | 
    
         
             
                def closeDocument(self) -> None:
         
     | 
| 
       308 
304 
     | 
    
         
             
                    """Generate all the XML."""
         
     | 
| 
       309 
305 
     | 
    
         
             
                    self._coreXml()
         
     | 
| 
         @@ -322,8 +318,6 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       322 
318 
     | 
    
         
             
                    if self._usedNotes:
         
     | 
| 
       323 
319 
     | 
    
         
             
                        self._footnotesXml()
         
     | 
| 
       324 
320 
     | 
    
         | 
| 
       325 
     | 
    
         
            -
                    return
         
     | 
| 
       326 
     | 
    
         
            -
             
     | 
| 
       327 
321 
     | 
    
         
             
                def saveDocument(self, path: Path) -> None:
         
     | 
| 
       328 
322 
     | 
    
         
             
                    """Save the data to a .docx file."""
         
     | 
| 
       329 
323 
     | 
    
         
             
                    # Content Lists
         
     | 
| 
         @@ -373,8 +367,6 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       373 
367 
     | 
    
         
             
                            xmlToZip(f"{rel.path}/{name}", rel.xml, outZip)
         
     | 
| 
       374 
368 
     | 
    
         
             
                        xmlToZip("[Content_Types].xml", dTypes, outZip)
         
     | 
| 
       375 
369 
     | 
    
         | 
| 
       376 
     | 
    
         
            -
                    return
         
     | 
| 
       377 
     | 
    
         
            -
             
     | 
| 
       378 
370 
     | 
    
         
             
                ##
         
     | 
| 
       379 
371 
     | 
    
         
             
                #  Internal Functions
         
     | 
| 
       380 
372 
     | 
    
         
             
                ##
         
     | 
| 
         @@ -454,8 +446,6 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       454 
446 
     | 
    
         
             
                    if temp := text[fStart:]:
         
     | 
| 
       455 
447 
     | 
    
         
             
                        par.addContent(self._textRunToXml(temp, xFmt, fClass, fLink))
         
     | 
| 
       456 
448 
     | 
    
         | 
| 
       457 
     | 
    
         
            -
                    return
         
     | 
| 
       458 
     | 
    
         
            -
             
     | 
| 
       459 
449 
     | 
    
         
             
                def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element:
         
     | 
| 
       460 
450 
     | 
    
         
             
                    """Encode the text run into XML."""
         
     | 
| 
       461 
451 
     | 
    
         
             
                    xR = xmlElement(_wTag("r"))
         
     | 
| 
         @@ -668,8 +658,6 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       668 
658 
     | 
    
         
             
                    for style in styles:
         
     | 
| 
       669 
659 
     | 
    
         
             
                        self._styles[style.styleId] = style
         
     | 
| 
       670 
660 
     | 
    
         | 
| 
       671 
     | 
    
         
            -
                    return
         
     | 
| 
       672 
     | 
    
         
            -
             
     | 
| 
       673 
661 
     | 
    
         
             
                def _nextRelId(self) -> str:
         
     | 
| 
       674 
662 
     | 
    
         
             
                    """Generate the next unique rId."""
         
     | 
| 
       675 
663 
     | 
    
         
             
                    return f"rId{len(self._rels) + 1}"
         
     | 
| 
         @@ -1054,6 +1042,10 @@ class ToDocX(Tokenizer): 
     | 
|
| 
       1054 
1042 
     | 
    
         | 
| 
       1055 
1043 
     | 
    
         | 
| 
       1056 
1044 
     | 
    
         
             
            class DocXParagraph:
         
     | 
| 
      
 1045 
     | 
    
         
            +
                """DocX Text Paragraph.
         
     | 
| 
      
 1046 
     | 
    
         
            +
             
     | 
| 
      
 1047 
     | 
    
         
            +
                This class holds a single paragraph of a DocX document.
         
     | 
| 
      
 1048 
     | 
    
         
            +
                """
         
     | 
| 
       1057 
1049 
     | 
    
         | 
| 
       1058 
1050 
     | 
    
         
             
                __slots__ = (
         
     | 
| 
       1059 
1051 
     | 
    
         
             
                    "_bottomMargin", "_breakAfter", "_breakBefore", "_content",
         
     | 
| 
         @@ -1073,7 +1065,6 @@ class DocXParagraph: 
     | 
|
| 
       1073 
1065 
     | 
    
         
             
                    self._breakBefore = False
         
     | 
| 
       1074 
1066 
     | 
    
         
             
                    self._breakAfter = False
         
     | 
| 
       1075 
1067 
     | 
    
         
             
                    self._footnoteRef = False
         
     | 
| 
       1076 
     | 
    
         
            -
                    return
         
     | 
| 
       1077 
1068 
     | 
    
         | 
| 
       1078 
1069 
     | 
    
         
             
                ##
         
     | 
| 
       1079 
1070 
     | 
    
         
             
                #  Properties
         
     | 
| 
         @@ -1091,53 +1082,43 @@ class DocXParagraph: 
     | 
|
| 
       1091 
1082 
     | 
    
         
             
                def setStyle(self, style: DocXParStyle | None) -> None:
         
     | 
| 
       1092 
1083 
     | 
    
         
             
                    """Set the paragraph style."""
         
     | 
| 
       1093 
1084 
     | 
    
         
             
                    self._style = style
         
     | 
| 
       1094 
     | 
    
         
            -
                    return
         
     | 
| 
       1095 
1085 
     | 
    
         | 
| 
       1096 
1086 
     | 
    
         
             
                def setAlignment(self, value: str) -> None:
         
     | 
| 
       1097 
1087 
     | 
    
         
             
                    """Set paragraph alignment."""
         
     | 
| 
       1098 
1088 
     | 
    
         
             
                    if value in ("left", "center", "right", "both"):
         
     | 
| 
       1099 
1089 
     | 
    
         
             
                        self._textAlign = value
         
     | 
| 
       1100 
     | 
    
         
            -
                    return
         
     | 
| 
       1101 
1090 
     | 
    
         | 
| 
       1102 
1091 
     | 
    
         
             
                def setMarginTop(self, value: float) -> None:
         
     | 
| 
       1103 
1092 
     | 
    
         
             
                    """Set margin above in pt."""
         
     | 
| 
       1104 
1093 
     | 
    
         
             
                    self._topMargin = value
         
     | 
| 
       1105 
     | 
    
         
            -
                    return
         
     | 
| 
       1106 
1094 
     | 
    
         | 
| 
       1107 
1095 
     | 
    
         
             
                def setMarginBottom(self, value: float) -> None:
         
     | 
| 
       1108 
1096 
     | 
    
         
             
                    """Set margin below in pt."""
         
     | 
| 
       1109 
1097 
     | 
    
         
             
                    self._bottomMargin = value
         
     | 
| 
       1110 
     | 
    
         
            -
                    return
         
     | 
| 
       1111 
1098 
     | 
    
         | 
| 
       1112 
1099 
     | 
    
         
             
                def setMarginLeft(self, value: float) -> None:
         
     | 
| 
       1113 
1100 
     | 
    
         
             
                    """Set margin left in pt."""
         
     | 
| 
       1114 
1101 
     | 
    
         
             
                    self._leftMargin = value
         
     | 
| 
       1115 
     | 
    
         
            -
                    return
         
     | 
| 
       1116 
1102 
     | 
    
         | 
| 
       1117 
1103 
     | 
    
         
             
                def setMarginRight(self, value: float) -> None:
         
     | 
| 
       1118 
1104 
     | 
    
         
             
                    """Set margin right in pt."""
         
     | 
| 
       1119 
1105 
     | 
    
         
             
                    self._rightMargin = value
         
     | 
| 
       1120 
     | 
    
         
            -
                    return
         
     | 
| 
       1121 
1106 
     | 
    
         | 
| 
       1122 
1107 
     | 
    
         
             
                def setIndentFirst(self, state: bool) -> None:
         
     | 
| 
       1123 
1108 
     | 
    
         
             
                    """Set first line indent."""
         
     | 
| 
       1124 
1109 
     | 
    
         
             
                    self._indentFirst = state
         
     | 
| 
       1125 
     | 
    
         
            -
                    return
         
     | 
| 
       1126 
1110 
     | 
    
         | 
| 
       1127 
1111 
     | 
    
         
             
                def setPageBreakBefore(self, state: bool) -> None:
         
     | 
| 
       1128 
1112 
     | 
    
         
             
                    """Set page break before flag."""
         
     | 
| 
       1129 
1113 
     | 
    
         
             
                    self._breakBefore = state
         
     | 
| 
       1130 
     | 
    
         
            -
                    return
         
     | 
| 
       1131 
1114 
     | 
    
         | 
| 
       1132 
1115 
     | 
    
         
             
                def setPageBreakAfter(self, state: bool) -> None:
         
     | 
| 
       1133 
1116 
     | 
    
         
             
                    """Set page break after flag."""
         
     | 
| 
       1134 
1117 
     | 
    
         
             
                    self._breakAfter = state
         
     | 
| 
       1135 
     | 
    
         
            -
                    return
         
     | 
| 
       1136 
1118 
     | 
    
         | 
| 
       1137 
1119 
     | 
    
         
             
                def setIsFootnote(self, state: bool) -> None:
         
     | 
| 
       1138 
1120 
     | 
    
         
             
                    """Set is footnote flag."""
         
     | 
| 
       1139 
1121 
     | 
    
         
             
                    self._footnoteRef = state
         
     | 
| 
       1140 
     | 
    
         
            -
                    return
         
     | 
| 
       1141 
1122 
     | 
    
         | 
| 
       1142 
1123 
     | 
    
         
             
                ##
         
     | 
| 
       1143 
1124 
     | 
    
         
             
                #  Methods
         
     | 
| 
         @@ -1146,10 +1127,9 @@ class DocXParagraph: 
     | 
|
| 
       1146 
1127 
     | 
    
         
             
                def addContent(self, run: ET.Element) -> None:
         
     | 
| 
       1147 
1128 
     | 
    
         
             
                    """Add a run segment to the paragraph."""
         
     | 
| 
       1148 
1129 
     | 
    
         
             
                    self._content.append(run)
         
     | 
| 
       1149 
     | 
    
         
            -
                    return
         
     | 
| 
       1150 
1130 
     | 
    
         | 
| 
       1151 
1131 
     | 
    
         
             
                def toXml(self, body: ET.Element) -> None:
         
     | 
| 
       1152 
     | 
    
         
            -
                    """ 
     | 
| 
      
 1132 
     | 
    
         
            +
                    """Generate the XML. Call after all content is set."""
         
     | 
| 
       1153 
1133 
     | 
    
         
             
                    if style := self._style:
         
     | 
| 
       1154 
1134 
     | 
    
         
             
                        xP = xmlSubElem(body, _wTag("p"))
         
     | 
| 
       1155 
1135 
     | 
    
         | 
| 
         @@ -1191,5 +1171,3 @@ class DocXParagraph: 
     | 
|
| 
       1191 
1171 
     | 
    
         
             
                        if self._breakAfter:
         
     | 
| 
       1192 
1172 
     | 
    
         
             
                            xR = xmlSubElem(xP, _wTag("r"))
         
     | 
| 
       1193 
1173 
     | 
    
         
             
                            xmlSubElem(xR, _wTag("br"), attrib={_wTag("type"): "page"})
         
     | 
| 
       1194 
     | 
    
         
            -
             
     | 
| 
       1195 
     | 
    
         
            -
                    return
         
     | 
    
        novelwriter/formats/tohtml.py
    CHANGED
    
    | 
         @@ -20,7 +20,7 @@ General Public License for more details. 
     | 
|
| 
       20 
20 
     | 
    
         | 
| 
       21 
21 
     | 
    
         
             
            You should have received a copy of the GNU General Public License
         
     | 
| 
       22 
22 
     | 
    
         
             
            along with this program. If not, see <https://www.gnu.org/licenses/>.
         
     | 
| 
       23 
     | 
    
         
            -
            """
         
     | 
| 
      
 23 
     | 
    
         
            +
            """  # noqa
         
     | 
| 
       24 
24 
     | 
    
         
             
            from __future__ import annotations
         
     | 
| 
       25 
25 
     | 
    
         | 
| 
       26 
26 
     | 
    
         
             
            import json
         
     | 
| 
         @@ -32,7 +32,7 @@ from typing import TYPE_CHECKING 
     | 
|
| 
       32 
32 
     | 
    
         
             
            from novelwriter.common import formatTimeStamp
         
     | 
| 
       33 
33 
     | 
    
         
             
            from novelwriter.constants import nwHtmlUnicode, nwStyles
         
     | 
| 
       34 
34 
     | 
    
         
             
            from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt, stripEscape
         
     | 
| 
       35 
     | 
    
         
            -
            from novelwriter.formats.tokenizer import Tokenizer
         
     | 
| 
      
 35 
     | 
    
         
            +
            from novelwriter.formats.tokenizer import COMMENT_BLOCKS, Tokenizer
         
     | 
| 
       36 
36 
     | 
    
         
             
            from novelwriter.types import FONT_STYLE, FONT_WEIGHTS, QtHexRgb
         
     | 
| 
       37 
37 
     | 
    
         | 
| 
       38 
38 
     | 
    
         
             
            if TYPE_CHECKING:
         
     | 
| 
         @@ -77,7 +77,7 @@ HTML_NONE = (0, "") 
     | 
|
| 
       77 
77 
     | 
    
         | 
| 
       78 
78 
     | 
    
         | 
| 
       79 
79 
     | 
    
         
             
            class ToHtml(Tokenizer):
         
     | 
| 
       80 
     | 
    
         
            -
                """Core: HTML Document Writer
         
     | 
| 
      
 80 
     | 
    
         
            +
                """Core: HTML Document Writer.
         
     | 
| 
       81 
81 
     | 
    
         | 
| 
       82 
82 
     | 
    
         
             
                Extend the Tokenizer class to writer HTML output. This class is
         
     | 
| 
       83 
83 
     | 
    
         
             
                also used by the Document Viewer, and Manuscript Build Preview.
         
     | 
| 
         @@ -90,7 +90,6 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       90 
90 
     | 
    
         
             
                    self._usedNotes: dict[str, int] = {}
         
     | 
| 
       91 
91 
     | 
    
         
             
                    self._usedFields: list[tuple[int, str]] = []
         
     | 
| 
       92 
92 
     | 
    
         
             
                    self.setReplaceUnicode(False)
         
     | 
| 
       93 
     | 
    
         
            -
                    return
         
     | 
| 
       94 
93 
     | 
    
         | 
| 
       95 
94 
     | 
    
         
             
                ##
         
     | 
| 
       96 
95 
     | 
    
         
             
                #  Setters
         
     | 
| 
         @@ -101,7 +100,6 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       101 
100 
     | 
    
         
             
                    class tags.
         
     | 
| 
       102 
101 
     | 
    
         
             
                    """
         
     | 
| 
       103 
102 
     | 
    
         
             
                    self._cssStyles = cssStyles
         
     | 
| 
       104 
     | 
    
         
            -
                    return
         
     | 
| 
       105 
103 
     | 
    
         | 
| 
       106 
104 
     | 
    
         
             
                def setReplaceUnicode(self, doReplace: bool) -> None:
         
     | 
| 
       107 
105 
     | 
    
         
             
                    """Set the translation map to either minimal or full unicode for
         
     | 
| 
         @@ -114,7 +112,6 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       114 
112 
     | 
    
         
             
                    if doReplace:
         
     | 
| 
       115 
113 
     | 
    
         
             
                        # Extend to all relevant Unicode characters
         
     | 
| 
       116 
114 
     | 
    
         
             
                        self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
         
     | 
| 
       117 
     | 
    
         
            -
                    return
         
     | 
| 
       118 
115 
     | 
    
         | 
| 
       119 
116 
     | 
    
         
             
                ##
         
     | 
| 
       120 
117 
     | 
    
         
             
                #  Class Methods
         
     | 
| 
         @@ -130,7 +127,6 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       130 
127 
     | 
    
         
             
                    """
         
     | 
| 
       131 
128 
     | 
    
         
             
                    super().doPreProcessing()
         
     | 
| 
       132 
129 
     | 
    
         
             
                    self._text = self._text.translate(self._trMap)
         
     | 
| 
       133 
     | 
    
         
            -
                    return
         
     | 
| 
       134 
130 
     | 
    
         | 
| 
       135 
131 
     | 
    
         
             
                def doConvert(self) -> None:
         
     | 
| 
       136 
132 
     | 
    
         
             
                    """Convert the list of text tokens into an HTML document."""
         
     | 
| 
         @@ -159,34 +155,33 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       159 
155 
     | 
    
         
             
                            # If we don't have formatting, we can do a plain replace
         
     | 
| 
       160 
156 
     | 
    
         
             
                            tText = tText.replace("<", "<").replace(">", ">")
         
     | 
| 
       161 
157 
     | 
    
         | 
| 
       162 
     | 
    
         
            -
                        # Styles
         
     | 
| 
      
 158 
     | 
    
         
            +
                        # Inline Styles
         
     | 
| 
       163 
159 
     | 
    
         
             
                        aStyle = []
         
     | 
| 
       164 
     | 
    
         
            -
                        if  
     | 
| 
       165 
     | 
    
         
            -
                             
     | 
| 
       166 
     | 
    
         
            -
             
     | 
| 
       167 
     | 
    
         
            -
                             
     | 
| 
       168 
     | 
    
         
            -
             
     | 
| 
       169 
     | 
    
         
            -
                             
     | 
| 
       170 
     | 
    
         
            -
             
     | 
| 
       171 
     | 
    
         
            -
                             
     | 
| 
       172 
     | 
    
         
            -
             
     | 
| 
       173 
     | 
    
         
            -
             
     | 
| 
       174 
     | 
    
         
            -
                             
     | 
| 
       175 
     | 
    
         
            -
             
     | 
| 
       176 
     | 
    
         
            -
                             
     | 
| 
       177 
     | 
    
         
            -
             
     | 
| 
       178 
     | 
    
         
            -
             
     | 
| 
       179 
     | 
    
         
            -
                             
     | 
| 
       180 
     | 
    
         
            -
             
     | 
| 
       181 
     | 
    
         
            -
                             
     | 
| 
       182 
     | 
    
         
            -
             
     | 
| 
       183 
     | 
    
         
            -
             
     | 
| 
       184 
     | 
    
         
            -
                             
     | 
| 
       185 
     | 
    
         
            -
             
     | 
| 
       186 
     | 
    
         
            -
                             
     | 
| 
       187 
     | 
    
         
            -
             
     | 
| 
       188 
     | 
    
         
            -
                             
     | 
| 
       189 
     | 
    
         
            -
                                aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
         
     | 
| 
      
 160 
     | 
    
         
            +
                        if tStyle & BlockFmt.LEFT:
         
     | 
| 
      
 161 
     | 
    
         
            +
                            aStyle.append("text-align: left;")
         
     | 
| 
      
 162 
     | 
    
         
            +
                        elif tStyle & BlockFmt.RIGHT:
         
     | 
| 
      
 163 
     | 
    
         
            +
                            aStyle.append("text-align: right;")
         
     | 
| 
      
 164 
     | 
    
         
            +
                        elif tStyle & BlockFmt.CENTRE:
         
     | 
| 
      
 165 
     | 
    
         
            +
                            aStyle.append("text-align: center;")
         
     | 
| 
      
 166 
     | 
    
         
            +
                        elif tStyle & BlockFmt.JUSTIFY:
         
     | 
| 
      
 167 
     | 
    
         
            +
                            aStyle.append("text-align: justify;")
         
     | 
| 
      
 168 
     | 
    
         
            +
             
     | 
| 
      
 169 
     | 
    
         
            +
                        if tStyle & BlockFmt.PBB:
         
     | 
| 
      
 170 
     | 
    
         
            +
                            aStyle.append("page-break-before: always;")
         
     | 
| 
      
 171 
     | 
    
         
            +
                        if tStyle & BlockFmt.PBA:
         
     | 
| 
      
 172 
     | 
    
         
            +
                            aStyle.append("page-break-after: always;")
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
                        if tStyle & BlockFmt.Z_BTM:
         
     | 
| 
      
 175 
     | 
    
         
            +
                            aStyle.append("margin-bottom: 0;")
         
     | 
| 
      
 176 
     | 
    
         
            +
                        if tStyle & BlockFmt.Z_TOP:
         
     | 
| 
      
 177 
     | 
    
         
            +
                            aStyle.append("margin-top: 0;")
         
     | 
| 
      
 178 
     | 
    
         
            +
             
     | 
| 
      
 179 
     | 
    
         
            +
                        if tStyle & BlockFmt.IND_L:
         
     | 
| 
      
 180 
     | 
    
         
            +
                            aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
         
     | 
| 
      
 181 
     | 
    
         
            +
                        if tStyle & BlockFmt.IND_R:
         
     | 
| 
      
 182 
     | 
    
         
            +
                            aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
         
     | 
| 
      
 183 
     | 
    
         
            +
                        if tStyle & BlockFmt.IND_T:
         
     | 
| 
      
 184 
     | 
    
         
            +
                            aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
         
     | 
| 
       190 
185 
     | 
    
         | 
| 
       191 
186 
     | 
    
         
             
                        if aStyle:
         
     | 
| 
       192 
187 
     | 
    
         
             
                            stVals = " ".join(aStyle)
         
     | 
| 
         @@ -229,7 +224,7 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       229 
224 
     | 
    
         
             
                        elif tType == BlockTyp.SKIP:
         
     | 
| 
       230 
225 
     | 
    
         
             
                            lines.append(f"<p{hStyle}> </p>\n")
         
     | 
| 
       231 
226 
     | 
    
         | 
| 
       232 
     | 
    
         
            -
                        elif tType  
     | 
| 
      
 227 
     | 
    
         
            +
                        elif tType in COMMENT_BLOCKS:
         
     | 
| 
       233 
228 
     | 
    
         
             
                            lines.append(f"<p class='comment'{hStyle}>{self._formatText(tText, tFmt)}</p>\n")
         
     | 
| 
       234 
229 
     | 
    
         | 
| 
       235 
230 
     | 
    
         
             
                        elif tType == BlockTyp.KEYWORD:
         
     | 
| 
         @@ -238,8 +233,6 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       238 
233 
     | 
    
         | 
| 
       239 
234 
     | 
    
         
             
                    self._pages.append("".join(lines))
         
     | 
| 
       240 
235 
     | 
    
         | 
| 
       241 
     | 
    
         
            -
                    return
         
     | 
| 
       242 
     | 
    
         
            -
             
     | 
| 
       243 
236 
     | 
    
         
             
                def closeDocument(self) -> None:
         
     | 
| 
       244 
237 
     | 
    
         
             
                    """Run close document tasks."""
         
     | 
| 
       245 
238 
     | 
    
         
             
                    # Replace fields if there are stats available
         
     | 
| 
         @@ -266,8 +259,6 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       266 
259 
     | 
    
         | 
| 
       267 
260 
     | 
    
         
             
                        self._pages.append("".join(lines))
         
     | 
| 
       268 
261 
     | 
    
         | 
| 
       269 
     | 
    
         
            -
                    return
         
     | 
| 
       270 
     | 
    
         
            -
             
     | 
| 
       271 
262 
     | 
    
         
             
                def saveDocument(self, path: Path) -> None:
         
     | 
| 
       272 
263 
     | 
    
         
             
                    """Save the data to an HTML file."""
         
     | 
| 
       273 
264 
     | 
    
         
             
                    if path.suffix.lower() == ".json":
         
     | 
| 
         @@ -288,37 +279,33 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       288 
279 
     | 
    
         
             
                            json.dump(data, fObj, indent=2)
         
     | 
| 
       289 
280 
     | 
    
         | 
| 
       290 
281 
     | 
    
         
             
                    else:
         
     | 
| 
      
 282 
     | 
    
         
            +
                        html = []
         
     | 
| 
      
 283 
     | 
    
         
            +
                        html.append("<!DOCTYPE html>")
         
     | 
| 
      
 284 
     | 
    
         
            +
                        html.append("<html>")
         
     | 
| 
      
 285 
     | 
    
         
            +
                        html.append("<head>")
         
     | 
| 
      
 286 
     | 
    
         
            +
                        html.append(f"<title>{self._project.data.name:s}</title>")
         
     | 
| 
      
 287 
     | 
    
         
            +
                        html.append("<meta charset='utf-8'>")
         
     | 
| 
      
 288 
     | 
    
         
            +
                        if self._cssStyles:
         
     | 
| 
      
 289 
     | 
    
         
            +
                            html.append("<meta name='viewport' content='width=device-width, initial-scale=1'>")
         
     | 
| 
      
 290 
     | 
    
         
            +
                            html.append("<style>")
         
     | 
| 
      
 291 
     | 
    
         
            +
                            html.extend(self.getStyleSheet())
         
     | 
| 
      
 292 
     | 
    
         
            +
                            html.append("</style>")
         
     | 
| 
      
 293 
     | 
    
         
            +
                        html.append("</head>")
         
     | 
| 
      
 294 
     | 
    
         
            +
                        html.append("<body>")
         
     | 
| 
      
 295 
     | 
    
         
            +
                        html.append(("".join(self._pages)).replace("\t", "	").rstrip())
         
     | 
| 
      
 296 
     | 
    
         
            +
                        html.append("</body>")
         
     | 
| 
      
 297 
     | 
    
         
            +
                        html.append("</html>\n")
         
     | 
| 
      
 298 
     | 
    
         
            +
             
     | 
| 
       291 
299 
     | 
    
         
             
                        with open(path, mode="w", encoding="utf-8") as fObj:
         
     | 
| 
       292 
     | 
    
         
            -
                            fObj.write((
         
     | 
| 
       293 
     | 
    
         
            -
                                "<!DOCTYPE html>\n"
         
     | 
| 
       294 
     | 
    
         
            -
                                "<html>\n"
         
     | 
| 
       295 
     | 
    
         
            -
                                "<head>\n"
         
     | 
| 
       296 
     | 
    
         
            -
                                "<meta charset='utf-8'>\n"
         
     | 
| 
       297 
     | 
    
         
            -
                                "<title>{title:s}</title>\n"
         
     | 
| 
       298 
     | 
    
         
            -
                                "<style>\n"
         
     | 
| 
       299 
     | 
    
         
            -
                                "{style:s}\n"
         
     | 
| 
       300 
     | 
    
         
            -
                                "</style>\n"
         
     | 
| 
       301 
     | 
    
         
            -
                                "</head>\n"
         
     | 
| 
       302 
     | 
    
         
            -
                                "<body>\n"
         
     | 
| 
       303 
     | 
    
         
            -
                                "{body:s}\n"
         
     | 
| 
       304 
     | 
    
         
            -
                                "</body>\n"
         
     | 
| 
       305 
     | 
    
         
            -
                                "</html>\n"
         
     | 
| 
       306 
     | 
    
         
            -
                            ).format(
         
     | 
| 
       307 
     | 
    
         
            -
                                title=self._project.data.name,
         
     | 
| 
       308 
     | 
    
         
            -
                                style="\n".join(self.getStyleSheet()),
         
     | 
| 
       309 
     | 
    
         
            -
                                body=("".join(self._pages)).replace("\t", "	").rstrip(),
         
     | 
| 
       310 
     | 
    
         
            -
                            ))
         
     | 
| 
      
 300 
     | 
    
         
            +
                            fObj.write("\n".join(html))
         
     | 
| 
       311 
301 
     | 
    
         | 
| 
       312 
302 
     | 
    
         
             
                    logger.info("Wrote file: %s", path)
         
     | 
| 
       313 
303 
     | 
    
         | 
| 
       314 
     | 
    
         
            -
                    return
         
     | 
| 
       315 
     | 
    
         
            -
             
     | 
| 
       316 
304 
     | 
    
         
             
                def replaceTabs(self, nSpaces: int = 8, spaceChar: str = " ") -> None:
         
     | 
| 
       317 
305 
     | 
    
         
             
                    """Replace tabs with spaces in the html."""
         
     | 
| 
       318 
306 
     | 
    
         
             
                    tabSpace = spaceChar*nSpaces
         
     | 
| 
       319 
307 
     | 
    
         
             
                    pages = [aLine.replace("\t", tabSpace) for aLine in self._pages]
         
     | 
| 
       320 
308 
     | 
    
         
             
                    self._pages = pages
         
     | 
| 
       321 
     | 
    
         
            -
                    return
         
     | 
| 
       322 
309 
     | 
    
         | 
| 
       323 
310 
     | 
    
         
             
                def getStyleSheet(self) -> list[str]:
         
     | 
| 
       324 
311 
     | 
    
         
             
                    """Generate a stylesheet for the current settings."""
         
     | 
| 
         @@ -410,7 +397,11 @@ class ToHtml(Tokenizer): 
     | 
|
| 
       410 
397 
     | 
    
         
             
                    # isn't already open, and only closed if it has previously been opened.
         
     | 
| 
       411 
398 
     | 
    
         
             
                    tags: list[tuple[int, str]] = []
         
     | 
| 
       412 
399 
     | 
    
         
             
                    state = dict.fromkeys(HTML_OPENER, False)
         
     | 
| 
      
 400 
     | 
    
         
            +
                    plain = not self._cssStyles
         
     | 
| 
       413 
401 
     | 
    
         
             
                    for pos, fmt, data in tFmt:
         
     | 
| 
      
 402 
     | 
    
         
            +
                        if plain and fmt in (TextFmt.COL_B, TextFmt.COL_E):
         
     | 
| 
      
 403 
     | 
    
         
            +
                            # We ignore colour tags if CSS is off
         
     | 
| 
      
 404 
     | 
    
         
            +
                            continue
         
     | 
| 
       414 
405 
     | 
    
         
             
                        if m := HTML_OPENER.get(fmt):
         
     | 
| 
       415 
406 
     | 
    
         
             
                            if not state.get(fmt, True):
         
     | 
| 
       416 
407 
     | 
    
         
             
                                if fmt == TextFmt.COL_B and (color := self._classes.get(data)):
         
     |