Skip to content

Commit

Permalink
Add a QTextDocument format class for previews (#1892)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo committed May 25, 2024
2 parents f92c22c + d3ea763 commit f23bd38
Show file tree
Hide file tree
Showing 24 changed files with 1,210 additions and 413 deletions.
2 changes: 1 addition & 1 deletion novelwriter/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class nwHeaders:

H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
H_SIZES = {0: 1.00, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}
H_SIZES = {0: 2.50, 1: 2.00, 2: 1.75, 3: 1.50, 4: 1.25}


class nwFiles:
Expand Down
43 changes: 29 additions & 14 deletions novelwriter/core/docbuild.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
from novelwriter.core.tokenizer import Tokenizer
from novelwriter.core.tomarkdown import ToMarkdown
from novelwriter.core.toodt import ToOdt
from novelwriter.core.toqdoc import TextDocumentTheme, ToQTextDocument
from novelwriter.enum import nwBuildFmt
from novelwriter.error import formatException, logException

Expand All @@ -54,7 +55,7 @@ class NWBuildDocument:

__slots__ = (
"_project", "_build", "_queue", "_error", "_cache", "_count",
"_outline", "_preview"
"_outline",
)

def __init__(self, project: NWProject, build: BuildSettings) -> None:
Expand All @@ -65,7 +66,6 @@ def __init__(self, project: NWProject, build: BuildSettings) -> None:
self._cache = None
self._count = False
self._outline = False
self._preview = False
return

##
Expand Down Expand Up @@ -99,15 +99,6 @@ def setBuildOutline(self, state: bool) -> None:
self._outline = state
return

def setPreviewMode(self, state: bool) -> None:
"""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

##
# Special Methods
##
Expand All @@ -134,6 +125,32 @@ def queueAll(self) -> None:
self._queue.append(item.itemHandle)
return

def iterBuildPreview(self, theme: TextDocumentTheme) -> Iterable[tuple[int, bool]]:
"""Build a preview QTextDocument."""
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)

self._outline = True
self._count = True

font = QFont()
font.fromString(self._build.getStr("format.textFont"))

makeObj.initDocument(font, theme)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
yield i, self._doBuild(makeObj, tHandle)
else:
yield i, False

makeObj.appendFootnotes()

self._error = None
self._cache = makeObj

return

def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
"""Wrapper for builders based on format."""
if bFormat in (nwBuildFmt.ODT, nwBuildFmt.FODT):
Expand Down Expand Up @@ -182,8 +199,6 @@ def iterBuildHTML(self, path: Path | None, asJson: bool = False) -> Iterable[tup
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)

makeObj.setPreview(self._preview)
makeObj.setLinkHeadings(self._preview)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
Expand All @@ -193,7 +208,7 @@ def iterBuildHTML(self, path: Path | None, asJson: bool = False) -> Iterable[tup

makeObj.appendFootnotes()

if not (self._build.getBool("html.preserveTabs") or self._preview):
if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs()

self._error = None
Expand Down
69 changes: 14 additions & 55 deletions novelwriter/core/tohtml.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,6 @@

logger = logging.getLogger(__name__)

HTML4_TAGS = {
Tokenizer.FMT_B_B: "<b>",
Tokenizer.FMT_B_E: "</b>",
Tokenizer.FMT_I_B: "<i>",
Tokenizer.FMT_I_E: "</i>",
Tokenizer.FMT_D_B: "<span style='text-decoration: line-through;'>",
Tokenizer.FMT_D_E: "</span>",
Tokenizer.FMT_U_B: "<u>",
Tokenizer.FMT_U_E: "</u>",
Tokenizer.FMT_M_B: "<mark>",
Tokenizer.FMT_M_E: "</mark>",
Tokenizer.FMT_SUP_B: "<sup>",
Tokenizer.FMT_SUP_E: "</sup>",
Tokenizer.FMT_SUB_B: "<sub>",
Tokenizer.FMT_SUB_E: "</sub>",
Tokenizer.FMT_STRIP: "",
}

HTML5_TAGS = {
Tokenizer.FMT_B_B: "<strong>",
Tokenizer.FMT_B_E: "</strong>",
Expand All @@ -82,14 +64,9 @@ class ToHtml(Tokenizer):
also used by the Document Viewer, and Manuscript Build Preview.
"""

M_PREVIEW = 0 # Tweak output for the DocViewer
M_EXPORT = 1 # Tweak output for saving to HTML or printing
M_EBOOK = 2 # Tweak output for converting to epub

def __init__(self, project: NWProject) -> None:
super().__init__(project)

self._genMode = self.M_EXPORT
self._cssStyles = True
self._fullHTML: list[str] = []

Expand All @@ -112,11 +89,6 @@ def fullHTML(self) -> list[str]:
# Setters
##

def setPreview(self, state: bool) -> None:
"""Set to preview generator mode."""
self._genMode = self.M_PREVIEW if state else self.M_EXPORT
return

def setStyles(self, cssStyles: bool) -> None:
"""Enable or disable CSS styling. Some elements may still have
class tags.
Expand Down Expand Up @@ -157,8 +129,7 @@ def doConvert(self) -> None:
"""Convert the list of text tokens into an HTML document."""
self._result = ""

hTags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
if self._isNovel and self._genMode != self.M_PREVIEW:
if self._isNovel:
# For story files, we bump the titles one level up
h1Cl = " class='title'"
h1 = "h1"
Expand Down Expand Up @@ -240,7 +211,7 @@ def doConvert(self) -> None:

# Process Text Type
if tType == self.T_TEXT:
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat, hTags)}</p>\n")
lines.append(f"<p{hStyle}>{self._formatText(tText, tFormat)}</p>\n")

elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "<br>")
Expand Down Expand Up @@ -269,13 +240,13 @@ def doConvert(self) -> None:
lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n")

elif tType == self.T_SYNOPSIS and self._doSynopsis:
lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), True))
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), True))

elif tType == self.T_SHORT and self._doSynopsis:
lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), False))
lines.append(self._formatSynopsis(self._formatText(tText, tFormat), False))

elif tType == self.T_COMMENT and self._doComments:
lines.append(self._formatComments(self._formatText(tText, tFormat, hTags)))
lines.append(self._formatComments(self._formatText(tText, tFormat)))

elif tType == self.T_KEYWORD and self._doKeywords:
tag, text = self._formatKeywords(tText)
Expand All @@ -291,15 +262,14 @@ def doConvert(self) -> None:
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
tags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
footnotes = self._localLookup("Footnotes")

lines = []
lines.append(f"<h3>{footnotes}</h3>\n")
lines.append("<ol>\n")
for key, index in self._usedNotes.items():
if content := self._footnotes.get(key):
text = self._formatText(*content, tags)
text = self._formatText(*content)
lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
lines.append("</ol>\n")

Expand Down Expand Up @@ -468,7 +438,7 @@ def getStyleSheet(self) -> list[str]:
# Internal Functions
##

def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str:
def _formatText(self, text: str, tFmt: T_Formats) -> str:
"""Apply formatting tags to text."""
temp = text
for pos, fmt, data in reversed(tFmt):
Expand All @@ -481,7 +451,7 @@ def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str:
else:
html = "<sup>ERR</sup>"
else:
html = tags.get(fmt, "ERR")
html = HTML5_TAGS.get(fmt, "ERR")
temp = f"{temp[:pos]}{html}{temp[pos:]}"
temp = temp.replace("\n", "<br>")
return stripEscape(temp)
Expand All @@ -492,18 +462,12 @@ def _formatSynopsis(self, text: str, synopsis: bool) -> str:
sSynop = self._localLookup("Synopsis")
else:
sSynop = self._localLookup("Short Description")
if self._genMode == self.M_PREVIEW:
return f"<p class='note'><span class='modifier'>{sSynop}:</span> {text}</p>\n"
else:
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"
return f"<p class='synopsis'><strong>{sSynop}:</strong> {text}</p>\n"

def _formatComments(self, text: str) -> str:
"""Apply HTML formatting to comments."""
if self._genMode == self.M_PREVIEW:
return f"<p class='comment'>{text}</p>\n"
else:
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"
sComm = self._localLookup("Comment")
return f"<p class='comment'><strong>{sComm}:</strong> {text}</p>\n"

def _formatKeywords(self, text: str) -> tuple[str, str]:
"""Apply HTML formatting to keywords."""
Expand All @@ -519,13 +483,8 @@ def _formatKeywords(self, text: str) -> tuple[str, str]:
if two:
result += f" | <span class='optional'>{two}</a>"
else:
if self._genMode == self.M_PREVIEW:
result += ", ".join(
f"<a class='tag' href='#{bits[0][1:]}={t}'>{t}</a>" for t in bits[1:]
)
else:
result += ", ".join(
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
)
result += ", ".join(
f"<a class='tag' href='#tag_{t}'>{t}</a>" for t in bits[1:]
)

return bits[0][1:], result
22 changes: 16 additions & 6 deletions novelwriter/core/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class Tokenizer(ABC):
# Lookups
L_HEADINGS = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD3, T_HEAD4]
L_SKIP_INDENT = [T_TITLE, T_HEAD1, T_HEAD2, T_HEAD2, T_HEAD3, T_HEAD4, T_SEP, T_SKIP]
L_SUMMARY = [T_SYNOPSIS, T_SHORT]

def __init__(self, project: NWProject) -> None:

Expand Down Expand Up @@ -155,14 +156,15 @@ def __init__(self, project: NWProject) -> None:
self._keepBreaks = True # Keep line breaks in paragraphs

# Margins
self._marginTitle = (1.000, 0.500)
self._marginHead1 = (1.000, 0.500)
self._marginHead2 = (0.834, 0.500)
self._marginHead3 = (0.584, 0.500)
self._marginHead4 = (0.584, 0.500)
self._marginTitle = (1.417, 0.500)
self._marginHead1 = (1.417, 0.500)
self._marginHead2 = (1.668, 0.500)
self._marginHead3 = (1.168, 0.500)
self._marginHead4 = (1.168, 0.500)
self._marginText = (0.000, 0.584)
self._marginMeta = (0.000, 0.584)
self._marginFoot = (1.417, 0.467)
self._marginSep = (1.168, 1.168)

# Title Formats
self._fmtTitle = nwHeadFmt.TITLE # Formatting for titles
Expand Down Expand Up @@ -378,6 +380,11 @@ def setMetaMargins(self, upper: float, lower: float) -> None:
self._marginMeta = (float(upper), float(lower))
return

def setSeparatorMargins(self, upper: float, lower: float) -> None:
"""Set the upper and lower meta text margin."""
self._marginSep = (float(upper), float(lower))
return

def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings."""
self._linkHeadings = state
Expand Down Expand Up @@ -597,7 +604,10 @@ def tokenizeText(self) -> None:
# are automatically skipped.

valid, bits, _ = self._project.index.scanThis(aLine)
if valid and bits and bits[0] not in self._skipKeywords:
if (
valid and bits and bits[0] in nwLabels.KEY_NAME
and bits[0] not in self._skipKeywords
):
tokens.append((
self.T_KEYWORD, nHead, aLine[1:].strip(), [], sAlign
))
Expand Down
10 changes: 7 additions & 3 deletions novelwriter/core/toodt.py
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ def __init__(self, project: NWProject, isFlat: bool) -> None:
self._mTopHead = "0.423cm"
self._mTopText = "0.000cm"
self._mTopMeta = "0.000cm"
self._mTopSep = "0.247cm"

self._mBotTitle = "0.212cm"
self._mBotHead1 = "0.212cm"
Expand All @@ -201,6 +202,7 @@ def __init__(self, project: NWProject, isFlat: bool) -> None:
self._mBotHead = "0.212cm"
self._mBotText = "0.247cm"
self._mBotMeta = "0.106cm"
self._mBotSep = "0.247cm"

self._mBotFoot = "0.106cm"
self._mLeftFoot = "0.600cm"
Expand Down Expand Up @@ -299,6 +301,7 @@ def initDocument(self) -> None:
self._mTopHead = self._emToCm(mScale * self._marginHead4[0])
self._mTopText = self._emToCm(mScale * self._marginText[0])
self._mTopMeta = self._emToCm(mScale * self._marginMeta[0])
self._mTopSep = self._emToCm(mScale * self._marginSep[0])

self._mBotTitle = self._emToCm(mScale * self._marginTitle[1])
self._mBotHead1 = self._emToCm(mScale * self._marginHead1[1])
Expand All @@ -308,6 +311,7 @@ def initDocument(self) -> None:
self._mBotHead = self._emToCm(mScale * self._marginHead4[1])
self._mBotText = self._emToCm(mScale * self._marginText[1])
self._mBotMeta = self._emToCm(mScale * self._marginMeta[1])
self._mBotSep = self._emToCm(mScale * self._marginSep[1])

self._mLeftFoot = self._emToCm(self._marginFoot[0])
self._mBotFoot = self._emToCm(self._marginFoot[1])
Expand Down Expand Up @@ -501,7 +505,7 @@ def doConvert(self) -> None:
self._addTextPar(xText, S_SEP, oStyle, tText)

elif tType == self.T_SKIP:
self._addTextPar(xText, S_SEP, oStyle, "")
self._addTextPar(xText, S_TEXT, oStyle, "")

elif tType == self.T_SYNOPSIS and self._doSynopsis:
tTemp, tFmt = self._formatSynopsis(tText, tFormat, True)
Expand Down Expand Up @@ -944,8 +948,8 @@ def _useableStyles(self) -> None:
style.setParentStyleName("Standard")
style.setNextStyleName(S_TEXT)
style.setClass("text")
style.setMarginTop(self._mTopText)
style.setMarginBottom(self._mBotText)
style.setMarginTop(self._mTopSep)
style.setMarginBottom(self._mBotSep)
style.setLineHeight(self._fLineHeight)
style.setTextAlign("center")
style.setFontName(self._fontFamily)
Expand Down
Loading

0 comments on commit f23bd38

Please sign in to comment.