"""
novelWriter – HTML Text Converter
=================================
File History:
Created: 2019-05-07 [0.0.1] ToHtml
This file is a part of novelWriter
Copyright (C) 2019 Veronica Berglyd Olsen and novelWriter contributors
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, ""),
TextFmt.ANM_B: (TextFmt.ANM_E, ""),
TextFmt.ARF_B: (TextFmt.ARF_E, ""),
TextFmt.HRF_B: (TextFmt.HRF_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, ""),
TextFmt.ANM_E: (TextFmt.ANM_B, ""),
TextFmt.ARF_E: (TextFmt.ARF_B, ""),
TextFmt.HRF_E: (TextFmt.HRF_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._trMap = {}
self._cssStyles = True
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
self.setReplaceUnicode(False)
return
##
# 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._pages)
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."""
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 = []
for tType, tMeta, tText, tFmt, tStyle in self._blocks:
# Replace < and > with HTML entities
if tFmt:
# If we have formatting, we must recompute the locations
cText = []
i = 0
for c in tText:
if c == "<":
cText.append("<")
tFmt = [(p + 3 if p > i else p, f, k) for p, f, k in tFmt]
i += 4
elif c == ">":
cText.append(">")
tFmt = [(p + 3 if p > i else p, f, k) for p, f, k in tFmt]
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 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_BTM:
aStyle.append("margin-bottom: 0;")
if tStyle & BlockFmt.Z_TOP:
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 tMeta:
aNm = f""
else:
aNm = ""
# Process Text Type
if tType == BlockTyp.TEXT:
lines.append(f"
{self._formatText(tText, tFmt)}
\n") elif tType == BlockTyp.TITLE: tHead = tText.replace("\n", "{tText}
\n") elif tType == BlockTyp.SKIP: lines.append(f"\n") elif tType == BlockTyp.COMMENT: lines.append(f"
{self._formatText(tText, tFmt)}
\n") elif tType == BlockTyp.KEYWORD: tClass = f"meta meta-{tMeta}" lines.append(f"{self._formatText(tText, tFmt)}
\n") self._pages.append("".join(lines)) return def closeDocument(self) -> None: """Run close document tasks.""" # Replace fields if there are stats available if self._usedFields and self._counts: pages = len(self._pages) for doc, field in self._usedFields: if doc >= 0 and doc < pages and (value := self._counts.get(field)) is not None: self._pages[doc] = self._pages[doc].replace( f"{{{{{field}}}}}", self._formatInt(value) ) # Add footnotes if self._usedNotes: footnotes = self._localLookup("Footnotes") lines = [] lines.append(f"{text}