novelWriter 2.5.3__py3-none-any.whl → 2.6b2__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-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/METADATA +1 -1
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/RECORD +80 -60
- novelwriter/__init__.py +49 -10
- novelwriter/assets/i18n/project_en_GB.json +1 -0
- novelwriter/assets/icons/typicons_dark/icons.conf +8 -0
- novelwriter/assets/icons/typicons_dark/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_dark/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_dark/nw_toolbar.svg +5 -0
- novelwriter/assets/icons/typicons_light/icons.conf +8 -0
- novelwriter/assets/icons/typicons_light/mixed_copy.svg +4 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-bottom.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-left.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-right.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_margin-top.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-height.svg +6 -0
- novelwriter/assets/icons/typicons_light/mixed_size-width.svg +6 -0
- novelwriter/assets/icons/typicons_light/nw_toolbar.svg +5 -0
- novelwriter/assets/manual.pdf +0 -0
- novelwriter/assets/sample.zip +0 -0
- novelwriter/common.py +100 -2
- novelwriter/config.py +25 -15
- novelwriter/constants.py +168 -60
- novelwriter/core/buildsettings.py +66 -39
- novelwriter/core/coretools.py +145 -147
- novelwriter/core/docbuild.py +132 -170
- novelwriter/core/index.py +38 -37
- novelwriter/core/item.py +41 -8
- novelwriter/core/itemmodel.py +518 -0
- novelwriter/core/options.py +4 -1
- novelwriter/core/project.py +67 -89
- novelwriter/core/spellcheck.py +9 -14
- novelwriter/core/status.py +7 -5
- novelwriter/core/tree.py +268 -287
- novelwriter/dialogs/docmerge.py +7 -17
- novelwriter/dialogs/preferences.py +46 -33
- novelwriter/dialogs/projectsettings.py +5 -5
- novelwriter/enum.py +36 -23
- novelwriter/extensions/configlayout.py +27 -12
- novelwriter/extensions/modified.py +13 -1
- novelwriter/extensions/pagedsidebar.py +5 -5
- novelwriter/formats/shared.py +155 -0
- novelwriter/formats/todocx.py +1191 -0
- novelwriter/formats/tohtml.py +451 -0
- novelwriter/{core → formats}/tokenizer.py +487 -491
- novelwriter/formats/tomarkdown.py +217 -0
- novelwriter/{core → formats}/toodt.py +311 -432
- novelwriter/formats/toqdoc.py +484 -0
- novelwriter/formats/toraw.py +91 -0
- novelwriter/gui/doceditor.py +342 -284
- novelwriter/gui/dochighlight.py +96 -84
- novelwriter/gui/docviewer.py +88 -31
- novelwriter/gui/docviewerpanel.py +17 -25
- novelwriter/gui/editordocument.py +17 -2
- novelwriter/gui/itemdetails.py +25 -28
- novelwriter/gui/mainmenu.py +129 -63
- novelwriter/gui/noveltree.py +45 -47
- novelwriter/gui/outline.py +196 -249
- novelwriter/gui/projtree.py +594 -1241
- novelwriter/gui/search.py +9 -10
- novelwriter/gui/sidebar.py +7 -6
- novelwriter/gui/theme.py +10 -5
- novelwriter/guimain.py +100 -196
- novelwriter/shared.py +66 -27
- novelwriter/text/counting.py +2 -0
- novelwriter/text/patterns.py +168 -60
- novelwriter/tools/manusbuild.py +14 -12
- novelwriter/tools/manuscript.py +120 -78
- novelwriter/tools/manussettings.py +424 -291
- novelwriter/tools/welcome.py +4 -4
- novelwriter/tools/writingstats.py +3 -3
- novelwriter/types.py +23 -7
- novelwriter/core/tohtml.py +0 -530
- novelwriter/core/tomarkdown.py +0 -252
- novelwriter/core/toqdoc.py +0 -419
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/LICENSE.md +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/WHEEL +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/entry_points.txt +0 -0
- {novelWriter-2.5.3.dist-info → novelWriter-2.6b2.dist-info}/top_level.txt +0 -0
    
        novelwriter/common.py
    CHANGED
    
    | @@ -29,6 +29,7 @@ import unicodedata | |
| 29 29 | 
             
            import uuid
         | 
| 30 30 | 
             
            import xml.etree.ElementTree as ET
         | 
| 31 31 |  | 
| 32 | 
            +
            from collections.abc import Callable
         | 
| 32 33 | 
             
            from configparser import ConfigParser
         | 
| 33 34 | 
             
            from datetime import datetime
         | 
| 34 35 | 
             
            from pathlib import Path
         | 
| @@ -36,8 +37,8 @@ from typing import TYPE_CHECKING, Any, Literal, TypeVar | |
| 36 37 | 
             
            from urllib.parse import urljoin
         | 
| 37 38 | 
             
            from urllib.request import pathname2url
         | 
| 38 39 |  | 
| 39 | 
            -
            from PyQt5.QtCore import QCoreApplication, QUrl
         | 
| 40 | 
            -
            from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontInfo
         | 
| 40 | 
            +
            from PyQt5.QtCore import QCoreApplication, QMimeData, QUrl
         | 
| 41 | 
            +
            from PyQt5.QtGui import QColor, QDesktopServices, QFont, QFontDatabase, QFontInfo
         | 
| 41 42 |  | 
| 42 43 | 
             
            from novelwriter.constants import nwConst, nwLabels, nwUnicode, trConst
         | 
| 43 44 | 
             
            from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
         | 
| @@ -204,6 +205,14 @@ def checkIntTuple(value: int, valid: tuple | list | set, default: int) -> int: | |
| 204 205 | 
             
                return default
         | 
| 205 206 |  | 
| 206 207 |  | 
| 208 | 
            +
            def firstFloat(*args: Any) -> float:
         | 
| 209 | 
            +
                """Return the first value that is a float."""
         | 
| 210 | 
            +
                for arg in args:
         | 
| 211 | 
            +
                    if isinstance(arg, float):
         | 
| 212 | 
            +
                        return arg
         | 
| 213 | 
            +
                return 0.0
         | 
| 214 | 
            +
             | 
| 215 | 
            +
             | 
| 207 216 | 
             
            ##
         | 
