From 5befd244e68c0a8e7cbc6fbb80c9ce54c88a93e3 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 21 Mar 2024 16:42:58 +0100
Subject: [PATCH] Move outline generation to Tokenizer class instead
---
novelwriter/core/docbuild.py | 22 +++++++--
novelwriter/core/index.py | 8 ++--
novelwriter/core/project.py | 4 +-
novelwriter/core/sessions.py | 4 +-
novelwriter/core/status.py | 8 ++--
novelwriter/core/tohtml.py | 23 ++-------
novelwriter/core/tokenizer.py | 79 +++++++++++++++++++++----------
novelwriter/core/tree.py | 4 +-
novelwriter/gui/docviewer.py | 10 ++--
novelwriter/gui/editordocument.py | 4 +-
novelwriter/tools/manuscript.py | 30 +++++-------
11 files changed, 109 insertions(+), 87 deletions(-)
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index ce5afdd7..3c1d6951 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -52,7 +52,10 @@ class NWBuildDocument:
manuscript, based on a build definition object (BuildSettings).
"""
- __slots__ = ("_project", "_build", "_queue", "_error", "_cache", "_count", "_preview")
+ __slots__ = (
+ "_project", "_build", "_queue", "_error", "_cache", "_count",
+ "_outline", "_preview"
+ )
def __init__(self, project: NWProject, build: BuildSettings) -> None:
self._project = project
@@ -61,6 +64,7 @@ class NWBuildDocument:
self._error = None
self._cache = None
self._count = False
+ self._outline = False
self._preview = False
return
@@ -86,13 +90,21 @@ class NWBuildDocument:
##
def setCountEnabled(self, state: bool) -> None:
- """Turn on/off stats counting for builds."""
+ """Turn on/off stats for builds."""
self._count = state
return
+ def setBuildOutline(self, state: bool) -> None:
+ """Turn on/off outline for builds."""
+ self._outline = state
+ return
+
def setPreviewMode(self, state: bool) -> None:
- """Set the preview mode of the build. Implies count mode."""
+ """Set the preview mode of the build. This also enables stats
+ count and outline mode.
+ """
self._preview = state
+ self._outline = state
self._count = state
return
@@ -364,12 +376,16 @@ class NWBuildDocument:
bldObj.doConvert()
if self._count:
bldObj.countStats()
+ if self._outline:
+ bldObj.buildOutline()
elif tItem.isFileType():
bldObj.setText(tHandle)
bldObj.doPreProcessing()
bldObj.tokenizeText()
if self._count:
bldObj.countStats()
+ if self._outline:
+ bldObj.buildOutline()
if convert:
bldObj.doConvert()
else:
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 66e2130b..7160ea03 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -33,7 +33,7 @@ import logging
from time import time
from typing import TYPE_CHECKING
from pathlib import Path
-from collections.abc import Generator, ItemsView, Iterable
+from collections.abc import ItemsView, Iterable
from novelwriter import SHARED
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
@@ -523,7 +523,7 @@ class NWIndex:
return tItem[sTitle]
return None
- def iterItemHeadings(self, tHandle: str) -> Generator[str, IndexHeading]:
+ def iterItemHeadings(self, tHandle: str) -> Iterable[tuple[str, IndexHeading]]:
"""Get all headings for a specific item."""
if tItem := self._itemIndex[tHandle]:
yield from tItem.items()
@@ -531,7 +531,7 @@ class NWIndex:
def novelStructure(
self, rootHandle: str | None = None, activeOnly: bool = True
- ) -> Generator[tuple[str, str, str, IndexHeading]]:
+ ) -> Iterable[tuple[str, str, str, IndexHeading]]:
"""Iterate over all titles in the novel, in the correct order as
they appear in the tree view and in the respective document
files, but skipping all note files.
@@ -673,7 +673,7 @@ class NWIndex:
def getTagsData(
self, activeOnly: bool = True
- ) -> Generator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
+ ) -> Iterable[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
"""Return all known tags."""
for tag, data in self._tagsIndex.items():
iItem = self._itemIndex[data.get("handle")]
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index b69c29bc..80aee83f 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -31,7 +31,7 @@ from time import time
from typing import TYPE_CHECKING
from pathlib import Path
from functools import partial
-from collections.abc import Generator
+from collections.abc import Iterable
from PyQt5.QtCore import QCoreApplication
@@ -517,7 +517,7 @@ class NWProject:
# Class Methods
##
- def iterProjectItems(self) -> Generator[NWItem]:
+ def iterProjectItems(self) -> Iterable[NWItem]:
"""This function ensures that the item tree loaded is sent to
the GUI tree view in such a way that the tree can be built. That
is, the parent item must be sent before its child. In principle,
diff --git a/novelwriter/core/sessions.py b/novelwriter/core/sessions.py
index 5356c6d4..2f79ad20 100644
--- a/novelwriter/core/sessions.py
+++ b/novelwriter/core/sessions.py
@@ -29,7 +29,7 @@ import logging
from time import time
from typing import TYPE_CHECKING
from pathlib import Path
-from collections.abc import Generator
+from collections.abc import Iterable
from novelwriter.error import logException
from novelwriter.common import formatTimeStamp
@@ -110,7 +110,7 @@ class NWSessionLog:
return True
- def iterRecords(self) -> Generator[dict]:
+ def iterRecords(self) -> Iterable[dict]:
"""Iterate through all records in the log."""
sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE)
if isinstance(sessFile, Path) and sessFile.is_file():
diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py
index ad8db0df..4b6911f2 100644
--- a/novelwriter/core/status.py
+++ b/novelwriter/core/status.py
@@ -28,10 +28,10 @@ import random
import logging
from typing import TYPE_CHECKING, Literal
-from collections.abc import Generator, ItemsView, Iterator, KeysView, ValuesView
+from collections.abc import ItemsView, Iterable, Iterator, KeysView, ValuesView
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
-from PyQt5.QtCore import QRectF, Qt
+from PyQt5.QtCore import QRectF
from novelwriter import CONFIG
from novelwriter.common import minmax, simplified
@@ -193,7 +193,7 @@ class NWStatus:
self._store[key]["count"] += 1
return
- def pack(self) -> Generator[tuple[str, dict]]:
+ def pack(self) -> Iterable[tuple[str, dict]]:
"""Pack the status entries into a dictionary."""
for key, data in self._store.items():
yield (data["name"], {
@@ -248,7 +248,7 @@ class NWStatus:
def _createIcon(self, red: int, green: int, blue: int) -> QIcon:
"""Generate an icon for a status label."""
pixmap = QPixmap(self._iPX, self._iPX)
- pixmap.fill(Qt.transparent)
+ pixmap.fill(QColor(0, 0, 0, 0))
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index ad548aa6..72539c70 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -55,7 +55,6 @@ class ToHtml(Tokenizer):
self._genMode = self.M_EXPORT
self._cssStyles = True
self._fullHTML: list[str] = []
- self._navMap: dict[str, str] = {}
# Internals
self._trMap = {}
@@ -71,10 +70,6 @@ class ToHtml(Tokenizer):
def fullHTML(self) -> list[str]:
return self._fullHTML
- @property
- def navigationMap(self) -> dict[str, str]:
- return self._navMap
-
##
# Setters
##
@@ -174,6 +169,8 @@ class ToHtml(Tokenizer):
pStyle = None
lines = []
+ tHandle = self._handle
+
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities
@@ -232,11 +229,9 @@ class ToHtml(Tokenizer):
else:
hStyle = ""
- if self._linkHeadings and self._handle:
- tHH = f"{self._handle}:T{nHead:04d}"
- aNm = f""
+ if self._linkHeadings and tHandle:
+ aNm = f""
else:
- tHH = ""
aNm = ""
# Process Text Type
@@ -256,32 +251,22 @@ class ToHtml(Tokenizer):
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"
{tText}
\n") diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index dd30cfc6..9125ba01 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -35,13 +35,13 @@ from functools import partial from PyQt5.QtCore import QCoreApplication, QRegularExpression -from novelwriter.enum import nwComment, nwItemLayout from novelwriter.common import formatTimeStamp, numberToRoman, checkInt from novelwriter.constants import ( nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst ) from novelwriter.core.index import processComment from novelwriter.core.project import NWProject +from novelwriter.enum import nwComment, nwItemLayout logger = logging.getLogger(__name__) @@ -120,13 +120,14 @@ class Tokenizer(ABC): self._text = "" # The raw text to be tokenized self._handle = None # The item handle currently being processed self._result = "" # The result of the last document - self._counts = {} # Counter data self._keepMarkdown = False # Whether to keep the markdown text self._allMarkdown = [] # The result novelWriter markdown of all documents - # Processed Tokens + # Processed Tokens and Meta Data self._tokens: list[tuple[int, int, str, list[tuple[int, int]], int]] = [] + self._counts: dict[str, int] = {} + self._outline: dict[str, str] = {} # User Settings self._textFont = "Serif" # Output text font @@ -226,6 +227,11 @@ class Tokenizer(ABC): """The collected stats about the text.""" return self._counts + @property + def textOutline(self) -> dict[str, str]: + """The generated outline of the text.""" + return self._outline + @property def errData(self) -> list[str]: """The error data.""" @@ -392,43 +398,42 @@ class Tokenizer(ABC): def doConvert(self) -> None: raise NotImplementedError - def addRootHeading(self, tHandle: str) -> bool: + def addRootHeading(self, tHandle: str) -> None: """Add a heading at the start of a new root folder.""" - tItem = self._project.tree[tHandle] - if not tItem or not tItem.isRootType(): - return False + self._text = "" + self._handle = None - if self._isFirst: - textAlign = self.A_CENTRE - self._isFirst = False - else: - textAlign = self.A_PBB | self.A_CENTRE + if (tItem := self._project.tree[tHandle]) and tItem.isRootType(): + self._handle = tHandle + if self._isFirst: + textAlign = self.A_CENTRE + self._isFirst = False + else: + textAlign = self.A_PBB | self.A_CENTRE - trNotes = self._localLookup("Notes") - title = f"{trNotes}: {tItem.itemName}" - self._tokens = [] - self._tokens.append(( - self.T_TITLE, 0, title, [], textAlign - )) - if self._keepMarkdown: - self._allMarkdown.append(f"# {title}\n\n") + trNotes = self._localLookup("Notes") + title = f"{trNotes}: {tItem.itemName}" + self._tokens = [] + self._tokens.append(( + self.T_TITLE, 1, title, [], textAlign + )) + if self._keepMarkdown: + self._allMarkdown.append(f"#! {title}\n\n") - return True + return def setText(self, tHandle: str, text: str | None = None) -> None: """Set the text for the tokenizer from a handle. If text is not - set, its is loaded from the file. + set, it's is loaded from the file. """ self._text = "" self._handle = None if nwItem := self._project.tree[tHandle]: if text is None: text = self._project.storage.getDocument(tHandle).readDocument() or "" - self._text = text self._handle = tHandle self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT - return def doPreProcessing(self) -> None: @@ -798,7 +803,29 @@ class Tokenizer(ABC): return - def countStats(self) -> dict[str, int]: + def buildOutline(self) -> None: + """Build an outline of the text up to level 3 headings.""" + tHandle = self._handle or "" + isNovel = self._isNovel + for tType, nHead, tText, _, _ in self._tokens: + if tType == self.T_TITLE: + prefix = "TT" + elif tType == self.T_HEAD1: + prefix = "PT" if isNovel else "H1" + elif tType == self.T_HEAD2: + prefix = "CH" if isNovel else "H2" + elif tType == self.T_HEAD3: + prefix = "SC" if isNovel else "H3" + else: + continue + + key = f"{tHandle}:T{nHead:04d}" + text = tText.replace(nwHeadFmt.BR, " ").replace("&", "&") + self._outline[key] = f"{prefix}|{text}" + + return + + def countStats(self) -> None: """Count stats on the tokenized text.""" titleCount = self._counts.get("titleCount", 0) paragraphCount = self._counts.get("paragraphCount", 0) @@ -905,7 +932,7 @@ class Tokenizer(ABC): self._counts["textWordChars"] = textWordChars self._counts["titleWordChars"] = titleWordChars - return {} + return def saveRawMarkdown(self, path: str | Path) -> None: """Save the raw text to a plain text file.""" diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 4dea1a95..f206fdb2 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -28,7 +28,7 @@ import logging from typing import TYPE_CHECKING, Literal, overload from pathlib import Path -from collections.abc import Generator, Iterator +from collections.abc import Iterable, Iterator from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.error import logException @@ -387,7 +387,7 @@ class NWTree: rootClasses.add(nwItem.itemClass) return rootClasses - def iterRoots(self, itemClass: nwItemClass | None) -> Generator[tuple[str, NWItem]]: + def iterRoots(self, itemClass: nwItemClass | None) -> Iterable[tuple[str, NWItem]]: """Iterate over all root items of a given class in order.""" for tHandle in self._order: nwItem = self.__getitem__(tHandle) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index ac123ab9..36b9d80d 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -41,13 +41,13 @@ from PyQt5.QtWidgets import ( ) from novelwriter import CONFIG, SHARED +from novelwriter.constants import nwHeaders, nwUnicode +from novelwriter.core.tohtml import ToHtml from novelwriter.enum import nwItemType, nwDocAction, nwDocMode from novelwriter.error import logException -from novelwriter.constants import nwHeaders, nwUnicode +from novelwriter.extensions.eventfilters import WheelEventFilter from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON -from novelwriter.core.tohtml import ToHtml -from novelwriter.extensions.eventfilters import WheelEventFilter logger = logging.getLogger(__name__) @@ -633,7 +633,7 @@ class GuiDocViewHeader(QWidget): # Internal Variables self._docHandle = None - self._docOutline: dict[int, tuple[str, int]] = {} + self._docOutline: dict[str, tuple[str, int]] = {} iPx = SHARED.theme.baseIconSize mPx = CONFIG.pxInt(4) @@ -730,7 +730,7 @@ class GuiDocViewHeader(QWidget): self.refreshButton.setVisible(False) return - def setOutline(self, data: dict[int, tuple[str, int]]) -> None: + def setOutline(self, data: dict[str, tuple[str, int]]) -> None: """Set the document outline dataset.""" tHandle = self._docHandle if data != self._docOutline and tHandle: diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py index 421e89a0..04babe3f 100644 --- a/novelwriter/gui/editordocument.py +++ b/novelwriter/gui/editordocument.py @@ -23,10 +23,10 @@ along with this program. If not, see