Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a QTextDocument format class for previews #1892

Merged
merged 15 commits into from
May 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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