| 208 217 | 
             
            #  Formatting Functions
         | 
| 209 218 | 
             
            ##
         | 
| @@ -406,6 +415,10 @@ def numberToRoman(value: int, toLower: bool = False) -> str: | |
| 406 415 | 
             
                return roman.lower() if toLower else roman
         | 
| 407 416 |  | 
| 408 417 |  | 
| 418 | 
            +
            ##
         | 
| 419 | 
            +
            #  Qt Helpers
         | 
| 420 | 
            +
            ##
         | 
| 421 | 
            +
             | 
| 409 422 | 
             
            def cssCol(col: QColor, alpha: int | None = None) -> str:
         | 
| 410 423 | 
             
                """Convert a QColor object to an rgba entry to use in CSS."""
         | 
| 411 424 | 
             
                return f"rgba({col.red()}, {col.green()}, {col.blue()}, {alpha or col.alpha()})"
         | 
| @@ -421,6 +434,48 @@ def describeFont(font: QFont) -> str: | |
| 421 434 | 
             
                return "Error"
         | 
| 422 435 |  | 
| 423 436 |  | 
| 437 | 
            +
            def fontMatcher(font: QFont) -> QFont:
         | 
| 438 | 
            +
                """Make sure the font is the correct family, if possible. This
         | 
| 439 | 
            +
                ensures that Qt doesn't re-use another font under the hood. The
         | 
| 440 | 
            +
                default Qt5 font matching algorithm doesn't handle well changing
         | 
| 441 | 
            +
                application fonts at runtime.
         | 
| 442 | 
            +
                """
         | 
| 443 | 
            +
                info = QFontInfo(font)
         | 
| 444 | 
            +
                if (famRequest := font.family()) != (famActual := info.family()):
         | 
| 445 | 
            +
                    logger.warning("Font mismatch: Requested '%s', but got '%s'", famRequest, famActual)
         | 
| 446 | 
            +
                    db = QFontDatabase()
         | 
| 447 | 
            +
                    if famRequest in db.families():
         | 
| 448 | 
            +
                        styleRequest, sizeRequest = font.styleName(), font.pointSize()
         | 
| 449 | 
            +
                        logger.info("Lookup: %s, %s, %d pt", famRequest, styleRequest, sizeRequest)
         | 
| 450 | 
            +
                        temp = db.font(famRequest, styleRequest, sizeRequest)
         | 
| 451 | 
            +
                        temp.setPointSize(sizeRequest)  # Make sure it isn't changed
         | 
| 452 | 
            +
                        famFound, styleFound, sizeFound = temp.family(), temp.styleName(), temp.pointSize()
         | 
| 453 | 
            +
                        if famFound == famRequest:
         | 
| 454 | 
            +
                            logger.info("Found: %s, %s, %d pt", famFound, styleFound, sizeFound)
         | 
| 455 | 
            +
                            return temp
         | 
| 456 | 
            +
                    logger.warning("Could not find a font match in the font database")
         | 
| 457 | 
            +
                    logger.warning("If you just changed font, you may need to restart the application")
         | 
| 458 | 
            +
                return font
         | 
| 459 | 
            +
             | 
| 460 | 
            +
             | 
| 461 | 
            +
            def qtLambda(func: Callable, *args: Any, **kwargs: Any) -> Callable:
         | 
| 462 | 
            +
                """A replacement for Python lambdas that works for Qt slots."""
         | 
| 463 | 
            +
                def wrapper(*a_: Any) -> None:
         | 
| 464 | 
            +
                    func(*args, **kwargs)
         | 
| 465 | 
            +
                return wrapper
         | 
| 466 | 
            +
             | 
| 467 | 
            +
             | 
| 468 | 
            +
            def encodeMimeHandles(mimeData: QMimeData, handles: list[str]) -> None:
         | 
| 469 | 
            +
                """Encode handles into a mime data object."""
         | 
| 470 | 
            +
                mimeData.setData(nwConst.MIME_HANDLE, b"|".join(h.encode() for h in handles))
         | 
| 471 | 
            +
                return
         | 
| 472 | 
            +
             | 
| 473 | 
            +
             | 
| 474 | 
            +
            def decodeMimeHandles(mimeData: QMimeData) -> list[str]:
         | 
| 475 | 
            +
                """Decode and split a mime data object with handles."""
         | 
| 476 | 
            +
                return mimeData.data(nwConst.MIME_HANDLE).data().decode().split("|")
         | 
| 477 | 
            +
             | 
| 478 | 
            +
             | 
| 424 479 | 
             
            ##
         | 
| 425 480 | 
             
            #  Encoder Functions
         | 
| 426 481 | 
             
            ##
         | 
| @@ -472,6 +527,10 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str: | |
| 472 527 | 
             
                return "".join(buffer)
         | 
| 473 528 |  | 
| 474 529 |  | 
| 530 | 
            +
            ##
         | 
| 531 | 
            +
            #  XML Helpers
         | 
| 532 | 
            +
            ##
         | 
| 533 | 
            +
             | 
| 475 534 | 
             
            def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
         | 
