Add footnote handling in html and markdown output

This commit is contained in:
Veronica Berglyd Olsen
2024-04-15 18:22:55 +02:00
parent 8d24243d4f
commit 44192c88f5
4 changed files with 159 additions and 96 deletions
+4
View File
@@ -191,6 +191,8 @@ class NWBuildDocument:
else: else:
yield i, False yield i, False
makeObj.appendFootnotes()
if not (self._build.getBool("html.preserveTabs") or self._preview): if not (self._build.getBool("html.preserveTabs") or self._preview):
makeObj.replaceTabs() makeObj.replaceTabs()
@@ -231,6 +233,8 @@ class NWBuildDocument:
else: else:
yield i, False yield i, False
makeObj.appendFootnotes()
self._error = None self._error = None
self._cache = makeObj self._cache = makeObj
+76 -52
View File
@@ -33,10 +33,44 @@ from novelwriter import CONFIG
from novelwriter.common import formatTimeStamp from novelwriter.common import formatTimeStamp
from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode
from novelwriter.core.project import NWProject 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__) 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>",
}
HTML5_TAGS = {
Tokenizer.FMT_B_B: "<strong>",
Tokenizer.FMT_B_E: "</strong>",
Tokenizer.FMT_I_B: "<em>",
Tokenizer.FMT_I_E: "</em>",
Tokenizer.FMT_D_B: "<del>",
Tokenizer.FMT_D_E: "</del>",
Tokenizer.FMT_U_B: "<span style='text-decoration: underline;'>",
Tokenizer.FMT_U_E: "</span>",
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>",
}
class ToHtml(Tokenizer): class ToHtml(Tokenizer):
"""Core: HTML Document Writer """Core: HTML Document Writer
@@ -117,38 +151,9 @@ class ToHtml(Tokenizer):
def doConvert(self) -> None: def doConvert(self) -> None:
"""Convert the list of text tokens into an HTML document.""" """Convert the list of text tokens into an HTML document."""
if self._genMode == self.M_PREVIEW: self._result = ""
htmlTags = { # HTML4 + CSS2 (for Qt)
self.FMT_B_B: "<b>",
self.FMT_B_E: "</b>",
self.FMT_I_B: "<i>",
self.FMT_I_E: "</i>",
self.FMT_D_B: "<span style='text-decoration: line-through;'>",
self.FMT_D_E: "</span>",
self.FMT_U_B: "<u>",
self.FMT_U_E: "</u>",
self.FMT_M_B: "<mark>",
self.FMT_M_E: "</mark>",
}
else:
htmlTags = { # HTML5 (for export)
self.FMT_B_B: "<strong>",
self.FMT_B_E: "</strong>",
self.FMT_I_B: "<em>",
self.FMT_I_E: "</em>",
self.FMT_D_B: "<del>",
self.FMT_D_E: "</del>",
self.FMT_U_B: "<span style='text-decoration: underline;'>",
self.FMT_U_E: "</span>",
self.FMT_M_B: "<mark>",
self.FMT_M_E: "</mark>",
}
htmlTags[self.FMT_SUP_B] = "<sup>"
htmlTags[self.FMT_SUP_E] = "</sup>"
htmlTags[self.FMT_SUB_B] = "<sub>"
htmlTags[self.FMT_SUB_E] = "</sub>"
hTags = HTML4_TAGS if self._genMode == self.M_PREVIEW else HTML5_TAGS
if self._isNovel and self._genMode != self.M_PREVIEW: if self._isNovel and self._genMode != self.M_PREVIEW:
# For story files, we bump the titles one level up # For story files, we bump the titles one level up
h1Cl = " class='title'" h1Cl = " class='title'"
@@ -163,12 +168,9 @@ class ToHtml(Tokenizer):
h3 = "h3" h3 = "h3"
h4 = "h4" h4 = "h4"
self._result = ""
para = [] para = []
pStyle = None
lines = [] lines = []
pStyle = None
tHandle = self._handle tHandle = self._handle
for tType, nHead, tText, tFormat, tStyle in self._tokens: for tType, nHead, tText, tFormat, tStyle in self._tokens:
@@ -181,11 +183,11 @@ class ToHtml(Tokenizer):
for c in tText: for c in tText:
if c == "<": if c == "<":
cText.append("&lt;") cText.append("&lt;")
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 i += 4
elif c == ">": elif c == ">":
cText.append("&gt;") cText.append("&gt;")
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 i += 4
else: else:
cText.append(c) cText.append(c)
@@ -275,28 +277,18 @@ class ToHtml(Tokenizer):
lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n") lines.append(f"<p class='skip'{hStyle}>&nbsp;</p>\n")
elif tType == self.T_TEXT: elif tType == self.T_TEXT:
tTemp = tText
if pStyle is None: if pStyle is None:
pStyle = hStyle pStyle = hStyle
for pos, fmt, key in reversed(tFormat): para.append(self._formatText(tText, tFormat, hTags).rstrip())
if fmt > self.MRK_BOUNDARY:
if key in self._markers:
index = self._markers[key][0]
if fmt == self.MRK_FOOTNOTE:
ref = f"<sup><a href='#footnote_{index}'>[{index+1}]</a></sup>"
tTemp = f"{tTemp[:pos]}{ref}{tTemp[pos:]}"
else:
tTemp = f"{tTemp[:pos]}{htmlTags[fmt]}{tTemp[pos:]}"
para.append(stripEscape(tTemp.rstrip()))
elif tType == self.T_SYNOPSIS and self._doSynopsis: 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: 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: 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: elif tType == self.T_KEYWORD and self._doKeywords:
tag, text = self._formatKeywords(tText) tag, text = self._formatKeywords(tText)
@@ -309,6 +301,25 @@ class ToHtml(Tokenizer):
return 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"<h3>{footnotes}</h3>\n")
lines.append("<ol>\n")
for index, content in self._footnotes.values():
text = "</p><p>".join(self._formatText(t, f, tags) for t, f in content)
lines.append(f"<li id='footnote_{index}'><p>{text}</p></li>\n")
lines.append("</ol>\n")
result = "".join(lines)
self._result += result
self._fullHTML.append(result)
return
def saveHtml5(self, path: str | Path) -> None: def saveHtml5(self, path: str | Path) -> None:
"""Save the data to an HTML file.""" """Save the data to an HTML file."""
with open(path, mode="w", encoding="utf-8") as fObj: with open(path, mode="w", encoding="utf-8") as fObj:
@@ -460,6 +471,19 @@ class ToHtml(Tokenizer):
# Internal Functions # 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"<sup><a href='#footnote_{index}'>[{index}]</a></sup>"
else:
html = tags.get(fmt, "ERR")
temp = f"{temp[:pos]}{html}{temp[pos:]}"
return stripEscape(temp)
def _formatSynopsis(self, text: str, synopsis: bool) -> str: def _formatSynopsis(self, text: str, synopsis: bool) -> str:
"""Apply HTML formatting to synopsis.""" """Apply HTML formatting to synopsis."""
if synopsis: if synopsis:
+78 -44
View File
@@ -29,11 +29,48 @@ from pathlib import Path
from novelwriter.constants import nwHeadFmt, nwLabels, nwUnicode from novelwriter.constants import nwHeadFmt, nwLabels, nwUnicode
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
from novelwriter.core.tokenizer import Tokenizer from novelwriter.core.tokenizer import T_Formats, Tokenizer
logger = logging.getLogger(__name__) 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): class ToMarkdown(Tokenizer):
"""Core: Markdown Document Writer """Core: Markdown Document Writer
@@ -90,47 +127,15 @@ class ToMarkdown(Tokenizer):
def doConvert(self) -> None: def doConvert(self) -> None:
"""Convert the list of text tokens into a Markdown document.""" """Convert the list of text tokens into a Markdown document."""
self._result = ""
if self._genMode == self.M_STD: if self._genMode == self.M_STD:
# Standard Markdown mTags = STD_MD
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: "",
}
cSkip = "" cSkip = ""
else: else:
# Extended Markdown mTags = EXT_MD
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: "~",
}
cSkip = nwUnicode.U_MMSP cSkip = nwUnicode.U_MMSP
self._result = ""
para = [] para = []
lines = [] lines = []
lineSep = " \n" if self._preserveBreaks else " " lineSep = " \n" if self._preserveBreaks else " "
@@ -170,22 +175,19 @@ class ToMarkdown(Tokenizer):
lines.append(f"{cSkip}\n\n") lines.append(f"{cSkip}\n\n")
elif tType == self.T_TEXT: elif tType == self.T_TEXT:
tTemp = tText para.append(self._formatText(tText, tFormat, mTags).rstrip())
for pos, fmt, _ in reversed(tFormat):
tTemp = f"{tTemp[:pos]}{mdTags[fmt]}{tTemp[pos:]}"
para.append(tTemp.rstrip())
elif tType == self.T_SYNOPSIS and self._doSynopsis: elif tType == self.T_SYNOPSIS and self._doSynopsis:
label = self._localLookup("Synopsis") 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: elif tType == self.T_SHORT and self._doSynopsis:
label = self._localLookup("Short Description") 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: elif tType == self.T_COMMENT and self._doComments:
label = self._localLookup("Comment") 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: elif tType == self.T_KEYWORD and self._doKeywords:
lines.append(self._formatKeywords(tText, tStyle)) lines.append(self._formatKeywords(tText, tStyle))
@@ -195,6 +197,25 @@ class ToMarkdown(Tokenizer):
return 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: def saveMarkdown(self, path: str | Path) -> None:
"""Save the data to a plain text file.""" """Save the data to a plain text file."""
with open(path, mode="w", encoding="utf-8") as outFile: with open(path, mode="w", encoding="utf-8") as outFile:
@@ -214,6 +235,19 @@ class ToMarkdown(Tokenizer):
# Internal Functions # 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: def _formatKeywords(self, text: str, style: int) -> str:
"""Apply Markdown formatting to keywords.""" """Apply Markdown formatting to keywords."""
valid, bits, _ = self._project.index.scanThis("@"+text) valid, bits, _ = self._project.index.scanThis("@"+text)
+1
View File
@@ -215,6 +215,7 @@ class GuiDocViewer(QTextBrowser):
aDoc.doPreProcessing() aDoc.doPreProcessing()
aDoc.tokenizeText() aDoc.tokenizeText()
aDoc.doConvert() aDoc.doConvert()
aDoc.appendFootnotes()
except Exception: except Exception:
logger.error("Failed to generate preview for document with handle '%s'", tHandle) logger.error("Failed to generate preview for document with handle '%s'", tHandle)
logException() logException()