"""
novelWriter – HTML Text Converter
=================================
File History:
Created: 2019-05-07 [0.0.1] ToHtml
This file is a part of novelWriter
Copyright 2018–2024, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see "),
Tokenizer.FMT_U_B: (Tokenizer.FMT_U_E, ""),
Tokenizer.FMT_M_B: (Tokenizer.FMT_M_E, ""),
Tokenizer.FMT_SUP_B: (Tokenizer.FMT_SUP_E, ""),
Tokenizer.FMT_SUB_B: (Tokenizer.FMT_SUB_E, ""),
Tokenizer.FMT_DL_B: (Tokenizer.FMT_DL_E, ""),
Tokenizer.FMT_ADL_B: (Tokenizer.FMT_ADL_E, ""),
}
# Each closer tag, with the id of its corresponding opener and tag format
HTML_CLOSER: dict[int, tuple[int, str]] = {
Tokenizer.FMT_B_E: (Tokenizer.FMT_B_B, ""),
Tokenizer.FMT_I_E: (Tokenizer.FMT_I_B, ""),
Tokenizer.FMT_D_E: (Tokenizer.FMT_D_B, ""),
Tokenizer.FMT_U_E: (Tokenizer.FMT_U_B, ""),
Tokenizer.FMT_M_E: (Tokenizer.FMT_M_B, ""),
Tokenizer.FMT_SUP_E: (Tokenizer.FMT_SUP_B, ""),
Tokenizer.FMT_SUB_E: (Tokenizer.FMT_SUB_B, ""),
Tokenizer.FMT_DL_E: (Tokenizer.FMT_DL_B, ""),
Tokenizer.FMT_ADL_E: (Tokenizer.FMT_ADL_B, ""),
}
# Empty HTML tag record
HTML_NONE = (0, "")
class ToHtml(Tokenizer):
"""Core: HTML Document Writer
Extend the Tokenizer class to writer HTML output. This class is
also used by the Document Viewer, and Manuscript Build Preview.
"""
def __init__(self, project: NWProject) -> None:
super().__init__(project)
self._cssStyles = True
self._fullHTML: list[str] = []
# Internals
self._trMap = {}
self._usedNotes: dict[str, int] = {}
self.setReplaceUnicode(False)
return
##
# Properties
##
@property
def fullHTML(self) -> list[str]:
return self._fullHTML
##
# Setters
##
def setStyles(self, cssStyles: bool) -> None:
"""Enable or disable CSS styling. Some elements may still have
class tags.
"""
self._cssStyles = cssStyles
return
def setReplaceUnicode(self, doReplace: bool) -> None:
"""Set the translation map to either minimal or full unicode for
html entities replacement.
"""
# Control characters must always be replaced
# Angle brackets are replaced later as they are also used in
# formatting codes
self._trMap = str.maketrans({"&": "&"})
if doReplace:
# Extend to all relevant Unicode characters
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
return
##
# Class Methods
##
def getFullResultSize(self) -> int:
"""Return the size of the full HTML result."""
return sum(len(x) for x in self._fullHTML)
def doPreProcessing(self) -> None:
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
super().doPreProcessing()
self._text = self._text.translate(self._trMap)
return
def doConvert(self) -> None:
"""Convert the list of text tokens into an HTML document."""
self._result = ""
if self._isNovel:
# For story files, we bump the titles one level up
h1Cl = " class='title'"
h1 = "h1"
h2 = "h1"
h3 = "h2"
h4 = "h3"
else:
h1Cl = ""
h1 = "h1"
h2 = "h2"
h3 = "h3"
h4 = "h4"
lines = []
tHandle = self._handle
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities
if tFormat:
# If we have formatting, we must recompute the locations
cText = []
i = 0
for c in tText:
if c == "<":
cText.append("<")
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
i += 4
elif c == ">":
cText.append(">")
tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat]
i += 4
else:
cText.append(c)
i += 1
tText = "".join(cText)
else:
# If we don't have formatting, we can do a plain replace
tText = tText.replace("<", "<").replace(">", ">")
# Styles
aStyle = []
if tStyle is not None and self._cssStyles:
if tStyle & self.A_LEFT:
aStyle.append("text-align: left;")
elif tStyle & self.A_RIGHT:
aStyle.append("text-align: right;")
elif tStyle & self.A_CENTRE:
aStyle.append("text-align: center;")
elif tStyle & self.A_JUSTIFY:
aStyle.append("text-align: justify;")
if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;")
if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;")
if tStyle & self.A_Z_BTMMRG:
aStyle.append("margin-bottom: 0;")
if tStyle & self.A_Z_TOPMRG:
aStyle.append("margin-top: 0;")
if tStyle & self.A_IND_L:
aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
if tStyle & self.A_IND_R:
aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
if tStyle & self.A_IND_T:
aStyle.append(f"text-indent: {self._firstWidth:.2f}em;")
if aStyle:
stVals = " ".join(aStyle)
hStyle = f" style='{stVals}'"
else:
hStyle = ""
if self._linkHeadings and tHandle:
aNm = f""
else:
aNm = ""
# Process Text Type
if tType == self.T_TEXT:
lines.append(f"
{self._formatText(tText, tFormat)}
\n") elif tType == self.T_TITLE: tHead = tText.replace(nwHeadFmt.BR, "{tText}
\n") elif tType == self.T_SKIP: lines.append(f"\n") elif tType == self.T_SYNOPSIS and self._doSynopsis: 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), False)) elif tType == self.T_COMMENT and self._doComments: lines.append(self._formatComments(self._formatText(tText, tFormat))) elif tType == self.T_KEYWORD and self._doKeywords: tag, text = self._formatKeywords(tText) kClass = f" class='meta meta-{tag}'" if tag else "" tTemp = f"
{text}
\n" lines.append(tTemp) self._result = "".join(lines) self._fullHTML.append(self._result) return def appendFootnotes(self) -> None: """Append the footnotes in the buffer.""" if self._usedNotes: footnotes = self._localLookup("Footnotes") lines = [] lines.append(f"{text}
{sSynop}: {text}
\n" def _formatComments(self, text: str) -> str: """Apply HTML formatting to comments.""" sComm = self._localLookup("Comment") return f"{sComm}: {text}
\n" def _formatKeywords(self, text: str) -> tuple[str, str]: """Apply HTML formatting to keywords.""" valid, bits, _ = self._project.index.scanThis("@"+text) if not valid or not bits or bits[0] not in nwLabels.KEY_NAME: return "", "" result = f"{self._localLookup(nwLabels.KEY_NAME[bits[0]])}: " if len(bits) > 1: if bits[0] == nwKeyWords.TAG_KEY: one, two = self._project.index.parseValue(bits[1]) result += f"{one}" if two: result += f" | {two}" else: result += ", ".join( f"{t}" for t in bits[1:] ) return bits[0][1:], result