| 476 535 | 
             
                """A modified version of the XML indent function in the standard
         | 
| 477 536 | 
             
                library. It behaves more closely to how the one from lxml does.
         | 
| @@ -515,6 +574,45 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None: | |
| 515 574 | 
             
                return
         | 
| 516 575 |  | 
| 517 576 |  | 
| 577 | 
            +
            def xmlElement(
         | 
| 578 | 
            +
                tag: str,
         | 
| 579 | 
            +
                text: str | int | float | bool | None = None,
         | 
| 580 | 
            +
                *,
         | 
| 581 | 
            +
                attrib: dict | None = None,
         | 
| 582 | 
            +
                tail: str | None = None,
         | 
| 583 | 
            +
            ) -> ET.Element:
         | 
| 584 | 
            +
                """A custom implementation of Element with more arguments."""
         | 
| 585 | 
            +
                xSub = ET.Element(tag, attrib=attrib or {})
         | 
| 586 | 
            +
                if text is not None:
         | 
| 587 | 
            +
                    if isinstance(text, bool):
         | 
| 588 | 
            +
                        xSub.text = str(text).lower()
         | 
| 589 | 
            +
                    else:
         | 
| 590 | 
            +
                        xSub.text = str(text)
         | 
| 591 | 
            +
                if tail is not None:
         | 
| 592 | 
            +
                    xSub.tail = tail
         | 
| 593 | 
            +
                return xSub
         | 
| 594 | 
            +
             | 
| 595 | 
            +
             | 
| 596 | 
            +
            def xmlSubElem(
         | 
| 597 | 
            +
                parent: ET.Element,
         | 
| 598 | 
            +
                tag: str,
         | 
| 599 | 
            +
                text: str | int | float | bool | None = None,
         | 
| 600 | 
            +
                *,
         | 
| 601 | 
            +
                attrib: dict | None = None,
         | 
| 602 | 
            +
                tail: str | None = None,
         | 
| 603 | 
            +
            ) -> ET.Element:
         | 
| 604 | 
            +
                """A custom implementation of SubElement with more arguments."""
         | 
| 605 | 
            +
                xSub = ET.SubElement(parent, tag, attrib=attrib or {})
         | 
| 606 | 
            +
                if text is not None:
         | 
| 607 | 
            +
                    if isinstance(text, bool):
         | 
| 608 | 
            +
                        xSub.text = str(text).lower()
         | 
| 609 | 
            +
                    else:
         | 
| 610 | 
            +
                        xSub.text = str(text)
         | 
| 611 | 
            +
                if tail is not None:
         | 
| 612 | 
            +
                    xSub.tail = tail
         | 
| 613 | 
            +
                return xSub
         | 
| 614 | 
            +
             | 
| 615 | 
            +
             | 
| 518 616 | 
             
            ##
         | 
| 519 617 | 
             
            #  File and File System Functions
         | 
| 520 618 | 
             
            ##
         | 
    
        novelwriter/config.py
    CHANGED
    
    | @@ -40,7 +40,10 @@ from PyQt5.QtCore import ( | |
| 40 40 | 
             
            from PyQt5.QtGui import QFont, QFontDatabase
         | 
| 41 41 | 
             
            from PyQt5.QtWidgets import QApplication
         | 
| 42 42 |  | 
| 43 | 
            -
            from novelwriter.common import  | 
| 43 | 
            +
            from novelwriter.common import (
         | 
| 44 | 
            +
                NWConfigParser, checkInt, checkPath, describeFont, fontMatcher,
         | 
| 45 | 
            +
                formatTimeStamp
         | 
| 46 | 
            +
            )
         | 
| 44 47 | 
             
            from novelwriter.constants import nwFiles, nwUnicode
         | 
| 45 48 | 
             
            from novelwriter.error import formatException, logException
         | 
| 46 49 |  | 
| @@ -161,8 +164,9 @@ class Config: | |
| 161 164 |  | 
| 162 165 | 
             
                    self.dialogStyle     = 2        # Quote type to use for dialogue
         | 
| 163 166 | 
             
                    self.allowOpenDial   = True     # Allow open-ended dialogue quotes
         | 
| 164 | 
            -
                    self.narratorBreak   = ""       # Symbol to use for narrator break
         | 
| 165 167 | 
             
                    self.dialogLine      = ""       # Symbol to use for dialogue line
         | 
| 168 | 
            +
                    self.narratorBreak   = ""       # Symbol to use for narrator break
         | 
| 169 | 
            +
                    self.narratorDialog  = ""       # Symbol for alternating between dialogue and narrator
         | 
| 166 170 | 
             
                    self.altDialogOpen   = ""       # Alternative dialog symbol, open
         | 
| 167 171 | 
             
                    self.altDialogClose  = ""       # Alternative dialog symbol, close
         | 
| 168 172 | 
             
                    self.highlightEmph   = True     # Add colour to text emphasis
         | 
| @@ -252,6 +256,10 @@ class Config: | |
| 252 256 | 
             
                def hasError(self) -> bool:
         | 
| 253 257 | 
             
                    return self._hasError
         | 
| 254 258 |  | 
| 259 | 
            +
                @property
         | 
| 260 | 
            +
                def locale(self) -> QLocale:
         | 
| 261 | 
            +
                    return self._dLocale
         | 
| 262 | 
            +
             | 
| 255 263 | 
             
                @property
         | 
| 256 264 | 
             
                def recentProjects(self) -> RecentProjects:
         | 
| 257 265 | 
             
                    return self._recentProjects
         | 
| @@ -364,10 +372,11 @@ class Config: | |
| 364 372 | 
             
                def setGuiFont(self, value: QFont | str | None) -> None:
         | 
| 365 373 | 
             
                    """Update the GUI's font style from settings."""
         | 
| 366 374 | 
             
                    if isinstance(value, QFont):
         | 
| 367 | 
            -
                        self.guiFont = value
         | 
| 375 | 
            +
                        self.guiFont = fontMatcher(value)
         | 
| 368 376 | 
             
                    elif value and isinstance(value, str):
         | 
| 369 | 
            -
                         | 
| 370 | 
            -
                         | 
| 377 | 
            +
                        font = QFont()
         | 
| 378 | 
            +
                        font.fromString(value)
         | 
| 379 | 
            +
                        self.guiFont = fontMatcher(font)
         | 
| 371 380 | 
             
                    else:
         | 
| 372 381 | 
             
                        font = QFont()
         | 
| 373 382 | 
             
                        fontDB = QFontDatabase()
         | 
| @@ -377,11 +386,9 @@ class Config: | |
| 377 386 | 
             
                            font.setPointSize(10)
         | 
| 378 387 | 
             
                        else:
         | 
| 379 388 | 
             
                            font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
         | 
| 380 | 
            -
                        self.guiFont = font
         | 
| 389 | 
            +
                        self.guiFont = fontMatcher(font)
         | 
| 381 390 | 
             
                        logger.debug("GUI font set to: %s", describeFont(font))
         | 
| 382 | 
            -
             | 
| 383 391 | 
             
                    QApplication.setFont(self.guiFont)
         | 
| 384 | 
            -
             | 
| 385 392 | 
             
                    return
         | 
| 386 393 |  | 
| 387 394 | 
             
                def setTextFont(self, value: QFont | str | None) -> None:
         | 
| @@ -389,10 +396,11 @@ class Config: | |
| 389 396 | 
             
                    set to default font.
         | 
