diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py index 0c2ab461..3c791334 100644 --- a/novelwriter/core/docbuild.py +++ b/novelwriter/core/docbuild.py @@ -191,6 +191,8 @@ class NWBuildDocument: else: yield i, False + makeObj.appendFootnotes() + if not (self._build.getBool("html.preserveTabs") or self._preview): makeObj.replaceTabs() @@ -231,6 +233,8 @@ class NWBuildDocument: else: yield i, False + makeObj.appendFootnotes() + self._error = None self._cache = makeObj diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index 176a6aed..d513d4ea 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -33,10 +33,44 @@ from novelwriter import CONFIG from novelwriter.common import formatTimeStamp from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode from novelwriter.core.project import NWProject -from novelwriter.core.tokenizer import Tokenizer, stripEscape +from novelwriter.core.tokenizer import T_Formats, Tokenizer, stripEscape logger = logging.getLogger(__name__) +HTML4_TAGS = { + Tokenizer.FMT_B_B: "", + Tokenizer.FMT_B_E: "", + Tokenizer.FMT_I_B: "", + Tokenizer.FMT_I_E: "", + Tokenizer.FMT_D_B: "", + Tokenizer.FMT_D_E: "", + 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: "", +} + +HTML5_TAGS = { + Tokenizer.FMT_B_B: "", + Tokenizer.FMT_B_E: "", + Tokenizer.FMT_I_B: "", + Tokenizer.FMT_I_E: "", + Tokenizer.FMT_D_B: "", + Tokenizer.FMT_D_E: "", + 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: "", +} + class ToHtml(Tokenizer): """Core: HTML Document Writer @@ -117,38 +151,9 @@ class ToHtml(Tokenizer): def doConvert(self) -> None: """Convert the list of text tokens into an HTML document.""" - if self._genMode == self.M_PREVIEW: - htmlTags = { # HTML4 + CSS2 (for Qt) - self.FMT_B_B: "", - self.FMT_B_E: "", - self.FMT_I_B: "", - self.FMT_I_E: "", - self.FMT_D_B: "", - self.FMT_D_E: "", - self.FMT_U_B: "", - self.FMT_U_E: "", - self.FMT_M_B: "", - self.FMT_M_E: "", - } - else: - htmlTags = { # HTML5 (for export) - self.FMT_B_B: "", - self.FMT_B_E: "", - self.FMT_I_B: "", - self.FMT_I_E: "", - self.FMT_D_B: "", - self.FMT_D_E: "", - self.FMT_U_B: "", - self.FMT_U_E: "", - self.FMT_M_B: "", - self.FMT_M_E: "", - } - - htmlTags[self.FMT_SUP_B] = "" - htmlTags[self.FMT_SUP_E] = "" - htmlTags[self.FMT_SUB_B] = "" - htmlTags[self.FMT_SUB_E] = "" + self._result = "" + hTags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS if self._isNovel and self._genMode != self.M_PREVIEW: # For story files, we bump the titles one level up h1Cl = " class='title'" @@ -163,12 +168,9 @@ class ToHtml(Tokenizer): h3 = "h3" h4 = "h4" - self._result = "" - para = [] - pStyle = None lines = [] - + pStyle = None tHandle = self._handle for tType, nHead, tText, tFormat, tStyle in self._tokens: @@ -181,11 +183,11 @@ class ToHtml(Tokenizer): for c in tText: if c == "<": cText.append("<") - tFormat = [[p + 3 if p > i else p, f, k] for p, f, k in tFormat] + 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] + tFormat = [(p + 3 if p > i else p, f, k) for p, f, k in tFormat] i += 4 else: cText.append(c) @@ -275,28 +277,18 @@ class ToHtml(Tokenizer): lines.append(f"

 

\n") elif tType == self.T_TEXT: - tTemp = tText if pStyle is None: pStyle = hStyle - for pos, fmt, key in reversed(tFormat): - if fmt > self.MRK_BOUNDARY: - if key in self._markers: - index = self._markers[key][0] - if fmt == self.MRK_FOOTNOTE: - ref = f"[{index+1}]" - tTemp = f"{tTemp[:pos]}{ref}{tTemp[pos:]}" - else: - tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}" - para.append(stripEscape(tTemp.rstrip())) + para.append(self._formatText(tText, tFormat, hTags).rstrip()) elif tType == self.T_SYNOPSIS and self._doSynopsis: - lines.append(self._formatSynopsis(tText, True)) + lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), True)) elif tType == self.T_SHORT and self._doSynopsis: - lines.append(self._formatSynopsis(tText, False)) + lines.append(self._formatSynopsis(self._formatText(tText, tFormat, hTags), False)) elif tType == self.T_COMMENT and self._doComments: - lines.append(self._formatComments(tText)) + lines.append(self._formatComments(self._formatText(tText, tFormat, hTags))) elif tType == self.T_KEYWORD and self._doKeywords: tag, text = self._formatKeywords(tText) @@ -309,6 +301,25 @@ class ToHtml(Tokenizer): return + def appendFootnotes(self) -> None: + """Append the footnotes in the buffer.""" + tags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS + footnotes = self._localLookup("Footnotes") + + lines = [] + lines.append(f"

{footnotes}

\n") + lines.append("
    \n") + for index, content in self._footnotes.values(): + text = "

    ".join(self._formatText(t, f, tags) for t, f in content) + lines.append(f"

  1. {text}

  2. \n") + lines.append("
\n") + + result = "".join(lines) + self._result += result + self._fullHTML.append(result) + + return + def saveHtml5(self, path: str | Path) -> None: """Save the data to an HTML file.""" with open(path, mode="w", encoding="utf-8") as fObj: @@ -460,6 +471,19 @@ class ToHtml(Tokenizer): # Internal Functions ## + def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str: + """Apply formatting tags to text.""" + temp = text + for pos, fmt, data in reversed(tFmt): + html = "" + if fmt == self.FMT_FNOTE: + index = self._footnotes.get(data, (0, ""))[0] or "ERR" + html = f"[{index}]" + else: + html = tags.get(fmt, "ERR") + temp = f"{temp[:pos]}{html}{temp[pos:]}" + return stripEscape(temp) + def _formatSynopsis(self, text: str, synopsis: bool) -> str: """Apply HTML formatting to synopsis.""" if synopsis: diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py index 482751c1..dd32b851 100644 --- a/novelwriter/core/tomd.py +++ b/novelwriter/core/tomd.py @@ -29,11 +29,48 @@ from pathlib import Path from novelwriter.constants import nwHeadFmt, nwLabels, nwUnicode from novelwriter.core.project import NWProject -from novelwriter.core.tokenizer import Tokenizer +from novelwriter.core.tokenizer import T_Formats, Tokenizer logger = logging.getLogger(__name__) +# Standard Markdown +STD_MD = { + Tokenizer.FMT_B_B: "**", + Tokenizer.FMT_B_E: "**", + Tokenizer.FMT_I_B: "_", + Tokenizer.FMT_I_E: "_", + Tokenizer.FMT_D_B: "", + Tokenizer.FMT_D_E: "", + 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: "", +} + +# Extended Markdown +EXT_MD = { + Tokenizer.FMT_B_B: "**", + Tokenizer.FMT_B_E: "**", + Tokenizer.FMT_I_B: "_", + Tokenizer.FMT_I_E: "_", + Tokenizer.FMT_D_B: "~~", + Tokenizer.FMT_D_E: "~~", + 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: "~", +} + + class ToMarkdown(Tokenizer): """Core: Markdown Document Writer @@ -90,47 +127,15 @@ class ToMarkdown(Tokenizer): def doConvert(self) -> None: """Convert the list of text tokens into a Markdown document.""" + self._result = "" + if self._genMode == self.M_STD: - # Standard Markdown - mdTags = { - self.FMT_B_B: "**", - self.FMT_B_E: "**", - self.FMT_I_B: "_", - self.FMT_I_E: "_", - self.FMT_D_B: "", - self.FMT_D_E: "", - self.FMT_U_B: "", - self.FMT_U_E: "", - self.FMT_M_B: "", - self.FMT_M_E: "", - self.FMT_SUP_B: "", - self.FMT_SUP_E: "", - self.FMT_SUB_B: "", - self.FMT_SUB_E: "", - } + mTags = STD_MD cSkip = "" else: - # Extended Markdown - mdTags = { - self.FMT_B_B: "**", - self.FMT_B_E: "**", - self.FMT_I_B: "_", - self.FMT_I_E: "_", - self.FMT_D_B: "~~", - self.FMT_D_E: "~~", - self.FMT_U_B: "", - self.FMT_U_E: "", - self.FMT_M_B: "==", - self.FMT_M_E: "==", - self.FMT_SUP_B: "^", - self.FMT_SUP_E: "^", - self.FMT_SUB_B: "~", - self.FMT_SUB_E: "~", - } + mTags = EXT_MD cSkip = nwUnicode.U_MMSP - self._result = "" - para = [] lines = [] lineSep = " \n" if self._preserveBreaks else " " @@ -170,22 +175,19 @@ class ToMarkdown(Tokenizer): lines.append(f"{cSkip}\n\n") elif tType == self.T_TEXT: - tTemp = tText - for pos, fmt, _ in reversed(tFormat): - tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}" - para.append(tTemp.rstrip()) + para.append(self._formatText(tText, tFormat, mTags).rstrip()) elif tType == self.T_SYNOPSIS and self._doSynopsis: label = self._localLookup("Synopsis") - lines.append(f"**{label}:** {tText}\n\n") + lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n") elif tType == self.T_SHORT and self._doSynopsis: label = self._localLookup("Short Description") - lines.append(f"**{label}:** {tText}\n\n") + lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n") elif tType == self.T_COMMENT and self._doComments: label = self._localLookup("Comment") - lines.append(f"**{label}:** {tText}\n\n") + lines.append(f"**{label}:** {self._formatText(tText, tFormat, mTags)}\n\n") elif tType == self.T_KEYWORD and self._doKeywords: lines.append(self._formatKeywords(tText, tStyle)) @@ -195,6 +197,25 @@ class ToMarkdown(Tokenizer): return + def appendFootnotes(self) -> None: + """Append the footnotes in the buffer.""" + tags = STD_MD if self._genMode == self.M_STD else EXT_MD + footnotes = self._localLookup("Footnotes") + + lines = [] + lines.append(f"### {footnotes}\n\n") + for index, content in self._footnotes.values(): + marker = f"{index}. " + indent = "\n\n"+" "*len(marker) + text = indent.join(self._formatText(t, f, tags) for t, f in content) + lines.append(f"{marker}{text}\n") + + result = "".join(lines) + self._result += result + self._fullMD.append(result) + + return + def saveMarkdown(self, path: str | Path) -> None: """Save the data to a plain text file.""" with open(path, mode="w", encoding="utf-8") as outFile: @@ -214,6 +235,19 @@ class ToMarkdown(Tokenizer): # Internal Functions ## + def _formatText(self, text: str, tFmt: T_Formats, tags: dict[int, str]) -> str: + """Apply formatting tags to text.""" + temp = text + for pos, fmt, data in reversed(tFmt): + md = "" + if fmt == self.FMT_FNOTE: + index = self._footnotes.get(data, (0, ""))[0] or "ERR" + md = f"[{index}]" + else: + md = tags.get(fmt, "") + temp = f"{temp[:pos]}{md}{temp[pos:]}" + return temp + def _formatKeywords(self, text: str, style: int) -> str: """Apply Markdown formatting to keywords.""" valid, bits, _ = self._project.index.scanThis("@"+text) diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index bc5caeb5..daa4566a 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -215,6 +215,7 @@ class GuiDocViewer(QTextBrowser): aDoc.doPreProcessing() aDoc.tokenizeText() aDoc.doConvert() + aDoc.appendFootnotes() except Exception: logger.error("Failed to generate preview for document with handle '%s'", tHandle) logException()