"""
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 "),
TextFmt.U_B: (TextFmt.U_E, ""),
TextFmt.M_B: (TextFmt.M_E, ""),
TextFmt.SUP_B: (TextFmt.SUP_E, ""),
TextFmt.SUB_B: (TextFmt.SUB_E, ""),
TextFmt.COL_B: (TextFmt.COL_E, ""),
}
# Each closer tag, with the id of its corresponding opener and tag format
HTML_CLOSER: dict[int, tuple[int, str]] = {
TextFmt.B_E: (TextFmt.B_B, ""),
TextFmt.I_E: (TextFmt.I_B, ""),
TextFmt.D_E: (TextFmt.D_B, ""),
TextFmt.U_E: (TextFmt.U_B, ""),
TextFmt.M_E: (TextFmt.M_B, ""),
TextFmt.SUP_E: (TextFmt.SUP_B, ""),
TextFmt.SUB_E: (TextFmt.SUB_B, ""),
TextFmt.COL_E: (TextFmt.COL_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._blocks:
# 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 & BlockFmt.LEFT:
aStyle.append("text-align: left;")
elif tStyle & BlockFmt.RIGHT:
aStyle.append("text-align: right;")
elif tStyle & BlockFmt.CENTRE:
aStyle.append("text-align: center;")
elif tStyle & BlockFmt.JUSTIFY:
aStyle.append("text-align: justify;")
if tStyle & BlockFmt.PBB:
aStyle.append("page-break-before: always;")
if tStyle & BlockFmt.PBA:
aStyle.append("page-break-after: always;")
if tStyle & BlockFmt.Z_BTMMRG:
aStyle.append("margin-bottom: 0;")
if tStyle & BlockFmt.Z_TOPMRG:
aStyle.append("margin-top: 0;")
if tStyle & BlockFmt.IND_L:
aStyle.append(f"margin-left: {self._blockIndent:.2f}em;")
if tStyle & BlockFmt.IND_R:
aStyle.append(f"margin-right: {self._blockIndent:.2f}em;")
if tStyle & BlockFmt.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 == BlockTyp.TEXT:
lines.append(f"
{self._formatText(tText, tFormat)}
\n") elif tType == BlockTyp.TITLE: tHead = tText.replace(nwHeadFmt.BR, "{tText}
\n") elif tType == BlockTyp.SKIP: lines.append(f"\n") elif tType == BlockTyp.COMMENT: lines.append(f"
{self._formatText(tText, tFormat)}
\n") elif tType == BlockTyp.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}