Add footnote handling in html and markdown output
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+76
-52
@@ -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: "<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):
|
||||
"""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: "<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>"
|
||||
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"<p class='skip'{hStyle}> </p>\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"<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()))
|
||||
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"<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:
|
||||
"""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"<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:
|
||||
"""Apply HTML formatting to synopsis."""
|
||||
if synopsis:
|
||||
|
||||
+78
-44
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user