| 390 397 | 
             
                    """
         | 
| 391 398 | 
             
                    if isinstance(value, QFont):
         | 
| 392 | 
            -
                        self.textFont = value
         | 
| 399 | 
            +
                        self.textFont = fontMatcher(value)
         | 
| 393 400 | 
             
                    elif value and isinstance(value, str):
         | 
| 394 | 
            -
                         | 
| 395 | 
            -
                         | 
| 401 | 
            +
                        font = QFont()
         | 
| 402 | 
            +
                        font.fromString(value)
         | 
| 403 | 
            +
                        self.textFont = fontMatcher(font)
         | 
| 396 404 | 
             
                    else:
         | 
| 397 405 | 
             
                        fontDB = QFontDatabase()
         | 
| 398 406 | 
             
                        fontFam = fontDB.families()
         | 
| @@ -406,8 +414,8 @@ class Config: | |
| 406 414 | 
             
                            font.setPointSize(12)
         | 
| 407 415 | 
             
                        else:
         | 
| 408 416 | 
             
                            font = fontDB.systemFont(QFontDatabase.SystemFont.GeneralFont)
         | 
| 409 | 
            -
                        self.textFont = font
         | 
| 410 | 
            -
                        logger.debug("Text font set to: %s", describeFont( | 
| 417 | 
            +
                        self.textFont = fontMatcher(font)
         | 
| 418 | 
            +
                        logger.debug("Text font set to: %s", describeFont(self.textFont))
         | 
| 411 419 | 
             
                    return
         | 
| 412 420 |  | 
| 413 421 | 
             
                ##
         | 
| @@ -653,10 +661,11 @@ class Config: | |
| 653 661 | 
             
                    self.showFullPath    = conf.rdBool(sec, "showfullpath", self.showFullPath)
         | 
| 654 662 | 
             
                    self.dialogStyle     = conf.rdInt(sec, "dialogstyle", self.dialogStyle)
         | 
| 655 663 | 
             
                    self.allowOpenDial   = conf.rdBool(sec, "allowopendial", self.allowOpenDial)
         | 
| 664 | 
            +
                    self.dialogLine      = conf.rdStr(sec, "dialogline", self.dialogLine)
         | 
| 656 665 | 
             
                    self.narratorBreak   = conf.rdStr(sec, "narratorbreak", self.narratorBreak)
         | 
| 666 | 
            +
                    self.narratorDialog  = conf.rdStr(sec, "narratordialog", self.narratorDialog)
         | 
| 657 667 | 
             
                    self.altDialogOpen   = conf.rdStr(sec, "altdialogopen", self.altDialogOpen)
         | 
| 658 668 | 
             
                    self.altDialogClose  = conf.rdStr(sec, "altdialogclose", self.altDialogClose)
         | 
| 659 | 
            -
                    self.dialogLine      = conf.rdStr(sec, "dialogline", self.dialogLine)
         | 
| 660 669 | 
             
                    self.highlightEmph   = conf.rdBool(sec, "highlightemph", self.highlightEmph)
         | 
| 661 670 | 
             
                    self.stopWhenIdle    = conf.rdBool(sec, "stopwhenidle", self.stopWhenIdle)
         | 
| 662 671 | 
             
                    self.userIdleTime    = conf.rdInt(sec, "useridletime", self.userIdleTime)
         | 
| @@ -762,10 +771,11 @@ class Config: | |
| 762 771 | 
             
                        "showfullpath":    str(self.showFullPath),
         | 
| 763 772 | 
             
                        "dialogstyle":     str(self.dialogStyle),
         | 
| 764 773 | 
             
                        "allowopendial":   str(self.allowOpenDial),
         | 
| 774 | 
            +
                        "dialogline":      str(self.dialogLine),
         | 
| 765 775 | 
             
                        "narratorbreak":   str(self.narratorBreak),
         | 
| 776 | 
            +
                        "narratordialog":  str(self.narratorDialog),
         | 
| 766 777 | 
             
                        "altdialogopen":   str(self.altDialogOpen),
         | 
| 767 778 | 
             
                        "altdialogclose":  str(self.altDialogClose),
         | 
| 768 | 
            -
                        "dialogline":      str(self.dialogLine),
         | 
| 769 779 | 
             
                        "highlightemph":   str(self.highlightEmph),
         | 
| 770 780 | 
             
                        "stopwhenidle":    str(self.stopWhenIdle),
         | 
| 771 781 | 
             
                        "useridletime":    str(self.userIdleTime),
         | 
    
        novelwriter/constants.py
    CHANGED
    
    | @@ -53,6 +53,9 @@ class nwConst: | |
| 53 53 | 
             
                # Requests
         | 
| 54 54 | 
             
                USER_AGENT = "Mozilla/5.0 (compatible; novelWriter (Python))"
         | 
| 55 55 |  | 
| 56 | 
            +
                # Mime Types
         | 
| 57 | 
            +
                MIME_HANDLE = "text/vnd.novelwriter.handle"
         | 
| 58 | 
            +
             | 
| 56 59 | 
             
                # Gui Settings
         | 
| 57 60 | 
             
                STATUS_MSG_TIMEOUT = 15000  # milliseconds
         | 
| 58 61 | 
             
                MAX_SEARCH_RESULT = 1000
         | 
| @@ -60,11 +63,14 @@ class nwConst: | |
| 60 63 |  | 
| 61 64 | 
             
            class nwRegEx:
         | 
| 62 65 |  | 
| 66 | 
            +
                URL    = r"https?://(?:www\.|(?!www))[\w/()@:%_\+-.~#?&=]+"
         | 
| 67 | 
            +
                WORDS  = r"\b[^\s\-\+\/–—\[\]:]+\b"
         | 
| 68 | 
            +
                BREAK  = r"(?i)(?<!\\)(\[br\]\n?)"
         | 
| 63 69 | 
             
                FMT_EI = r"(?<![\w\\])(_)(?![\s_])(.+?)(?<![\s\\])(\1)(?!\w)"
         | 
| 64 70 | 
             
                FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
         | 
| 65 71 | 
             
                FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
         | 
| 66 | 
            -
                FMT_SC = r"(?i)(?<!\\)(\[ | 
| 67 | 
            -
                FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
         | 
| 72 | 
            +
                FMT_SC = r"(?i)(?<!\\)(\[(?:b|/b|i|/i|s|/s|u|/u|m|/m|sup|/sup|sub|/sub|br)\])"
         | 
| 73 | 
            +
                FMT_SV = r"(?i)(?<!\\)(\[(?:footnote|field):)(.+?)(?<!\\)(\])"
         | 
| 68 74 |  | 
| 69 75 |  | 
| 70 76 | 
             
            class nwShortcode:
         | 
| @@ -83,21 +89,49 @@ class nwShortcode: | |
| 83 89 | 
             
                SUP_C    = "[/sup]"
         | 
| 84 90 | 
             
                SUB_O    = "[sub]"
         | 
| 85 91 | 
             
                SUB_C    = "[/sub]"
         | 
| 92 | 
            +
                BREAK    = "[br]"
         | 
| 86 93 |  | 
| 87 94 | 
             
                FOOTNOTE_B = "[footnote:"
         | 
| 95 | 
            +
                FIELD_B    = "[field:"
         | 
| 88 96 |  | 
| 89 97 | 
             
                COMMENT_STYLES = {
         | 
| 90 98 | 
             
                    nwComment.FOOTNOTE: "[footnote:{0}]",
         | 
| 91 99 | 
             
                    nwComment.COMMENT:  "[comment:{0}]",
         | 
| 92 100 | 
             
                }
         | 
| 93 101 |  | 
| 102 | 
            +
                FIELD_VALUE = "[field:{0}]"
         | 
| 103 | 
            +
             | 
| 94 104 |  | 
| 95 | 
            -
            class  | 
| 105 | 
            +
            class nwStyles:
         | 
| 96 106 |  | 
| 97 107 | 
             
                H_VALID = ("H0", "H1", "H2", "H3", "H4")
         | 
| 98 108 | 
             
                H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
         | 
| 99 109 | 
             
                H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
         | 
| 100 110 |  | 
| 111 | 
            +
                T_NORMAL = 1.0
         | 
| 112 | 
            +
                T_SMALL  = 0.8
         | 
| 113 | 
            +
             | 
| 114 | 
            +
                T_LABEL = {
         | 
| 115 | 
            +
                    "H0": QT_TRANSLATE_NOOP("Constant", "Title"),
         | 
| 116 | 
            +
                    "H1": QT_TRANSLATE_NOOP("Constant", "Heading 1 (Partition)"),
         | 
| 117 | 
            +
                    "H2": QT_TRANSLATE_NOOP("Constant", "Heading 2 (Chapter)"),
         | 
| 118 | 
            +
                    "H3": QT_TRANSLATE_NOOP("Constant", "Heading 3 (Scene)"),
         | 
| 119 | 
            +
                    "H4": QT_TRANSLATE_NOOP("Constant", "Heading 4 (Section)"),
         | 
| 120 | 
            +
                    "TT": QT_TRANSLATE_NOOP("Constant", "Text Paragraph"),
         | 
| 121 | 
            +
                    "SP": QT_TRANSLATE_NOOP("Constant", "Scene Separator"),
         | 
| 122 | 
            +
                }
         | 
| 123 | 
            +
                T_MARGIN = {
         | 
| 124 | 
            +
                    "H0": (1.50, 0.60),  # Title margins (top, bottom)
         | 
| 125 | 
            +
                    "H1": (1.50, 0.60),  # Heading 1 margins (top, bottom)
         | 
| 126 | 
            +
                    "H2": (1.50, 0.60),  # Heading 2 margins (top, bottom)
         | 
| 127 | 
            +
                    "H3": (1.20, 0.60),  # Heading 3 margins (top, bottom)
         | 
| 128 | 
            +
                    "H4": (1.20, 0.60),  # Heading 4 margins (top, bottom)
         | 
| 129 | 
            +
                    "TT": (0.00, 0.60),  # Text margins (top, bottom)
         | 
| 130 | 
            +
                    "SP": (1.20, 1.20),  # Separator margins (top, bottom)
         | 
| 131 | 
            +
                    "MT": (0.00, 0.60),  # Meta margins (top, bottom)
         | 
| 132 | 
            +
                    "FT": (1.40, 0.40),  # Footnote margins (left, bottom)
         | 
| 133 | 
            +
                }
         | 
| 134 | 
            +
             | 
| 101 135 |  | 
| 102 136 | 
             
            class nwFiles:
         | 
| 103 137 |  | 
| @@ -121,34 +155,40 @@ class nwFiles: | |
| 121 155 |  | 
| 122 156 | 
             
            class nwKeyWords:
         | 
| 123 157 |  | 
| 124 | 
            -
                TAG_KEY | 
| 125 | 
            -
                POV_KEY | 
| 126 | 
            -
                FOCUS_KEY | 
| 127 | 
            -
                CHAR_KEY | 
| 128 | 
            -
                PLOT_KEY | 
| 129 | 
            -
                TIME_KEY | 
| 130 | 
            -
                WORLD_KEY | 
| 131 | 
            -
                OBJECT_KEY | 
| 132 | 
            -
                ENTITY_KEY | 
| 133 | 
            -
                CUSTOM_KEY | 
| 158 | 
            +
                TAG_KEY     = "@tag"
         | 
| 159 | 
            +
                POV_KEY     = "@pov"
         | 
| 160 | 
            +
                FOCUS_KEY   = "@focus"
         | 
| 161 | 
            +
                CHAR_KEY    = "@char"
         | 
| 162 | 
            +
                PLOT_KEY    = "@plot"
         | 
| 163 | 
            +
                TIME_KEY    = "@time"
         | 
| 164 | 
            +
                WORLD_KEY   = "@location"
         | 
| 165 | 
            +
                OBJECT_KEY  = "@object"
         | 
| 166 | 
            +
                ENTITY_KEY  = "@entity"
         | 
| 167 | 
            +
                CUSTOM_KEY  = "@custom"
         | 
| 168 | 
            +
                STORY_KEY   = "@story"
         | 
| 169 | 
            +
                MENTION_KEY = "@mention"
         | 
| 170 | 
            +
             | 
| 171 | 
            +
                # Note: The order here affects the order of menu entries
         | 
| 172 | 
            +
                ALL_KEYS = [
         | 
| 173 | 
            +
                    TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
         | 
| 174 | 
            +
                    OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
         | 
| 175 | 
            +
                ]
         | 
| 134 176 |  | 
| 135 177 | 
             
                # Set of Valid Keys
         | 
| 136 | 
            -
                VALID_KEYS =  | 
| 137 | 
            -
                    TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
         | 
| 138 | 
            -
                    WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
         | 
| 139 | 
            -
                }
         | 
| 178 | 
            +
                VALID_KEYS = set(ALL_KEYS)
         | 
| 140 179 |  | 
| 141 180 | 
             
                # Map from Keys to Item Class
         | 
| 142 181 | 
             
                KEY_CLASS = {
         | 
| 143 | 
            -
                    POV_KEY: | 
| 144 | 
            -
                    FOCUS_KEY: | 
| 145 | 
            -
                    CHAR_KEY: | 
| 146 | 
            -
                    PLOT_KEY: | 
| 147 | 
            -
                    TIME_KEY: | 
| 148 | 
            -
                    WORLD_KEY: | 
| 149 | 
            -
                    OBJECT_KEY: | 
| 150 | 
            -
                    ENTITY_KEY: | 
| 151 | 
            -
                    CUSTOM_KEY: | 
| 182 | 
            +
                    POV_KEY:     nwItemClass.CHARACTER,
         | 
| 183 | 
            +
                    FOCUS_KEY:   nwItemClass.CHARACTER,
         | 
| 184 | 
            +
                    CHAR_KEY:    nwItemClass.CHARACTER,
         | 
| 185 | 
            +
                    PLOT_KEY:    nwItemClass.PLOT,
         | 
| 186 | 
            +
                    TIME_KEY:    nwItemClass.TIMELINE,
         | 
| 187 | 
            +
                    WORLD_KEY:   nwItemClass.WORLD,
         | 
| 188 | 
            +
                    OBJECT_KEY:  nwItemClass.OBJECT,
         | 
| 189 | 
            +
                    ENTITY_KEY:  nwItemClass.ENTITY,
         | 
| 190 | 
            +
                    CUSTOM_KEY:  nwItemClass.CUSTOM,
         | 
| 191 | 
            +
                    STORY_KEY:   nwItemClass.NOVEL,
         | 
| 152 192 | 
             
                }
         | 
| 153 193 |  | 
| 154 194 |  | 
| @@ -165,6 +205,29 @@ class nwLists: | |
| 165 205 | 
             
                ]
         | 
| 166 206 |  | 
| 167 207 |  | 
| 208 | 
            +
            class nwStats:
         | 
| 209 | 
            +
             | 
| 210 | 
            +
                CHARS_ALL    = "allChars"
         | 
| 211 | 
            +
                CHARS_TEXT   = "textChars"
         | 
| 212 | 
            +
                CHARS_TITLE  = "titleChars"
         | 
| 213 | 
            +
                PARAGRAPHS   = "paragraphCount"
         | 
| 214 | 
            +
                TITLES       = "titleCount"
         | 
| 215 | 
            +
                WCHARS_ALL   = "allWordChars"
         | 
| 216 | 
            +
                WCHARS_TEXT  = "textWordChars"
         | 
| 217 | 
            +
                WCHARS_TITLE = "titleWordChars"
         | 
| 218 | 
            +
                WORDS_ALL    = "allWords"
         | 
| 219 | 
            +
                WORDS_TEXT   = "textWords"
         | 
| 220 | 
            +
                WORDS_TITLE  = "titleWords"
         | 
| 221 | 
            +
             | 
| 222 | 
            +
                # Note: The order here affects the order of menu entries
         | 
| 223 | 
            +
                ALL_FIELDS = [
         | 
| 224 | 
            +
                    WORDS_ALL, WORDS_TEXT, WORDS_TITLE,
         | 
| 225 | 
            +
                    CHARS_ALL, CHARS_TEXT, CHARS_TITLE,
         | 
| 226 | 
            +
                    WCHARS_ALL, WCHARS_TEXT, WCHARS_TITLE,
         | 
| 227 | 
            +
                    PARAGRAPHS, TITLES,
         | 
| 228 | 
            +
                ]
         | 
| 229 | 
            +
             | 
| 230 | 
            +
             | 
| 168 231 | 
             
            class nwLabels:
         | 
| 169 232 |  | 
| 170 233 | 
             
                CLASS_NAME = {
         | 
| @@ -211,54 +274,94 @@ class nwLabels: | |
| 211 274 | 
             
                    "doc_h4":   QT_TRANSLATE_NOOP("Constant", "Novel Section"),
         | 
| 212 275 | 
             
                    "note":     QT_TRANSLATE_NOOP("Constant", "Project Note"),
         | 
| 213 276 | 
             
                }
         | 
| 277 | 
            +
                ACTIVE_NAME = {
         | 
| 278 | 
            +
                    "checked":   QT_TRANSLATE_NOOP("Constant", "Active"),
         | 
| 279 | 
            +
                    "unchecked": QT_TRANSLATE_NOOP("Constant", "Inactive"),
         | 
| 280 | 
            +
                }
         | 
| 214 281 | 
             
                KEY_NAME = {
         | 
| 215 | 
            -
                    nwKeyWords.TAG_KEY: | 
| 216 | 
            -
                    nwKeyWords.POV_KEY: | 
| 217 | 
            -
                    nwKeyWords.FOCUS_KEY: | 
| 218 | 
            -
                    nwKeyWords.CHAR_KEY: | 
| 219 | 
            -
                    nwKeyWords.PLOT_KEY: | 
| 220 | 
            -
                    nwKeyWords.TIME_KEY: | 
| 221 | 
            -
                    nwKeyWords.WORLD_KEY: | 
| 222 | 
            -
                    nwKeyWords.OBJECT_KEY: | 
| 223 | 
            -
                    nwKeyWords.ENTITY_KEY: | 
| 224 | 
            -
                    nwKeyWords.CUSTOM_KEY: | 
| 282 | 
            +
                    nwKeyWords.TAG_KEY:     QT_TRANSLATE_NOOP("Constant", "Tag"),
         | 
| 283 | 
            +
                    nwKeyWords.POV_KEY:     QT_TRANSLATE_NOOP("Constant", "Point of View"),
         | 
| 284 | 
            +
                    nwKeyWords.FOCUS_KEY:   QT_TRANSLATE_NOOP("Constant", "Focus"),
         | 
| 285 | 
            +
                    nwKeyWords.CHAR_KEY:    QT_TRANSLATE_NOOP("Constant", "Characters"),
         | 
| 286 | 
            +
                    nwKeyWords.PLOT_KEY:    QT_TRANSLATE_NOOP("Constant", "Plot"),
         | 
| 287 | 
            +
                    nwKeyWords.TIME_KEY:    QT_TRANSLATE_NOOP("Constant", "Timeline"),
         | 
| 288 | 
            +
                    nwKeyWords.WORLD_KEY:   QT_TRANSLATE_NOOP("Constant", "Locations"),
         | 
| 289 | 
            +
                    nwKeyWords.OBJECT_KEY:  QT_TRANSLATE_NOOP("Constant", "Objects"),
         | 
| 290 | 
            +
                    nwKeyWords.ENTITY_KEY:  QT_TRANSLATE_NOOP("Constant", "Entities"),
         | 
| 291 | 
            +
                    nwKeyWords.CUSTOM_KEY:  QT_TRANSLATE_NOOP("Constant", "Custom"),
         | 
| 292 | 
            +
                    nwKeyWords.STORY_KEY:   QT_TRANSLATE_NOOP("Constant", "Story"),
         | 
| 293 | 
            +
                    nwKeyWords.MENTION_KEY: QT_TRANSLATE_NOOP("Constant", "Mentions"),
         | 
| 294 | 
            +
                }
         | 
| 295 | 
            +
                KEY_SHORTCUT = {
         | 
| 296 | 
            +
                    nwKeyWords.TAG_KEY:     "Ctrl+K, G",
         | 
| 297 | 
            +
                    nwKeyWords.POV_KEY:     "Ctrl+K, V",
         | 
| 298 | 
            +
                    nwKeyWords.FOCUS_KEY:   "Ctrl+K, F",
         | 
| 299 | 
            +
                    nwKeyWords.CHAR_KEY:    "Ctrl+K, C",
         | 
| 300 | 
            +
                    nwKeyWords.PLOT_KEY:    "Ctrl+K, P",
         | 
| 301 | 
            +
                    nwKeyWords.TIME_KEY:    "Ctrl+K, T",
         | 
| 302 | 
            +
                    nwKeyWords.WORLD_KEY:   "Ctrl+K, L",
         | 
| 303 | 
            +
                    nwKeyWords.OBJECT_KEY:  "Ctrl+K, O",
         | 
| 304 | 
            +
                    nwKeyWords.ENTITY_KEY:  "Ctrl+K, E",
         | 
| 305 | 
            +
                    nwKeyWords.CUSTOM_KEY:  "Ctrl+K, X",
         | 
| 306 | 
            +
                    nwKeyWords.STORY_KEY:   "Ctrl+K, N",
         | 
| 307 | 
            +
                    nwKeyWords.MENTION_KEY: "Ctrl+K, M",
         | 
| 225 308 | 
             
                }
         | 
| 226 309 | 
             
                OUTLINE_COLS = {
         | 
| 227 | 
            -
                    nwOutline.TITLE: | 
| 228 | 
            -
                    nwOutline.LEVEL: | 
| 229 | 
            -
                    nwOutline.LABEL: | 
| 230 | 
            -
                    nwOutline.LINE: | 
| 231 | 
            -
                    nwOutline. | 
| 232 | 
            -
                    nwOutline. | 
| 233 | 
            -
                    nwOutline. | 
| 234 | 
            -
                    nwOutline. | 
| 235 | 
            -
                    nwOutline. | 
| 236 | 
            -
                    nwOutline. | 
| 237 | 
            -
                    nwOutline. | 
| 238 | 
            -
                    nwOutline. | 
| 239 | 
            -
                    nwOutline. | 
| 240 | 
            -
                    nwOutline. | 
| 241 | 
            -
                    nwOutline. | 
| 242 | 
            -
                    nwOutline. | 
| 243 | 
            -
                    nwOutline. | 
| 310 | 
            +
                    nwOutline.TITLE:   QT_TRANSLATE_NOOP("Constant", "Title"),
         | 
| 311 | 
            +
                    nwOutline.LEVEL:   QT_TRANSLATE_NOOP("Constant", "Level"),
         | 
| 312 | 
            +
                    nwOutline.LABEL:   QT_TRANSLATE_NOOP("Constant", "Document"),
         | 
| 313 | 
            +
                    nwOutline.LINE:    QT_TRANSLATE_NOOP("Constant", "Line"),
         | 
| 314 | 
            +
                    nwOutline.STATUS:  QT_TRANSLATE_NOOP("Constant", "Status"),
         | 
| 315 | 
            +
                    nwOutline.CCOUNT:  QT_TRANSLATE_NOOP("Constant", "Chars"),
         | 
| 316 | 
            +
                    nwOutline.WCOUNT:  QT_TRANSLATE_NOOP("Constant", "Words"),
         | 
| 317 | 
            +
                    nwOutline.PCOUNT:  QT_TRANSLATE_NOOP("Constant", "Pars"),
         | 
| 318 | 
            +
                    nwOutline.POV:     QT_TRANSLATE_NOOP("Constant", "POV"),
         | 
| 319 | 
            +
                    nwOutline.FOCUS:   QT_TRANSLATE_NOOP("Constant", "Focus"),
         | 
| 320 | 
            +
                    nwOutline.CHAR:    KEY_NAME[nwKeyWords.CHAR_KEY],
         | 
| 321 | 
            +
                    nwOutline.PLOT:    KEY_NAME[nwKeyWords.PLOT_KEY],
         | 
| 322 | 
            +
                    nwOutline.WORLD:   KEY_NAME[nwKeyWords.WORLD_KEY],
         | 
| 323 | 
            +
                    nwOutline.TIME:    KEY_NAME[nwKeyWords.TIME_KEY],
         | 
| 324 | 
            +
                    nwOutline.OBJECT:  KEY_NAME[nwKeyWords.OBJECT_KEY],
         | 
| 325 | 
            +
                    nwOutline.ENTITY:  KEY_NAME[nwKeyWords.ENTITY_KEY],
         | 
| 326 | 
            +
                    nwOutline.CUSTOM:  KEY_NAME[nwKeyWords.CUSTOM_KEY],
         | 
| 327 | 
            +
                    nwOutline.STORY:   KEY_NAME[nwKeyWords.STORY_KEY],
         | 
| 328 | 
            +
                    nwOutline.MENTION: KEY_NAME[nwKeyWords.MENTION_KEY],
         | 
| 329 | 
            +
                    nwOutline.SYNOP:   QT_TRANSLATE_NOOP("Constant", "Synopsis"),
         | 
| 330 | 
            +
                }
         | 
| 331 | 
            +
                STATS_NAME = {
         | 
| 332 | 
            +
                    nwStats.CHARS_ALL:    QT_TRANSLATE_NOOP("Constant", "Characters"),
         | 
| 333 | 
            +
                    nwStats.CHARS_TEXT:   QT_TRANSLATE_NOOP("Constant", "Characters in Text"),
         | 
| 334 | 
            +
                    nwStats.CHARS_TITLE:  QT_TRANSLATE_NOOP("Constant", "Characters in Headings"),
         | 
| 335 | 
            +
                    nwStats.PARAGRAPHS:   QT_TRANSLATE_NOOP("Constant", "Paragraphs"),
         | 
| 336 | 
            +
                    nwStats.TITLES:       QT_TRANSLATE_NOOP("Constant", "Headings"),
         | 
| 337 | 
            +
                    nwStats.WCHARS_ALL:   QT_TRANSLATE_NOOP("Constant", "Characters, No Spaces"),
         | 
| 338 | 
            +
                    nwStats.WCHARS_TEXT:  QT_TRANSLATE_NOOP("Constant", "Characters in Text, No Spaces"),
         | 
| 339 | 
            +
                    nwStats.WCHARS_TITLE: QT_TRANSLATE_NOOP("Constant", "Characters in Headings, No Spaces"),
         | 
| 340 | 
            +
                    nwStats.WORDS_ALL:    QT_TRANSLATE_NOOP("Constant", "Words"),
         | 
| 341 | 
            +
                    nwStats.WORDS_TEXT:   QT_TRANSLATE_NOOP("Constant", "Words in Text"),
         | 
| 342 | 
            +
                    nwStats.WORDS_TITLE:  QT_TRANSLATE_NOOP("Constant", "Words in Headings"),
         | 
| 244 343 | 
             
                }
         | 
| 245 344 | 
             
                BUILD_FMT = {
         | 
| 246 345 | 
             
                    nwBuildFmt.ODT:    QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
         | 
| 247 346 | 
             
                    nwBuildFmt.FODT:   QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
         | 
| 248 | 
            -
                    nwBuildFmt. | 
| 347 | 
            +
                    nwBuildFmt.DOCX:   QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"),
         | 
| 348 | 
            +
                    nwBuildFmt.HTML:   QT_TRANSLATE_NOOP("Constant", "HTML 5 (.html)"),
         | 
| 249 349 | 
             
                    nwBuildFmt.NWD:    QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
         | 
| 250 350 | 
             
                    nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
         | 
| 251 351 | 
             
                    nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
         | 
| 252 | 
            -
                    nwBuildFmt. | 
| 352 | 
            +
                    nwBuildFmt.PDF:    QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"),
         | 
| 353 | 
            +
                    nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + HTML 5 (.json)"),
         | 
| 253 354 | 
             
                    nwBuildFmt.J_NWD:  QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"),
         | 
| 254 355 | 
             
                }
         | 
| 255 356 | 
             
                BUILD_EXT = {
         | 
| 256 357 | 
             
                    nwBuildFmt.ODT:    ".odt",
         | 
| 257 358 | 
             
                    nwBuildFmt.FODT:   ".fodt",
         | 
| 359 | 
            +
                    nwBuildFmt.DOCX:   ".docx",
         | 
| 258 360 | 
             
                    nwBuildFmt.HTML:   ".html",
         | 
| 259 361 | 
             
                    nwBuildFmt.NWD:    ".txt",
         | 
| 260 362 | 
             
                    nwBuildFmt.STD_MD: ".md",
         | 
| 261 363 | 
             
                    nwBuildFmt.EXT_MD: ".md",
         | 
| 364 | 
            +
                    nwBuildFmt.PDF:    ".pdf",
         | 
| 262 365 | 
             
                    nwBuildFmt.J_HTML: ".json",
         | 
| 263 366 | 
             
                    nwBuildFmt.J_NWD:  ".json",
         | 
| 264 367 | 
             
                }
         | 
| @@ -343,11 +446,11 @@ class nwHeadFmt: | |
| 343 446 | 
             
                    CHAR_POV, CHAR_FOCUS
         | 
| 344 447 | 
             
                ]
         | 
| 345 448 |  | 
| 346 | 
            -
                #  | 
| 347 | 
            -
                 | 
| 348 | 
            -
                 | 
| 349 | 
            -
                 | 
| 350 | 
            -
                 | 
| 449 | 
            +
                # Document Page Header
         | 
| 450 | 
            +
                DOC_PROJECT = "{Project}"
         | 
| 451 | 
            +
                DOC_AUTHOR = "{Author}"
         | 
| 452 | 
            +
                DOC_PAGE = "{Page}"
         | 
| 453 | 
            +
                DOC_AUTO = "{Project} / {Author} / {Page}"
         | 
| 351 454 |  | 
| 352 455 |  | 
| 353 456 | 
             
            class nwQuotes:
         | 
| @@ -449,6 +552,11 @@ class nwUnicode: | |
| 449 552 | 
             
                U_LTRI   = "\u25c0"  # Left-pointing triangle
         | 
| 450 553 | 
             
                U_LTRIS  = "\u25c2"  # Left-pointing triangle, small
         | 
| 451 554 |  | 
| 555 | 
            +
                # Special
         | 
| 556 | 
            +
                U_UNKN   = "\ufffd"  # Unknown character
         | 
| 557 | 
            +
                U_NAC1   = "\ufffe"  # Not a character
         | 
| 558 | 
            +
                U_NAC2   = "\uffff"  # Not a character
         | 
| 559 | 
            +
             | 
| 452 560 | 
             
                # HTML Equivalents
         | 
| 453 561 | 
             
                # ================
         | 
| 454 562 |  |