Allow inserting stats in documents (#2073)

This commit is contained in:
Veronica Berglyd Olsen
2024-10-30 00:14:48 +01:00
committed by GitHub
57 changed files with 1087 additions and 654 deletions
+29 -4
View File
@@ -492,6 +492,10 @@ def jsonEncode(data: dict | list | tuple, n: int = 0, nmax: int = 0) -> str:
return "".join(buffer)
##
# XML Helpers
##
def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
"""A modified version of the XML indent function in the standard
library. It behaves more closely to how the one from lxml does.
@@ -535,21 +539,42 @@ def xmlIndent(tree: ET.Element | ET.ElementTree) -> None:
return
def xmlElement(
tag: str,
text: str | int | float | bool | None = None,
*,
attrib: dict | None = None,
tail: str | None = None,
) -> ET.Element:
"""A custom implementation of Element with more arguments."""
xSub = ET.Element(tag, attrib=attrib or {})
if text is not None:
if isinstance(text, bool):
xSub.text = str(text).lower()
else:
xSub.text = str(text)
if tail is not None:
xSub.tail = tail
return xSub
def xmlSubElem(
parent: ET.Element,
tag: str,
text: str | int | float | bool | None = None,
attrib: dict | None = None
*,
attrib: dict | None = None,
tail: str | None = None,
) -> ET.Element:
"""A custom implementation of SubElement that takes text as an
argument.
"""
"""A custom implementation of SubElement with more arguments."""
xSub = ET.SubElement(parent, tag, attrib=attrib or {})
if text is not None:
if isinstance(text, bool):
xSub.text = str(text).lower()
else:
xSub.text = str(text)
if tail is not None:
xSub.tail = tail
return xSub
+4
View File
@@ -252,6 +252,10 @@ class Config:
def hasError(self) -> bool:
return self._hasError
@property
def locale(self) -> QLocale:
return self._dLocale
@property
def recentProjects(self) -> RecentProjects:
return self._recentProjects
+62 -6
View File
@@ -67,7 +67,7 @@ class nwRegEx:
FMT_EB = r"(?<![\w\\])(\*{2})(?![\s\*])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_ST = r"(?<![\w\\])(~{2})(?![\s~])(.+?)(?<![\s\\])(\1)(?!\w)"
FMT_SC = r"(?i)(?<!\\)(\[(?:b|/b|i|/i|s|/s|u|/u|m|/m|sup|/sup|sub|/sub|br)\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote):)(.+?)(?<!\\)(\])"
FMT_SV = r"(?i)(?<!\\)(\[(?:footnote|field):)(.+?)(?<!\\)(\])"
class nwShortcode:
@@ -89,12 +89,15 @@ class nwShortcode:
BREAK = "[br]"
FOOTNOTE_B = "[footnote:"
FIELD_B = "[field:"
COMMENT_STYLES = {
nwComment.FOOTNOTE: "[footnote:{0}]",
nwComment.COMMENT: "[comment:{0}]",
}
FIELD_VALUE = "[field:{0}]"
class nwStyles:
@@ -159,11 +162,14 @@ class nwKeyWords:
STORY_KEY = "@story"
MENTION_KEY = "@mention"
# Set of Valid Keys
VALID_KEYS = {
# Note: The order here affects the order of menu entries
ALL_KEYS = [
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY, WORLD_KEY,
OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY, STORY_KEY, MENTION_KEY,
}
]
# Set of Valid Keys
VALID_KEYS = set(ALL_KEYS)
# Map from Keys to Item Class
KEY_CLASS = {
@@ -193,6 +199,29 @@ class nwLists:
]
class nwStats:
CHARS_ALL = "allChars"
CHARS_TEXT = "textChars"
CHARS_TITLE = "titleChars"
PARAGRAPHS = "paragraphCount"
TITLES = "titleCount"
WCHARS_ALL = "allWordChars"
WCHARS_TEXT = "textWordChars"
WCHARS_TITLE = "titleWordChars"
WORDS_ALL = "allWords"
WORDS_TEXT = "textWords"
WORDS_TITLE = "titleWords"
# Note: The order here affects the order of menu entries
ALL_FIELDS = [
WORDS_ALL, WORDS_TEXT, WORDS_TITLE,
CHARS_ALL, CHARS_TEXT, CHARS_TITLE,
WCHARS_ALL, WCHARS_TEXT, WCHARS_TITLE,
PARAGRAPHS, TITLES,
]
class nwLabels:
CLASS_NAME = {
@@ -253,6 +282,20 @@ class nwLabels:
nwKeyWords.STORY_KEY: QT_TRANSLATE_NOOP("Constant", "Story"),
nwKeyWords.MENTION_KEY: QT_TRANSLATE_NOOP("Constant", "Mentions"),
}
KEY_SHORTCUT = {
nwKeyWords.TAG_KEY: "Ctrl+K, G",
nwKeyWords.POV_KEY: "Ctrl+K, V",
nwKeyWords.FOCUS_KEY: "Ctrl+K, F",
nwKeyWords.CHAR_KEY: "Ctrl+K, C",
nwKeyWords.PLOT_KEY: "Ctrl+K, P",
nwKeyWords.TIME_KEY: "Ctrl+K, T",
nwKeyWords.WORLD_KEY: "Ctrl+K, L",
nwKeyWords.OBJECT_KEY: "Ctrl+K, O",
nwKeyWords.ENTITY_KEY: "Ctrl+K, E",
nwKeyWords.CUSTOM_KEY: "Ctrl+K, X",
nwKeyWords.STORY_KEY: "Ctrl+K, N",
nwKeyWords.MENTION_KEY: "Ctrl+K, M",
}
OUTLINE_COLS = {
nwOutline.TITLE: QT_TRANSLATE_NOOP("Constant", "Title"),
nwOutline.LEVEL: QT_TRANSLATE_NOOP("Constant", "Level"),
@@ -274,16 +317,29 @@ class nwLabels:
nwOutline.MENTION: KEY_NAME[nwKeyWords.MENTION_KEY],
nwOutline.SYNOP: QT_TRANSLATE_NOOP("Constant", "Synopsis"),
}
STATS_NAME = {
nwStats.CHARS_ALL: QT_TRANSLATE_NOOP("Constant", "Characters"),
nwStats.CHARS_TEXT: QT_TRANSLATE_NOOP("Constant", "Characters in Text"),
nwStats.CHARS_TITLE: QT_TRANSLATE_NOOP("Constant", "Characters in Headings"),
nwStats.PARAGRAPHS: QT_TRANSLATE_NOOP("Constant", "Paragraphs"),
nwStats.TITLES: QT_TRANSLATE_NOOP("Constant", "Headings"),
nwStats.WCHARS_ALL: QT_TRANSLATE_NOOP("Constant", "Characters, No Spaces"),
nwStats.WCHARS_TEXT: QT_TRANSLATE_NOOP("Constant", "Characters in Text, No Spaces"),
nwStats.WCHARS_TITLE: QT_TRANSLATE_NOOP("Constant", "Characters in Headings, No Spaces"),
nwStats.WORDS_ALL: QT_TRANSLATE_NOOP("Constant", "Words"),
nwStats.WORDS_TEXT: QT_TRANSLATE_NOOP("Constant", "Words in Text"),
nwStats.WORDS_TITLE: QT_TRANSLATE_NOOP("Constant", "Words in Headings"),
}
BUILD_FMT = {
nwBuildFmt.ODT: QT_TRANSLATE_NOOP("Constant", "Open Document (.odt)"),
nwBuildFmt.FODT: QT_TRANSLATE_NOOP("Constant", "Flat Open Document (.fodt)"),
nwBuildFmt.DOCX: QT_TRANSLATE_NOOP("Constant", "Microsoft Word Document (.docx)"),
nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "novelWriter HTML (.html)"),
nwBuildFmt.HTML: QT_TRANSLATE_NOOP("Constant", "HTML 5 (.html)"),
nwBuildFmt.NWD: QT_TRANSLATE_NOOP("Constant", "novelWriter Markup (.txt)"),
nwBuildFmt.STD_MD: QT_TRANSLATE_NOOP("Constant", "Standard Markdown (.md)"),
nwBuildFmt.EXT_MD: QT_TRANSLATE_NOOP("Constant", "Extended Markdown (.md)"),
nwBuildFmt.PDF: QT_TRANSLATE_NOOP("Constant", "Portable Document Format (.pdf)"),
nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter HTML (.json)"),
nwBuildFmt.J_HTML: QT_TRANSLATE_NOOP("Constant", "JSON + HTML 5 (.json)"),
nwBuildFmt.J_NWD: QT_TRANSLATE_NOOP("Constant", "JSON + novelWriter Markup (.json)"),
}
BUILD_EXT = {
+7 -22
View File
@@ -127,16 +127,11 @@ class NWBuildDocument:
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
self._outline = True
yield from self._iterBuild(makeObj, filtered)
makeObj.appendFootnotes()
makeObj.closeDocument()
self._error = None
self._cache = makeObj
return
def iterBuildDocument(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
@@ -152,38 +147,31 @@ class NWBuildDocument:
makeObj = ToOdt(self._project, bFormat == nwBuildFmt.FODT)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
elif bFormat in (nwBuildFmt.HTML, nwBuildFmt.J_HTML):
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.appendFootnotes()
makeObj.closeDocument()
if not self._build.getBool("html.preserveTabs"):
makeObj.replaceTabs()
elif bFormat in (nwBuildFmt.STD_MD, nwBuildFmt.EXT_MD):
makeObj = ToMarkdown(self._project, bFormat == nwBuildFmt.EXT_MD)
filtered = self._setupBuild(makeObj)
yield from self._iterBuild(makeObj, filtered)
makeObj.appendFootnotes()
makeObj.closeDocument()
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
elif bFormat in (nwBuildFmt.NWD, nwBuildFmt.J_NWD):
makeObj = ToRaw(self._project)
filtered = self._setupBuild(makeObj)
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
@@ -191,19 +179,15 @@ class NWBuildDocument:
makeObj = ToDocX(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.closeDocument()
elif bFormat == nwBuildFmt.PDF:
makeObj = ToQTextDocument(self._project)
filtered = self._setupBuild(makeObj)
makeObj.initDocument()
yield from self._iterBuild(makeObj, filtered)
makeObj.appendFootnotes()
makeObj.closeDocument()
else:
logger.error("Unsupported document format")
@@ -240,7 +224,9 @@ class NWBuildDocument:
# Get Settings
textFont = QFont(CONFIG.textFont)
textFont.fromString(self._build.getStr("format.textFont"))
bldObj.setFont(textFont)
bldObj.setLanguage(self._project.data.language)
bldObj.setPartitionFormat(
self._build.getStr("headings.fmtPart"),
@@ -338,7 +324,6 @@ class NWBuildDocument:
bldObj.setReplaceUnicode(self._build.getBool("format.stripUnicode"))
if isinstance(bldObj, (ToOdt, ToDocX)):
bldObj.setLanguage(self._project.data.language)
bldObj.setHeaderFormat(
self._build.getStr("doc.pageHeader"),
self._build.getInt("doc.pageCountOffset"),
+3 -3
View File
@@ -184,11 +184,11 @@ class nwBuildFmt(Enum):
ODT = 0
FODT = 1
DOCX = 2
HTML = 3
NWD = 4
PDF = 3
HTML = 4
STD_MD = 5
EXT_MD = 6
PDF = 7
NWD = 7
J_HTML = 8
J_NWD = 9
+2 -1
View File
@@ -92,7 +92,8 @@ class TextFmt(IntEnum):
HRF_B = 21 # Begin href link
HRF_E = 22 # End href link
FNOTE = 23 # Footnote marker
STRIP = 24 # Strip the format code
FIELD = 24 # Data field
STRIP = 25 # Strip the format code
class BlockTyp(IntEnum):
+53 -43
View File
@@ -37,7 +37,7 @@ from PyQt5.QtCore import QMarginsF, QSizeF
from PyQt5.QtGui import QColor
from novelwriter import __version__
from novelwriter.common import firstFloat, xmlSubElem
from novelwriter.common import firstFloat, xmlElement, xmlSubElem
from novelwriter.constants import nwHeadFmt, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
@@ -57,13 +57,13 @@ RELS_BASE = f"{OOXML_SCM}/officeDocument/2006/relationships"
# Main XML NameSpaces
XML_NS = {
"r": RELS_BASE,
"w": f"{OOXML_SCM}/wordprocessingml/2006/main",
"cp": f"{OOXML_SCM}/package/2006/metadata/core-properties",
"dc": "http://purl.org/dc/elements/1.1/",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
"xml": "http://www.w3.org/XML/1998/namespace",
"dcterms": "http://purl.org/dc/terms/",
"r": RELS_BASE,
"w": f"{OOXML_SCM}/wordprocessingml/2006/main",
"xml": "http://www.w3.org/XML/1998/namespace",
"xsi": "http://www.w3.org/2001/XMLSchema-instance",
}
for ns, uri in XML_NS.items():
ET.register_namespace(ns, uri)
@@ -182,7 +182,6 @@ class ToDocX(Tokenizer):
# Internal
self._fontFamily = "Liberation Serif"
self._fontSize = 12.0
self._dLanguage = "en_GB"
self._pageSize = QSizeF(210.0, 297.0)
self._pageMargins = QMarginsF(20.0, 20.0, 20.0, 20.0)
@@ -192,6 +191,7 @@ class ToDocX(Tokenizer):
self._files: dict[str, DocXXmlFile] = {}
self._styles: dict[str, DocXParStyle] = {}
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[ET.Element, str]] = []
return
@@ -199,12 +199,6 @@ class ToDocX(Tokenizer):
# Setters
##
def setLanguage(self, language: str | None) -> None:
"""Set language for the document."""
if language:
self._dLanguage = language.replace("_", "-")
return
def setPageLayout(
self, width: float, height: float, top: float, bottom: float, left: float, right: float
) -> None:
@@ -334,10 +328,10 @@ class ToDocX(Tokenizer):
cDocs.append(("/word/_rels/document.xml.rels", RELS_TYPE))
# Relationships XML
rRels = ET.Element("Relationships", attrib={
rRels = xmlElement("Relationships", attrib={
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
})
wRels = ET.Element("Relationships", attrib={
wRels = xmlElement("Relationships", attrib={
"xmlns": f"{OOXML_SCM}/package/2006/relationships"
})
for name, rel in self._rels.items():
@@ -353,7 +347,7 @@ class ToDocX(Tokenizer):
xmlSubElem(rRels if isRoot else wRels, "Relationship", attrib=attrib)
# Content Types XML
dTypes = ET.Element("Types", attrib={
dTypes = xmlElement("Types", attrib={
"xmlns": f"{OOXML_SCM}/package/2006/content-types"
})
for name, content in cExts:
@@ -361,10 +355,8 @@ class ToDocX(Tokenizer):
for name, content in cDocs:
xmlSubElem(dTypes, "Override", attrib={"PartName": name, "ContentType": content})
def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
with zipObj.open(name, mode="w") as fObj:
xml = ET.ElementTree(xObj)
xml.write(fObj, encoding="utf-8", xml_declaration=True)
def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
zipObj.writestr(name, ET.tostring(root, encoding="utf-8", xml_declaration=True))
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
xmlToZip("_rels/.rels", rRels, outZip)
@@ -440,6 +432,8 @@ class ToDocX(Tokenizer):
fLink = ""
elif fFmt == TextFmt.FNOTE:
xNode = self._generateFootnote(fData)
elif fFmt == TextFmt.FIELD:
xNode = self._generateField(fData, xFmt)
elif fFmt == TextFmt.STRIP:
pass
@@ -454,9 +448,9 @@ class ToDocX(Tokenizer):
return
def _textRunToXml(self, text: str, fmt: int, fClass: str, fLink: str) -> ET.Element:
def _textRunToXml(self, text: str | None, fmt: int, fClass: str, fLink: str) -> ET.Element:
"""Encode the text run into XML."""
xR = ET.Element(_wTag("r"))
xR = xmlElement(_wTag("r"))
rPr = xmlSubElem(xR, _wTag("rPr"))
if fmt & X_BLD:
xmlSubElem(rPr, _wTag("b"))
@@ -477,18 +471,20 @@ class ToDocX(Tokenizer):
if fmt & X_COL and (color := self._classes.get(fClass)):
xmlSubElem(rPr, _wTag("color"), attrib={W_VAL: _docXCol(color)})
for segment in RX_TEXT.split(text):
if segment == "\n":
xmlSubElem(xR, _wTag("br"))
elif segment == "\t":
xmlSubElem(xR, _wTag("tab"))
elif segment:
_wText(xR, segment)
if isinstance(text, str):
for segment in RX_TEXT.split(text):
if segment == "\n":
xmlSubElem(xR, _wTag("br"))
elif segment == "\t":
xmlSubElem(xR, _wTag("tab"))
elif segment:
_wText(xR, segment)
if fmt & X_HRF and fLink:
xmlSubElem(rPr, _wTag("rStyle"), attrib={W_VAL: "InternetLink"})
rId = self._appendExternalRel(fLink)
xH = ET.Element(_wTag("hyperlink"), attrib={_mkTag("r", "id"): rId})
xH = xmlElement(_wTag("hyperlink"), attrib={
_mkTag("r", "id"): self._appendExternalRel(fLink),
})
xH.append(xR)
return xH
@@ -502,7 +498,7 @@ class ToDocX(Tokenizer):
"""Generate a footnote XML object."""
if key in self._footnotes:
idx = len(self._usedNotes) + 1
xR = ET.Element(_wTag("r"))
xR = xmlElement(_wTag("r"))
rPr = xmlSubElem(xR, _wTag("rPr"))
xmlSubElem(rPr, _wTag("vertAlign"), attrib={W_VAL: "superscript"})
xmlSubElem(xR, _wTag("footnoteReference"), attrib={_wTag("id"): str(idx)})
@@ -510,6 +506,15 @@ class ToDocX(Tokenizer):
return xR
return None
def _generateField(self, key: str, fmt: int) -> ET.Element | None:
"""Generate a data field XML object."""
if key and (field := key.partition(":")[2]):
xR = self._textRunToXml(None, fmt, "", "")
xT = _wText(xR, "0")
self._usedFields.append((xT, field))
return xR
return None
def _generateStyles(self) -> None:
"""Generate usable styles."""
styles: list[DocXParStyle] = []
@@ -682,7 +687,7 @@ class ToDocX(Tokenizer):
def _appXml(self) -> str:
"""Populate app.xml."""
rId = self._nextRelId()
xRoot = ET.Element("Properties", attrib={
xRoot = xmlElement("Properties", attrib={
"xmlns": f"{OOXML_SCM}/officeDocument/2006/extended-properties"
})
self._rels["app.xml"] = DocXXmlRel(
@@ -711,7 +716,7 @@ class ToDocX(Tokenizer):
def _coreXml(self) -> str:
"""Populate app.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_mkTag("cp", "coreProperties"))
xRoot = xmlElement(_mkTag("cp", "coreProperties"))
self._rels["core.xml"] = DocXXmlRel(
rId=rId,
relType=f"{OOXML_SCM}/package/2006/relationships/metadata/core-properties",
@@ -728,7 +733,7 @@ class ToDocX(Tokenizer):
xmlSubElem(xRoot, _mkTag("dcterms", "modified"), timeStamp, attrib=tsAttr)
xmlSubElem(xRoot, _mkTag("dc", "creator"), self._project.data.author)
xmlSubElem(xRoot, _mkTag("dc", "title"), self._project.data.name)
xmlSubElem(xRoot, _mkTag("dc", "language"), self._dLanguage)
xmlSubElem(xRoot, _mkTag("dc", "language"), self._dLocale.name())
xmlSubElem(xRoot, _mkTag("cp", "revision"), str(self._project.data.saveCount))
xmlSubElem(xRoot, _mkTag("cp", "lastModifiedBy"), self._project.data.author)
@@ -737,7 +742,7 @@ class ToDocX(Tokenizer):
def _stylesXml(self) -> str:
"""Populate styles.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("styles"))
xRoot = xmlElement(_wTag("styles"))
self._rels["styles.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/styles",
@@ -765,7 +770,7 @@ class ToDocX(Tokenizer):
})
xmlSubElem(xRPr, _wTag("sz"), attrib={W_VAL: size})
xmlSubElem(xRPr, _wTag("szCs"), attrib={W_VAL: size})
xmlSubElem(xRPr, _wTag("lang"), attrib={W_VAL: self._dLanguage})
xmlSubElem(xRPr, _wTag("lang"), attrib={W_VAL: self._dLocale.name()})
xmlSubElem(xPPr, _wTag("spacing"), attrib={_wTag("line"): line})
# Paragraph Styles
@@ -825,7 +830,7 @@ class ToDocX(Tokenizer):
def _defaultHeaderXml(self) -> str:
"""Populate header1.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("hdr"))
xRoot = xmlElement(_wTag("hdr"))
self._rels["header1.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/header",
@@ -872,7 +877,7 @@ class ToDocX(Tokenizer):
def _firstHeaderXml(self) -> str:
"""Populate header2.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("hdr"))
xRoot = xmlElement(_wTag("hdr"))
self._rels["header2.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/header",
@@ -897,8 +902,7 @@ class ToDocX(Tokenizer):
def _documentXml(self, hFirst: str | None, hDefault: str | None) -> str:
"""Populate document.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("document"))
xRoot.set("xmlns:w14", "http://schemas.microsoft.com/office/word/2010/wordml")
xRoot = xmlElement(_wTag("document"))
xBody = xmlSubElem(xRoot, _wTag("body"))
self._rels["document.xml"] = DocXXmlRel(
rId=rId,
@@ -920,6 +924,12 @@ class ToDocX(Tokenizer):
pars.append(par)
# Replace fields if there are stats available
if self._usedFields and self._counts:
for xField, field in self._usedFields:
if (value := self._counts.get(field)) is not None:
xField.text = self._formatInt(value)
# Write Paragraphs
for par in pars:
par.toXml(xBody)
@@ -967,7 +977,7 @@ class ToDocX(Tokenizer):
def _footnotesXml(self) -> str:
"""Populate footnotes.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("footnotes"))
xRoot = xmlElement(_wTag("footnotes"))
self._rels["footnotes.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/footnotes",
@@ -990,7 +1000,7 @@ class ToDocX(Tokenizer):
def _fontTableXml(self) -> str:
"""Populate fontTable.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("fonts"))
xRoot = xmlElement(_wTag("fonts"))
self._rels["fontTable.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/fontTable",
@@ -1013,7 +1023,7 @@ class ToDocX(Tokenizer):
def _settingsXml(self) -> str:
"""Populate settings.xml."""
rId = self._nextRelId()
xRoot = ET.Element(_wTag("settings"))
xRoot = xmlElement(_wTag("settings"))
self._rels["settings.xml"] = DocXXmlRel(
rId=rId,
relType=f"{RELS_BASE}/settings",
+17 -2
View File
@@ -84,6 +84,7 @@ class ToHtml(Tokenizer):
self._trMap = {}
self._cssStyles = True
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
self.setReplaceUnicode(False)
return
@@ -249,8 +250,18 @@ class ToHtml(Tokenizer):
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
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")
@@ -416,6 +427,10 @@ class ToHtml(Tokenizer):
tags.append((pos, f"<sup><a href='#footnote_{index}'>{index}</a></sup>"))
else:
tags.append((pos, "<sup>ERR</sup>"))
elif fmt == TextFmt.FIELD:
if field := data.partition(":")[2]:
self._usedFields.append((len(self._pages), field))
tags.append((pos, f"{{{{{field}}}}}"))
# Check all format types and close any tag that is still open. This
# ensures that unclosed tags don't spill over to the next paragraph.
+41 -23
View File
@@ -31,12 +31,14 @@ from abc import ABC, abstractmethod
from pathlib import Path
from typing import NamedTuple
from PyQt5.QtCore import QLocale
from PyQt5.QtGui import QColor, QFont
from novelwriter import CONFIG
from novelwriter.common import checkInt, numberToRoman
from novelwriter.constants import (
nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwStyles, nwUnicode, trConst
nwHeadFmt, nwKeyWords, nwLabels, nwShortcode, nwStats, nwStyles, nwUnicode,
trConst
)
from novelwriter.core.index import processComment
from novelwriter.core.project import NWProject
@@ -103,6 +105,7 @@ class Tokenizer(ABC):
self._outline: dict[str, str] = {}
# User Settings
self._dLocale = CONFIG.locale # The document locale
self._textFont = QFont("Serif", 11) # Output text font
self._lineHeight = 1.15 # Line height in units of em
self._colorHeads = True # Colourise headings
@@ -192,6 +195,7 @@ class Tokenizer(ABC):
}
self._shortCodeVals = {
nwShortcode.FOOTNOTE_B: TextFmt.FNOTE,
nwShortcode.FIELD_B: TextFmt.FIELD,
}
# Dialogue
@@ -224,6 +228,12 @@ class Tokenizer(ABC):
# Setters
##
def setLanguage(self, language: str | None) -> None:
"""Set language for the document."""
if language:
self._dLocale = QLocale(language)
return
def setTheme(self, theme: TextDocumentTheme) -> None:
"""Set the document colour theme."""
self._theme = theme
@@ -426,6 +436,10 @@ class Tokenizer(ABC):
def doConvert(self) -> None:
raise NotImplementedError
@abstractmethod
def closeDocument(self) -> None:
raise NotImplementedError
@abstractmethod
def saveDocument(self, path: Path) -> None:
raise NotImplementedError
@@ -948,20 +962,20 @@ class Tokenizer(ABC):
def countStats(self) -> None:
"""Count stats on the tokenized text."""
titleCount = self._counts.get("titleCount", 0)
paragraphCount = self._counts.get("paragraphCount", 0)
titleCount = self._counts.get(nwStats.TITLES, 0)
paragraphCount = self._counts.get(nwStats.PARAGRAPHS, 0)
allWords = self._counts.get("allWords", 0)
textWords = self._counts.get("textWords", 0)
titleWords = self._counts.get("titleWords", 0)
allWords = self._counts.get(nwStats.WORDS_ALL, 0)
textWords = self._counts.get(nwStats.WORDS_TEXT, 0)
titleWords = self._counts.get(nwStats.WORDS_TITLE, 0)
allChars = self._counts.get("allChars", 0)
textChars = self._counts.get("textChars", 0)
titleChars = self._counts.get("titleChars", 0)
allChars = self._counts.get(nwStats.CHARS_ALL, 0)
textChars = self._counts.get(nwStats.CHARS_TEXT, 0)
titleChars = self._counts.get(nwStats.CHARS_TITLE, 0)
allWordChars = self._counts.get("allWordChars", 0)
textWordChars = self._counts.get("textWordChars", 0)
titleWordChars = self._counts.get("titleWordChars", 0)
allWordChars = self._counts.get(nwStats.WCHARS_ALL, 0)
textWordChars = self._counts.get(nwStats.WCHARS_TEXT, 0)
titleWordChars = self._counts.get(nwStats.WCHARS_TITLE, 0)
for tType, _, tText, _, _ in self._blocks:
tText = tText.replace(nwUnicode.U_ENDASH, " ")
@@ -1006,20 +1020,20 @@ class Tokenizer(ABC):
allChars += len(tText)
allWordChars += len("".join(words))
self._counts["titleCount"] = titleCount
self._counts["paragraphCount"] = paragraphCount
self._counts[nwStats.TITLES] = titleCount
self._counts[nwStats.PARAGRAPHS] = paragraphCount
self._counts["allWords"] = allWords
self._counts["textWords"] = textWords
self._counts["titleWords"] = titleWords
self._counts[nwStats.WORDS_ALL] = allWords
self._counts[nwStats.WORDS_TEXT] = textWords
self._counts[nwStats.WORDS_TITLE] = titleWords
self._counts["allChars"] = allChars
self._counts["textChars"] = textChars
self._counts["titleChars"] = titleChars
self._counts[nwStats.CHARS_ALL] = allChars
self._counts[nwStats.CHARS_TEXT] = textChars
self._counts[nwStats.CHARS_TITLE] = titleChars
self._counts["allWordChars"] = allWordChars
self._counts["textWordChars"] = textWordChars
self._counts["titleWordChars"] = titleWordChars
self._counts[nwStats.WCHARS_ALL] = allWordChars
self._counts[nwStats.WCHARS_TEXT] = textWordChars
self._counts[nwStats.WCHARS_TITLE] = titleWordChars
return
@@ -1027,6 +1041,10 @@ class Tokenizer(ABC):
# Internal Functions
##
def _formatInt(self, value: int) -> str:
"""Return a localised integer."""
return self._dLocale.toString(value)
def _formatComment(self, style: ComStyle, key: str, text: str) -> tuple[str, T_Formats]:
"""Apply formatting to comments and notes."""
tTxt, tFmt = self._extractFormats(text)
+17 -2
View File
@@ -86,6 +86,7 @@ class ToMarkdown(Tokenizer):
super().__init__(project)
self._extended = extended
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
return
##
@@ -149,8 +150,18 @@ class ToMarkdown(Tokenizer):
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
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:
tags = EXT_MD if self._extended else STD_MD
footnotes = self._localLookup("Footnotes")
@@ -196,6 +207,10 @@ class ToMarkdown(Tokenizer):
md = f"[{index}]"
else:
md = "[ERR]"
elif fmt == TextFmt.FIELD:
if field := data.partition(":")[2]:
self._usedFields.append((len(self._pages), field))
md = f"{{{{{field}}}}}"
else:
md = tags.get(fmt, "")
temp = f"{temp[:pos]}{md}{temp[pos:]}"
+43 -17
View File
@@ -38,7 +38,7 @@ from zipfile import ZIP_DEFLATED, ZipFile
from PyQt5.QtGui import QColor, QFont
from novelwriter import __version__
from novelwriter.common import xmlIndent, xmlSubElem
from novelwriter.common import xmlElement, xmlIndent, xmlSubElem
from novelwriter.constants import nwHeadFmt, nwStyles
from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt, stripEscape
@@ -54,6 +54,7 @@ XML_NS = {
"loext": "urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0",
"manifest": "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
"meta": "urn:oasis:names:tc:opendocument:xmlns:meta:1.0",
"number": "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0",
"office": "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"style": "urn:oasis:names:tc:opendocument:xmlns:style:1.0",
"text": "urn:oasis:names:tc:opendocument:xmlns:text:1.0",
@@ -115,6 +116,7 @@ S_FIND = "First_20_line_20_indent"
S_TEXT = "Text_20_body"
S_META = "Text_20_Meta"
S_HNF = "Header_20_and_20_Footer"
S_NUM = "N0"
# Font Data
FONT_WEIGHT_NUM = ["100", "200", "300", "400", "500", "600", "700", "800", "900"]
@@ -223,14 +225,6 @@ class ToOdt(Tokenizer):
# Setters
##
def setLanguage(self, language: str | None) -> None:
"""Set language for the document."""
if language:
lang, _, country = language.partition("_")
self._dLanguage = lang or self._dLanguage
self._dCountry = country or self._dCountry
return
def setPageLayout(
self, width: float, height: float, top: float, bottom: float, left: float, right: float
) -> None:
@@ -264,6 +258,10 @@ class ToOdt(Tokenizer):
fontWeight = str(intWeight)
fontBold = str(min(intWeight + 300, 900))
lang, _, country = self._dLocale.name().partition("_")
self._dLanguage = lang or self._dLanguage
self._dCountry = country or self._dCountry
self._fontFamily = self._textFont.family()
self._fontSize = self._textFont.pointSize()
self._fontWeight = FONT_WEIGHT_MAP.get(fontWeight, fontWeight)
@@ -517,13 +515,14 @@ class ToOdt(Tokenizer):
oVers = _mkTag("office", "version")
xSett = ET.Element(oRoot, attrib={oVers: X_VERS})
def xmlToZip(name: str, xObj: ET.Element, zipObj: ZipFile) -> None:
with zipObj.open(name, mode="w") as fObj:
xml = ET.ElementTree(xObj)
xml.write(fObj, encoding="utf-8", xml_declaration=True)
def xmlToZip(name: str, root: ET.Element, zipObj: ZipFile) -> None:
zipObj.writestr(
name, ET.tostring(root, encoding="utf-8", xml_declaration=True),
compress_type=ZIP_DEFLATED, compresslevel=3,
)
with ZipFile(path, mode="w", compression=ZIP_DEFLATED, compresslevel=3) as outZip:
outZip.writestr("mimetype", X_MIME)
with ZipFile(path, mode="w") as outZip:
outZip.writestr("mimetype", X_MIME, compress_type=None, compresslevel=None)
xmlToZip("META-INF/manifest.xml", xMani, outZip)
xmlToZip("settings.xml", xSett, outZip)
xmlToZip("content.xml", self._dCont, outZip)
@@ -630,6 +629,8 @@ class ToOdt(Tokenizer):
fLink = ""
elif fFmt == TextFmt.FNOTE:
xNode = self._generateFootnote(fData)
elif fFmt == TextFmt.FIELD:
xNode = self._generateField(fData, xFmt)
elif fFmt == TextFmt.STRIP:
pass
@@ -715,16 +716,33 @@ class ToOdt(Tokenizer):
if content := self._footnotes.get(key):
self._nNote += 1
nStyle = ODTParagraphStyle("New")
xNote = ET.Element(_mkTag("text", "note"), attrib={
xNote = xmlElement(_mkTag("text", "note"), attrib={
_mkTag("text", "id"): f"ftn{self._nNote}",
_mkTag("text", "note-class"): "footnote",
})
xmlSubElem(xNote, _mkTag("text", "note-citation"), self._nNote)
xBody = ET.SubElement(xNote, _mkTag("text", "note-body"))
xBody = xmlSubElem(xNote, _mkTag("text", "note-body"))
self._addTextPar(xBody, "Footnote", nStyle, content[0], tFmt=content[1])
return xNote
return None
def _generateField(self, key: str, fmt: int) -> ET.Element | None:
"""Generate a data field XML object."""
if key and (field := key.partition(":")[2]):
xField = xmlElement(_mkTag("text", "user-field-get"), "0", tail="", attrib={
_mkTag("style", "data-style-name"): S_NUM,
_mkTag("text", "name"): f"Manuscript{field[:1].upper()}{field[1:]}",
})
if fmt == 0x00:
return xField
else:
xSpan = xmlElement(TAG_SPAN, "", tail="", attrib={
_mkTag("text", "style-name"): self._textStyle(fmt),
})
xSpan.append(xField)
return xSpan
return None
def _emToCm(self, value: float) -> str:
"""Converts an em value to centimetres."""
return f"{value*2.54/72*self._fontSize:.3f}cm"
@@ -828,6 +846,14 @@ class ToOdt(Tokenizer):
_mkTag("style", "class"): "extra",
})
# Numbers Style
xStyl = ET.SubElement(self._xStyl, _mkTag("number", "number-style"), attrib={
_mkTag("style", "name"): S_NUM,
})
ET.SubElement(xStyl, _mkTag("number", "number"), attrib={
_mkTag("number", "min-integer-digits"): "1",
})
return
def _useableStyles(self) -> None:
+23 -7
View File
@@ -40,8 +40,8 @@ from novelwriter.formats.shared import BlockFmt, BlockTyp, T_Formats, TextFmt
from novelwriter.formats.tokenizer import HEADINGS, Tokenizer
from novelwriter.types import (
QtAlignAbsolute, QtAlignCenter, QtAlignJustify, QtAlignLeft, QtAlignRight,
QtPageBreakAfter, QtPageBreakBefore, QtTransparent, QtVAlignNormal,
QtVAlignSub, QtVAlignSuper
QtKeepAnchor, QtMoveAnchor, QtPageBreakAfter, QtPageBreakBefore,
QtTransparent, QtVAlignNormal, QtVAlignSub, QtVAlignSuper
)
logger = logging.getLogger(__name__)
@@ -70,6 +70,7 @@ class ToQTextDocument(Tokenizer):
self._document.setDocumentMargin(0)
self._usedNotes: dict[str, int] = {}
self._usedFields: list[tuple[int, str]] = []
self._init = False
self._bold = QFont.Weight.Bold
@@ -246,11 +247,21 @@ class ToQTextDocument(Tokenizer):
return
def appendFootnotes(self) -> None:
"""Append the footnotes in the buffer."""
if self._usedNotes:
self._document.blockSignals(True)
def closeDocument(self) -> None:
"""Run close document tasks."""
self._document.blockSignals(True)
# Replace fields if there are stats available
if self._usedFields and self._counts:
cursor = QTextCursor(self._document)
for pos, field in reversed(self._usedFields):
if (value := self._counts.get(field)) is not None:
cursor.setPosition(pos, QtMoveAnchor)
cursor.setPosition(pos + 1, QtKeepAnchor)
cursor.insertText(self._formatInt(value))
# Add footnotes
if self._usedNotes:
cursor = QTextCursor(self._document)
cursor.movePosition(QTextCursor.MoveOperation.End)
@@ -268,7 +279,7 @@ class ToQTextDocument(Tokenizer):
cursor.insertText(f"{index}. ", cFmt)
self._insertFragments(*content, cursor, self._charFmt)
self._document.blockSignals(False)
self._document.blockSignals(False)
return
@@ -361,6 +372,11 @@ class ToQTextDocument(Tokenizer):
cursor.insertText(f"[{index}]", xFmt)
else:
cursor.insertText("[ERR]", cFmt)
elif fmt == TextFmt.FIELD:
if field := data.partition(":")[2]:
self._usedFields.append((cursor.position(), field))
cursor.insertText("0", cFmt)
pass
# Move pos for next pass
start = pos
+4
View File
@@ -52,6 +52,10 @@ class ToRaw(Tokenizer):
"""No conversion to perform."""
return
def closeDocument(self) -> None:
"""Nothing to close."""
return
def saveDocument(self, path: Path) -> None:
"""Save the raw text to a plain text file."""
if path.suffix.lower() == ".json":
+2 -6
View File
@@ -2065,12 +2065,8 @@ class GuiDocEditor(QPlainTextEdit):
feature for French, Spanish, etc, so it doesn't insert a
space before colons in meta data lines. See issue #1090.
"""
if char == ":" and len(text) > 1:
if text[0] == "@":
return False
if text[0] == "%":
if text[1:].lstrip()[:9].lower() == "synopsis:":
return False
if char == ":" and len(text) > 1 and text[0] == "@":
return False
return True
def _autoSelect(self) -> QTextCursor:
+1
View File
@@ -523,6 +523,7 @@ class TextBlockData(QTextBlockUserData):
self._metaData.append((s, e, res.group(0), "url"))
self._text = text
self._offset = offset
return
+1 -1
View File
@@ -229,7 +229,7 @@ class GuiDocViewer(QTextBrowser):
qDoc.doPreProcessing()
qDoc.tokenizeText()
qDoc.doConvert()
qDoc.appendFootnotes()
qDoc.closeDocument()
except Exception:
logger.error("Failed to generate preview for document with handle '%s'", tHandle)
logException()
+15 -19
View File
@@ -34,7 +34,10 @@ from PyQt5.QtWidgets import QAction, QMenuBar
from novelwriter import CONFIG, SHARED
from novelwriter.common import openExternalPath, qtLambda
from novelwriter.constants import nwConst, nwKeyWords, nwLabels, nwStyles, nwUnicode, trConst
from novelwriter.constants import (
nwConst, nwKeyWords, nwLabels, nwShortcode, nwStats, nwStyles, nwUnicode,
trConst
)
from novelwriter.enum import nwDocAction, nwDocInsert, nwFocus, nwView
from novelwriter.extensions.eventfilters import StatusTipFilter
@@ -571,25 +574,11 @@ class GuiMainMenu(QMenuBar):
# Insert > Tags and References
self.mInsKeywords = self.insMenu.addMenu(self.tr("Tags and References"))
self.mInsKWItems = {}
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
self.mInsKWItems[nwKeyWords.FOCUS_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, F")
self.mInsKWItems[nwKeyWords.CHAR_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, C")
self.mInsKWItems[nwKeyWords.PLOT_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, P")
self.mInsKWItems[nwKeyWords.TIME_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, T")
self.mInsKWItems[nwKeyWords.WORLD_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, L")
self.mInsKWItems[nwKeyWords.OBJECT_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, O")
self.mInsKWItems[nwKeyWords.ENTITY_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, E")
self.mInsKWItems[nwKeyWords.CUSTOM_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, X")
self.mInsKWItems[nwKeyWords.MENTION_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, M")
for key in self.mInsKWItems:
action = self.mInsKWItems[key][0]
action.setText(trConst(nwLabels.KEY_NAME[key]))
action.setShortcut(self.mInsKWItems[key][1])
for key in nwKeyWords.ALL_KEYS:
action = self.mInsKeywords.addAction(trConst(nwLabels.KEY_NAME[key]))
action.setShortcut(nwLabels.KEY_SHORTCUT[key])
action.triggered.connect(qtLambda(self.requestDocKeyWordInsert.emit, key))
self.mInsKeywords.addAction(action)
self.mainGui.addAction(self.mInsKWItems[key][0])
self.mainGui.addAction(action)
# Insert > Special Comments
self.mInsComments = self.insMenu.addMenu(self.tr("Special Comments"))
@@ -610,6 +599,13 @@ class GuiMainMenu(QMenuBar):
)
self.mainGui.addAction(self.aInsShort)
# Insert > Word/Character Count
self.mInsField = self.insMenu.addMenu(self.tr("Word/Character Count"))
for field in nwStats.ALL_FIELDS:
value = nwShortcode.FIELD_VALUE.format(field)
action = self.mInsField.addAction(trConst(nwLabels.STATS_NAME[field]))
action.triggered.connect(qtLambda(self.requestDocInsertText.emit, value))
# Insert > Breaks and Vertical Space
self.mInsBreaks = self.insMenu.addMenu(self.tr("Breaks and Vertical Space"))
+56 -55
View File
@@ -43,6 +43,7 @@ from PyQt5.QtWidgets import (
from novelwriter import CONFIG, SHARED
from novelwriter.common import fuzzyTime
from novelwriter.constants import nwLabels, nwStats, trConst
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog
@@ -928,8 +929,7 @@ class _StatsWidget(QWidget):
self.toggleButton = NIconToggleButton(self, SHARED.theme.baseIconSize, "unfold")
self.toggleButton.toggled.connect(self._toggleView)
self._buildMinimal()
self._buildMaximal()
self._buildStatsPanel()
self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.minWidget)
@@ -949,23 +949,23 @@ class _StatsWidget(QWidget):
def updateStats(self, data: dict[str, int]) -> None:
"""Update the stats values from a Tokenizer stats dict."""
# Minimal
self.minWordCount.setText("{0:n}".format(data.get("allWords", 0)))
self.minCharCount.setText("{0:n}".format(data.get("allChars", 0)))
self.minWordCount.setText("{0:n}".format(data.get(nwStats.WORDS_ALL, 0)))
self.minCharCount.setText("{0:n}".format(data.get(nwStats.CHARS_ALL, 0)))
# Maximal
self.maxTotalWords.setText("{0:n}".format(data.get("allWords", 0)))
self.maxHeadWords.setText("{0:n}".format(data.get("titleWords", 0)))
self.maxTextWords.setText("{0:n}".format(data.get("textWords", 0)))
self.maxTitleCount.setText("{0:n}".format(data.get("titleCount", 0)))
self.maxParCount.setText("{0:n}".format(data.get("paragraphCount", 0)))
self.maxTotalWords.setText("{0:n}".format(data.get(nwStats.WORDS_ALL, 0)))
self.maxHeadWords.setText("{0:n}".format(data.get(nwStats.WORDS_TITLE, 0)))
self.maxTextWords.setText("{0:n}".format(data.get(nwStats.WORDS_TEXT, 0)))
self.maxTitleCount.setText("{0:n}".format(data.get(nwStats.TITLES, 0)))
self.maxParCount.setText("{0:n}".format(data.get(nwStats.PARAGRAPHS, 0)))
self.maxTotalChars.setText("{0:n}".format(data.get("allChars", 0)))
self.maxHeaderChars.setText("{0:n}".format(data.get("titleChars", 0)))
self.maxTextChars.setText("{0:n}".format(data.get("textChars", 0)))
self.maxTotalChars.setText("{0:n}".format(data.get(nwStats.CHARS_ALL, 0)))
self.maxHeaderChars.setText("{0:n}".format(data.get(nwStats.CHARS_TITLE, 0)))
self.maxTextChars.setText("{0:n}".format(data.get(nwStats.CHARS_TEXT, 0)))
self.maxTotalWordChars.setText("{0:n}".format(data.get("allWordChars", 0)))
self.maxHeadWordChars.setText("{0:n}".format(data.get("titleWordChars", 0)))
self.maxTextWordChars.setText("{0:n}".format(data.get("textWordChars", 0)))
self.maxTotalWordChars.setText("{0:n}".format(data.get(nwStats.WCHARS_ALL, 0)))
self.maxHeadWordChars.setText("{0:n}".format(data.get(nwStats.WCHARS_TITLE, 0)))
self.maxTextWordChars.setText("{0:n}".format(data.get(nwStats.WCHARS_TEXT, 0)))
return
@@ -994,37 +994,29 @@ class _StatsWidget(QWidget):
# Internal Functions
##
def _buildMinimal(self) -> None:
def _buildStatsPanel(self) -> None:
"""Build the minimal stats page."""
mPx = CONFIG.pxInt(8)
self.lblWordCount = QLabel(self.tr("Words"), self)
self.minWordCount = QLabel(self)
self.lblCharCount = QLabel(self.tr("Characters"), self)
self.minCharCount = QLabel(self)
# Assemble
self.minLayout = QHBoxLayout()
self.minLayout.addWidget(self.lblWordCount)
self.minLayout.addWidget(self.minWordCount)
self.minLayout.addSpacing(mPx)
self.minLayout.addWidget(self.lblCharCount)
self.minLayout.addWidget(self.minCharCount)
self.minLayout.addStretch(1)
self.minLayout.setSpacing(mPx)
self.minLayout.setContentsMargins(0, 0, 0, 0)
self.minWidget.setLayout(self.minLayout)
return
def _buildMaximal(self) -> None:
"""Build the maximal stats page."""
hPx = CONFIG.pxInt(12)
vPx = CONFIG.pxInt(4)
# Left Column
trAllChars = trConst(nwLabels.STATS_NAME[nwStats.CHARS_ALL])
trTextChars = trConst(nwLabels.STATS_NAME[nwStats.CHARS_TEXT])
trTitleChars = trConst(nwLabels.STATS_NAME[nwStats.CHARS_TITLE])
trParagraphCount = trConst(nwLabels.STATS_NAME[nwStats.PARAGRAPHS])
trTitleCount = trConst(nwLabels.STATS_NAME[nwStats.TITLES])
trAllWordChars = trConst(nwLabels.STATS_NAME[nwStats.WCHARS_ALL])
trTextWordChars = trConst(nwLabels.STATS_NAME[nwStats.WCHARS_TEXT])
trTitleWordChars = trConst(nwLabels.STATS_NAME[nwStats.WCHARS_TITLE])
trAllWords = trConst(nwLabels.STATS_NAME[nwStats.WORDS_ALL])
trTextWords = trConst(nwLabels.STATS_NAME[nwStats.WORDS_TEXT])
trTitleWords = trConst(nwLabels.STATS_NAME[nwStats.WORDS_TITLE])
# Minimal Form
self.minWordCount = QLabel(self)
self.minCharCount = QLabel(self)
# Maximal Form, Left Column
self.maxTotalWords = QLabel(self)
self.maxHeadWords = QLabel(self)
self.maxTextWords = QLabel(self)
@@ -1038,20 +1030,19 @@ class _StatsWidget(QWidget):
self.maxParCount.setAlignment(QtAlignRight)
self.leftForm = QFormLayout()
self.leftForm.addRow(self.tr("Words"), self.maxTotalWords)
self.leftForm.addRow(self.tr("Words in Headings"), self.maxHeadWords)
self.leftForm.addRow(self.tr("Words in Text"), self.maxTextWords)
self.leftForm.addRow(trAllWords, self.maxTotalWords)
self.leftForm.addRow(trTitleWords, self.maxHeadWords)
self.leftForm.addRow(trTextWords, self.maxTextWords)
self.leftForm.addRow("", QLabel(self))
self.leftForm.addRow(self.tr("Headings"), self.maxTitleCount)
self.leftForm.addRow(self.tr("Paragraphs"), self.maxParCount)
self.leftForm.addRow(trTitleCount, self.maxTitleCount)
self.leftForm.addRow(trParagraphCount, self.maxParCount)
self.leftForm.setHorizontalSpacing(hPx)
self.leftForm.setVerticalSpacing(vPx)
# Right Column
# Maximal Form, Right Column
self.maxTotalChars = QLabel(self)
self.maxHeaderChars = QLabel(self)
self.maxTextChars = QLabel(self)
self.maxTotalWordChars = QLabel(self)
self.maxHeadWordChars = QLabel(self)
self.maxTextWordChars = QLabel(self)
@@ -1059,22 +1050,31 @@ class _StatsWidget(QWidget):
self.maxTotalChars.setAlignment(QtAlignRight)
self.maxHeaderChars.setAlignment(QtAlignRight)
self.maxTextChars.setAlignment(QtAlignRight)
self.maxTotalWordChars.setAlignment(QtAlignRight)
self.maxHeadWordChars.setAlignment(QtAlignRight)
self.maxTextWordChars.setAlignment(QtAlignRight)
self.rightForm = QFormLayout()
self.rightForm.addRow(self.tr("Characters"), self.maxTotalChars)
self.rightForm.addRow(self.tr("Characters in Headings"), self.maxHeaderChars)
self.rightForm.addRow(self.tr("Characters in Text"), self.maxTextChars)
self.rightForm.addRow(self.tr("Characters, No Spaces"), self.maxTotalWordChars)
self.rightForm.addRow(self.tr("Characters in Headings, No Spaces"), self.maxHeadWordChars)
self.rightForm.addRow(self.tr("Characters in Text, No Spaces"), self.maxTextWordChars)
self.rightForm.addRow(trAllChars, self.maxTotalChars)
self.rightForm.addRow(trTitleChars, self.maxHeaderChars)
self.rightForm.addRow(trTextChars, self.maxTextChars)
self.rightForm.addRow(trAllWordChars, self.maxTotalWordChars)
self.rightForm.addRow(trTitleWordChars, self.maxHeadWordChars)
self.rightForm.addRow(trTextWordChars, self.maxTextWordChars)
self.rightForm.setHorizontalSpacing(hPx)
self.rightForm.setVerticalSpacing(vPx)
# Assemble
self.minLayout = QHBoxLayout()
self.minLayout.addWidget(QLabel(trAllWords, self))
self.minLayout.addWidget(self.minWordCount)
self.minLayout.addSpacing(mPx)
self.minLayout.addWidget(QLabel(trAllChars, self))
self.minLayout.addWidget(self.minCharCount)
self.minLayout.addStretch(1)
self.minLayout.setSpacing(mPx)
self.minLayout.setContentsMargins(0, 0, 0, 0)
self.maxLayout = QHBoxLayout()
self.maxLayout.addLayout(self.leftForm)
self.maxLayout.addLayout(self.rightForm)
@@ -1082,6 +1082,7 @@ class _StatsWidget(QWidget):
self.maxLayout.setSpacing(CONFIG.pxInt(32))
self.maxLayout.setContentsMargins(0, 0, 0, 0)
self.minWidget.setLayout(self.minLayout)
self.maxWidget.setLayout(self.maxLayout)
return
+4 -2
View File
@@ -1,8 +1,8 @@
%%~name: Title Page
%%~path: 7031beac91f75/53b69b83cdafc
%%~kind: NOVEL/DOCUMENT
%%~hash: 4072adb6d21ff877577f033f19714d9bd01396f3
%%~date: Unknown/2024-10-24 16:25:44
%%~hash: 45ec619c9fd1b1185bc35b38d28cda13b05f0a88
%%~date: Unknown/2024-10-28 16:42:26
Jane Smith[br]
42 Main Street[br]
@@ -16,3 +16,5 @@ Jane Smith[br]
>> This is the title page. <<
>> It should be the first document of the project. <<
>> Word Count: [field:textWords] <<
+4 -4
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.6a2" hexVersion="0x020600a2" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-26 16:59:09">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2132" autoCount="279" editTime="95062">
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-28 17:26:03">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2135" autoCount="279" editTime="95108">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -36,13 +36,13 @@
<entry key="i56be10" count="1" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="31" novelWords="1014" notesWords="416">
<content items="31" novelWords="1016" notesWords="416">
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name>
</item>
<item handle="53b69b83cdafc" parent="7031beac91f75" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="136" wordCount="27" paraCount="3" cursorPos="69" />
<meta expanded="no" heading="H1" charCount="148" wordCount="29" paraCount="4" cursorPos="227" />
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="974e400180a99" parent="7031beac91f75" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
+2
View File
@@ -28,6 +28,7 @@ from pathlib import Path
import pytest
from PyQt5.QtCore import QLocale
from PyQt5.QtWidgets import QMessageBox
sys.path.insert(1, str(Path(__file__).parent.parent.absolute()))
@@ -54,6 +55,7 @@ def resetConfigVars():
CONFIG.setGuiFont(None)
CONFIG.setTextFont(None)
CONFIG._homePath = _TMP_ROOT
CONFIG._dLocale = QLocale("en_GB")
CONFIG.guiLocale = "en_GB"
return
+6 -2
View File
@@ -1,12 +1,16 @@
%%~name: Lorem Ipsum
%%~path: b3643d0f92e32/7a992350f3eb6
%%~kind: NOVEL/DOCUMENT
%%~hash: 8efda028000b70be0d7dbe9647b6026082ef05c9
%%~date: Unknown/Unknown
%%~hash: 370be3057fb6c0e0b5e4858915a37da18ffc7675
%%~date: Unknown/2024-10-28 18:18:37
#! Lorem Ipsum
>> **By lipsum.com** <<
>> Word Count: [field:allWords] <<
>> Character Count: [field:allChars] <<
>> “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” <<
>> “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” <<
+6 -6
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.6a2" hexVersion="0x020600a2" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-25 19:16:56">
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="48" autoCount="28" editTime="2374">
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-28 18:19:29">
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="50" autoCount="29" editTime="2448">
<name>Lorem Ipsum</name>
<author>lipsum.com</author>
</project>
@@ -9,7 +9,7 @@
<language>en_GB</language>
<spellChecking auto="no">None</spellChecking>
<lastHandle>
<entry key="editor">88d59a277361b</entry>
<entry key="editor">7a992350f3eb6</entry>
<entry key="viewer">None</entry>
<entry key="novelTree">b3643d0f92e32</entry>
<entry key="outline">None</entry>
@@ -31,17 +31,17 @@
<entry key="id6b1d0" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="21" novelWords="3111" notesWords="738">
<content items="21" novelWords="3115" notesWords="738">
<item handle="b3643d0f92e32" parent="None" root="b3643d0f92e32" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" />
<name status="sbaa94f" import="i613591">Novel</name>
</item>
<item handle="7a992350f3eb6" parent="b3643d0f92e32" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="230" wordCount="40" paraCount="3" cursorPos="14" />
<meta expanded="no" heading="H1" charCount="259" wordCount="44" paraCount="5" cursorPos="116" />
<name status="sedd043" import="i613591" active="yes">Lorem Ipsum</name>
</item>
<item handle="8c58a65414c23" parent="b3643d0f92e32" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H0" charCount="1058" wordCount="176" paraCount="2" cursorPos="43" />
<meta expanded="no" heading="H0" charCount="1058" wordCount="176" paraCount="2" cursorPos="53" />
<name status="sedd043" import="i613591" active="yes">Front Matter</name>
</item>
<item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -7,7 +7,7 @@
"novelWriter.itemIndex": {
"7a992350f3eb6": {
"headings": {
"T0001": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "cCount": 230, "wCount": 40, "pCount": 3, "synopsis": ""}
"T0001": {"level": "H1", "title": "Lorem Ipsum", "line": 1, "tag": "", "cCount": 259, "wCount": 44, "pCount": 5, "synopsis": ""}
}
},
"8c58a65414c23": {
@@ -1,9 +1,9 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Properties xmlns:ns0="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<ns0:TotalTime>39</ns0:TotalTime>
<ns0:Application>novelWriter/2.6a2</ns0:Application>
<ns0:Words>4031</ns0:Words>
<ns0:Characters>21271</ns0:Characters>
<ns0:CharactersWithSpaces>24935</ns0:CharactersWithSpaces>
<ns0:Paragraphs>43</ns0:Paragraphs>
<ns0:TotalTime>40</ns0:TotalTime>
<ns0:Application>novelWriter/2.6a3</ns0:Application>
<ns0:Words>4035</ns0:Words>
<ns0:Characters>21296</ns0:Characters>
<ns0:CharactersWithSpaces>24964</ns0:CharactersWithSpaces>
<ns0:Paragraphs>45</ns0:Paragraphs>
</ns0:Properties>
@@ -1,10 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">2024-10-26T17:01:14</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2024-10-26T17:01:14</dcterms:modified>
<dcterms:created xsi:type="dcterms:W3CDTF">2024-10-28T20:12:57</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2024-10-28T20:12:57</dcterms:modified>
<dc:creator>lipsum.com</dc:creator>
<dc:title>Lorem Ipsum</dc:title>
<dc:language>en-GB</dc:language>
<cp:revision>48</cp:revision>
<dc:language>en_GB</dc:language>
<cp:revision>50</cp:revision>
<cp:lastModifiedBy>lipsum.com</cp:lastModifiedBy>
</cp:coreProperties>
@@ -23,6 +23,34 @@
<w:t>By lipsum.com</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Normal" />
<w:jc w:val="center" />
</w:pPr>
<w:r>
<w:rPr />
<w:t xml:space="preserve">Word Count: </w:t>
</w:r>
<w:r>
<w:rPr />
<w:t>4,035</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Normal" />
<w:jc w:val="center" />
</w:pPr>
<w:r>
<w:rPr />
<w:t xml:space="preserve">Character Count: </w:t>
</w:r>
<w:r>
<w:rPr />
<w:t>27,064</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Normal" />
@@ -8,15 +8,15 @@
</w:compat>
<w:docVars>
<w:docVar w:name="ManuscriptTitleCount" w:val="11" />
<w:docVar w:name="ManuscriptParagraphCount" w:val="43" />
<w:docVar w:name="ManuscriptAllWords" w:val="4031" />
<w:docVar w:name="ManuscriptTextWords" w:val="3707" />
<w:docVar w:name="ManuscriptParagraphCount" w:val="45" />
<w:docVar w:name="ManuscriptAllWords" w:val="4035" />
<w:docVar w:name="ManuscriptTextWords" w:val="3711" />
<w:docVar w:name="ManuscriptTitleWords" w:val="21" />
<w:docVar w:name="ManuscriptAllChars" w:val="27035" />
<w:docVar w:name="ManuscriptTextChars" w:val="24935" />
<w:docVar w:name="ManuscriptAllChars" w:val="27064" />
<w:docVar w:name="ManuscriptTextChars" w:val="24964" />
<w:docVar w:name="ManuscriptTitleChars" w:val="123" />
<w:docVar w:name="ManuscriptAllWordChars" w:val="23095" />
<w:docVar w:name="ManuscriptTextWordChars" w:val="21271" />
<w:docVar w:name="ManuscriptAllWordChars" w:val="23120" />
<w:docVar w:name="ManuscriptTextWordChars" w:val="21296" />
<w:docVar w:name="ManuscriptTitleWordChars" w:val="113" />
</w:docVars>
</w:settings>
@@ -6,7 +6,7 @@
<w:rFonts w:ascii="Source Sans Pro" w:hAnsi="Source Sans Pro" w:cs="Source Sans Pro" />
<w:sz w:val="24" />
<w:szCs w:val="24" />
<w:lang w:val="en-GB" />
<w:lang w:val="en_GB" />
</w:rPr>
</w:rPrDefault>
<w:pPrDefault>
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<meta:creation-date>2024-10-22T14:19:48</meta:creation-date>
<meta:generator>novelWriter/2.6a1</meta:generator>
<meta:creation-date>2024-10-29T23:24:19</meta:creation-date>
<meta:generator>novelWriter/2.6a3</meta:generator>
<meta:initial-creator>Jane Smith</meta:initial-creator>
<meta:editing-cycles>1234</meta:editing-cycles>
<meta:editing-duration>P42DT12H34M56S</meta:editing-duration>
<dc:title>Test Project</dc:title>
<dc:date>2024-10-22T14:19:48</dc:date>
<dc:date>2024-10-29T23:24:19</dc:date>
<dc:creator>Jane Smith</dc:creator>
</office:meta>
<office:font-face-decls>
@@ -16,7 +16,7 @@
<office:styles>
<style:default-style style:family="paragraph">
<style:paragraph-properties style:line-break="strict" style:tab-stop-distance="1.251cm" style:writing-mode="page" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" fo:language="nb" fo:country="NO" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" fo:language="en" fo:country="GB" />
</style:default-style>
<style:style style:name="Standard" style:family="paragraph" style:class="text">
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="12pt" />
@@ -26,6 +26,9 @@
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
</style:style>
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
<number:number-style style:name="N0">
<number:number number:min-integer-digits="1" />
</number:number-style>
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.246cm" fo:line-height="115%" fo:text-align="left" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
@@ -79,6 +82,12 @@
<style:style style:name="P1" style:family="paragraph" style:parent-style-name="Heading_20_2">
<style:paragraph-properties fo:break-before="page" />
</style:style>
<style:style style:name="T1" style:family="text">
<style:text-properties fo:font-weight="bold" />
</style:style>
<style:style style:name="T2" style:family="text">
<style:text-properties fo:color="#4271ae" style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color" />
</style:style>
</office:automatic-styles>
<office:master-styles>
<style:master-page style:name="Standard" style:page-layout-name="PM1">
@@ -92,10 +101,30 @@
</office:master-styles>
<office:body>
<office:text>
<text:h text:style-name="Heading_20_2" text:outline-level="2">Chapter One</text:h>
<text:p text:style-name="Text_20_body">Text</text:p>
<text:user-field-decls>
<text:user-field-decl office:value-type="float" office:value="3" text:name="ManuscriptTitleCount" />
<text:user-field-decl office:value-type="float" office:value="3" text:name="ManuscriptParagraphCount" />
<text:user-field-decl office:value-type="float" office:value="217" text:name="ManuscriptAllWords" />
<text:user-field-decl office:value-type="float" office:value="211" text:name="ManuscriptTextWords" />
<text:user-field-decl office:value-type="float" office:value="6" text:name="ManuscriptTitleWords" />
<text:user-field-decl office:value-type="float" office:value="1471" text:name="ManuscriptAllChars" />
<text:user-field-decl office:value-type="float" office:value="1441" text:name="ManuscriptTextChars" />
<text:user-field-decl office:value-type="float" office:value="30" text:name="ManuscriptTitleChars" />
<text:user-field-decl office:value-type="float" office:value="1258" text:name="ManuscriptAllWordChars" />
<text:user-field-decl office:value-type="float" office:value="1231" text:name="ManuscriptTextWordChars" />
<text:user-field-decl office:value-type="float" office:value="27" text:name="ManuscriptTitleWordChars" />
</text:user-field-decls>
<text:p text:style-name="Title">My Novel</text:p>
<text:p text:style-name="Text_20_body"><text:span text:style-name="T1">Word Count: </text:span><text:span text:style-name="T1"><text:user-field-get style:data-style-name="N0" text:name="ManuscriptAllWords">0</text:user-field-get></text:span><text:line-break /><text:user-field-get style:data-style-name="N0" text:name="ManuscriptParagraphCount">0</text:user-field-get> paragrphs<text:line-break />Web: <text:a xlink:type="simple" xlink:href="http://example.com" text:style-name="T2">http://example.com</text:a></text:p>
<text:h text:style-name="P1" text:outline-level="2">Chapter One</text:h>
<text:p text:style-name="Text_20_body">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.</text:p>
<text:h text:style-name="P1" text:outline-level="2">Chapter Two</text:h>
<text:p text:style-name="Text_20_body">Text</text:p>
<text:p text:style-name="Text_20_body">Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.<text:note text:id="ftn1" text:note-class="footnote">
<text:note-citation>1</text:note-citation>
<text:note-body>
<text:p text:style-name="Footnote">Lorem ipsum</text:p>
</text:note-body>
</text:note></text:p>
</office:text>
</office:body>
</office:document>
+38 -4
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document-content xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3">
<office:document-content xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3">
<office:font-face-decls>
<style:font-face style:name="Liberation Serif" style:font-pitch="variable" />
</office:font-face-decls>
@@ -7,13 +7,47 @@
<style:style style:name="P1" style:family="paragraph" style:parent-style-name="Heading_20_2">
<style:paragraph-properties fo:break-before="page" />
</style:style>
<style:style style:name="T1" style:family="text">
<style:text-properties fo:font-weight="bold" />
</style:style>
<style:style style:name="T2" style:family="text">
<style:text-properties fo:color="#4271ae" style:text-underline-style="solid" style:text-underline-width="auto" style:text-underline-color="font-color" />
</style:style>
</office:automatic-styles>
<office:body>
<office:text>
<text:h text:style-name="Heading_20_2" text:outline-level="2">Chapter One</text:h>
<text:p text:style-name="Text_20_body">Text</text:p>
<text:user-field-decls>
<text:user-field-decl office:value-type="float" office:value="3" text:name="ManuscriptTitleCount" />
<text:user-field-decl office:value-type="float" office:value="3" text:name="ManuscriptParagraphCount" />
<text:user-field-decl office:value-type="float" office:value="217" text:name="ManuscriptAllWords" />
<text:user-field-decl office:value-type="float" office:value="211" text:name="ManuscriptTextWords" />
<text:user-field-decl office:value-type="float" office:value="6" text:name="ManuscriptTitleWords" />
<text:user-field-decl office:value-type="float" office:value="1471" text:name="ManuscriptAllChars" />
<text:user-field-decl office:value-type="float" office:value="1441" text:name="ManuscriptTextChars" />
<text:user-field-decl office:value-type="float" office:value="30" text:name="ManuscriptTitleChars" />
<text:user-field-decl office:value-type="float" office:value="1258" text:name="ManuscriptAllWordChars" />
<text:user-field-decl office:value-type="float" office:value="1231" text:name="ManuscriptTextWordChars" />
<text:user-field-decl office:value-type="float" office:value="27" text:name="ManuscriptTitleWordChars" />
</text:user-field-decls>
<text:p text:style-name="Title">My Novel</text:p>
<text:p text:style-name="Text_20_body">
<text:span text:style-name="T1">Word Count: </text:span>
<text:span text:style-name="T1">
<text:user-field-get style:data-style-name="N0" text:name="ManuscriptAllWords">0</text:user-field-get>
</text:span>
<text:line-break />
<text:user-field-get style:data-style-name="N0" text:name="ManuscriptParagraphCount">0</text:user-field-get> paragrphs<text:line-break />Web: <text:a xlink:type="simple" xlink:href="http://example.com" text:style-name="T2">http://example.com</text:a>
</text:p>
<text:h text:style-name="P1" text:outline-level="2">Chapter One</text:h>
<text:p text:style-name="Text_20_body">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc maximus justo non dictum commodo. Curabitur lacinia tempor orci vel luctus. Phasellus porta metus eu massa luctus, eget euismod risus rhoncus. Vestibulum sed arcu nisi. Maecenas pretium facilisis velit, vel semper lacus aliquam sit amet. Vestibulum vulputate neque ligula, rhoncus blandit turpis consequat id. Mauris sagittis vehicula imperdiet. Duis sed nunc pretium, ornare purus vel, sodales augue. Maecenas a suscipit risus. Quisque volutpat justo eleifend est ullamcorper fermentum. Donec ullamcorper et tortor a laoreet. Nam id risus nisi. Vivamus non imperdiet erat, sit amet imperdiet felis. Mauris vitae neque et est aliquam scelerisque non non ipsum.</text:p>
<text:h text:style-name="P1" text:outline-level="2">Chapter Two</text:h>
<text:p text:style-name="Text_20_body">Text</text:p>
<text:p text:style-name="Text_20_body">Nullam laoreet lorem nec malesuada vehicula. Vivamus tempus sodales lectus sed viverra. Aenean lacinia sollicitudin quam, quis tempus eros suscipit id. Duis sed rutrum nisi, ut pulvinar magna. Nam et cursus tortor. Phasellus ac odio tellus. Nullam in iaculis ipsum. Vivamus ante sem, ultricies sed varius quis, tristique nec tellus. Nullam eu urna vitae lacus hendrerit gravida. Quisque pulvinar erat ex, id efficitur velit sodales vitae. Proin vestibulum, sapien eget mattis euismod, tortor quam viverra risus, at congue mauris tortor eu nunc. Mauris pellentesque elit leo, quis eleifend sem placerat a. Vivamus iaculis dui eget tellus volutpat, ac varius nisi facilisis.<text:note text:id="ftn1" text:note-class="footnote">
<text:note-citation>1</text:note-citation>
<text:note-body>
<text:p text:style-name="Footnote">Lorem ipsum</text:p>
</text:note-body>
</text:note>
</text:p>
</office:text>
</office:body>
</office:document-content>
+4 -1
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document-styles xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3">
<office:document-styles xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3">
<office:font-face-decls>
<style:font-face style:name="Liberation Serif" style:font-pitch="variable" />
</office:font-face-decls>
@@ -16,6 +16,9 @@
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
</style:style>
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
<number:number-style style:name="N0">
<number:number number:min-integer-digits="1" />
</number:number-style>
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.246cm" fo:line-height="115%" fo:text-align="left" />
<style:text-properties style:font-name="Liberation Serif" fo:font-family="Liberation Serif" fo:font-size="12pt" fo:font-weight="normal" />
@@ -1,8 +1,8 @@
%%~name: New Scene
%%~path: 000000000000d/000000000000f
%%~kind: NOVEL/DOCUMENT
%%~hash: 0566024662fb6ed7ed645717addd64dabf058915
%%~date: 2024-10-27 13:03:13/2024-10-27 13:03:18
%%~hash: 89b54ddaec2fddfd220e91dd438c84fb3aef9fc2
%%~date: 2024-10-30 00:07:21/2024-10-30 00:07:26
# Novel
## Chapter
@@ -40,7 +40,9 @@ Some “ double quoted text with spaces padded ”.
@object: NoSpaceAdded
% synopsis: No space before this colon.
% synopsis : Space before this is OK.
%Footnote.abc : A simple footnote.
Add space before this colon : See?
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-27 13:01:24">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="6">
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-30 00:06:45">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="5">
<name>New Project</name>
<author>Jane Doe</author>
</project>
@@ -46,7 +46,7 @@
<name status="s000000" import="i000004" active="yes">New Chapter</name>
</item>
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H1" charCount="918" wordCount="154" paraCount="17" cursorPos="1140" />
<meta expanded="no" heading="H1" charCount="918" wordCount="154" paraCount="17" cursorPos="1174" />
<name status="s000000" import="i000004" active="yes">New Scene</name>
</item>
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
@@ -1,5 +1,5 @@
"Title","Document","Words","Pars","POV","Characters","Plot","Locations","Synopsis"
"Lorem Ipsum","Lorem Ipsum","40","3","","","","",""
"Lorem Ipsum","Lorem Ipsum","44","5","","","","",""
"Prologue","Prologue","94","2","","","","","Explanation from the lipsum.com website."
"Act One","Act One","6","1","","","","",""
"Chapter One","Chapter One","67","1","Bod","","Main","Europe","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."
1 Title Document Words Pars POV Characters Plot Locations Synopsis
2 Lorem Ipsum Lorem Ipsum 40 44 3 5
3 Prologue Prologue 94 2 Explanation from the lipsum.com website.
4 Act One Act One 6 1
5 Chapter One Chapter One 67 1 Bod Main Europe Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.
@@ -2,6 +2,10 @@
**By lipsum.com**
Word Count: 4,169
Character Count: 27,898
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
@@ -21,6 +21,8 @@ h4 {margin-top: 1.53em; margin-bottom: 0.65em;}
<article>
<h1 class='title' style='text-align: center;'>Lorem Ipsum</h1>
<p style='text-align: center;'><strong>By lipsum.com</strong></p>
<p style='text-align: center;'>Word Count: 4,169</p>
<p style='text-align: center;'>Character Count: 27,898</p>
<p style='text-align: center;'>“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”</p>
<p style='text-align: center;'>“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”</p>
<p class='comment' style='text-align: justify; page-break-before: always;'><strong><span style='color: #646464'>Comment:</span></strong> <span style='color: #646464'>Exctracted from the lipsum.com website.</span></p>
@@ -2,8 +2,8 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1729877010,
"buildTimeStr": "2024-10-25 19:23:30"
"buildTime": 1730136403,
"buildTimeStr": "2024-10-28 18:26:43"
},
"text": {
"css": [
@@ -23,6 +23,8 @@
[
"<h1 class='title' style='text-align: center;'>Lorem Ipsum</h1>",
"<p style='text-align: center;'><strong>By lipsum.com</strong></p>",
"<p style='text-align: center;'>Word Count: 4,169</p>",
"<p style='text-align: center;'>Character Count: 27,898</p>",
"<p style='text-align: center;'>\u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d</p>",
"<p style='text-align: center;'>\u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d</p>"
],
@@ -2,8 +2,8 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1729877010,
"buildTimeStr": "2024-10-25 19:23:30"
"buildTime": 1730136328,
"buildTimeStr": "2024-10-28 18:25:28"
},
"text": {
"nwd": [
@@ -12,6 +12,10 @@
"",
">> **By lipsum.com** <<",
"",
">> Word Count: [field:allWords] <<",
"",
">> Character Count: [field:allChars] <<",
"",
">> \u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d <<",
"",
">> \u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d <<"
@@ -2,6 +2,10 @@
>> **By lipsum.com** <<
>> Word Count: [field:allWords] <<
>> Character Count: [field:allChars] <<
>> “Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…” <<
>> “There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…” <<
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<meta:creation-date>2024-10-25T19:19:43</meta:creation-date>
<meta:generator>novelWriter/2.6a2</meta:generator>
<meta:creation-date>2024-10-29T09:41:30</meta:creation-date>
<meta:generator>novelWriter/2.6a3</meta:generator>
<meta:initial-creator>lipsum.com</meta:initial-creator>
<meta:editing-cycles>48</meta:editing-cycles>
<meta:editing-duration>P0DT0H39M34S</meta:editing-duration>
<meta:editing-cycles>50</meta:editing-cycles>
<meta:editing-duration>P0DT0H40M48S</meta:editing-duration>
<dc:title>Lorem Ipsum</dc:title>
<dc:date>2024-10-25T19:19:43</dc:date>
<dc:date>2024-10-29T09:41:30</dc:date>
<dc:creator>lipsum.com</dc:creator>
</office:meta>
<office:font-face-decls>
@@ -26,6 +26,9 @@
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-weight="normal" fo:font-style="normal" fo:font-size="15pt" />
</style:style>
<style:style style:name="Header_20_and_20_Footer" style:display-name="Header and Footer" style:family="paragraph" style:parent-style-name="Standard" style:class="extra" />
<number:number-style style:name="N0">
<number:number number:min-integer-digits="1" />
</number:number-style>
<style:style style:name="Text_20_body" style:family="paragraph" style:display-name="Text body" style:parent-style-name="Standard" style:class="text">
<style:paragraph-properties fo:margin-top="0.000cm" fo:margin-bottom="0.320cm" fo:line-height="150%" fo:text-align="left" />
<style:text-properties style:font-name="Arial" fo:font-family="Arial" fo:font-size="12pt" fo:font-weight="normal" />
@@ -151,19 +154,21 @@
<office:text>
<text:user-field-decls>
<text:user-field-decl office:value-type="float" office:value="19" text:name="ManuscriptTitleCount" />
<text:user-field-decl office:value-type="float" office:value="46" text:name="ManuscriptParagraphCount" />
<text:user-field-decl office:value-type="float" office:value="4165" text:name="ManuscriptAllWords" />
<text:user-field-decl office:value-type="float" office:value="3811" text:name="ManuscriptTextWords" />
<text:user-field-decl office:value-type="float" office:value="48" text:name="ManuscriptParagraphCount" />
<text:user-field-decl office:value-type="float" office:value="4169" text:name="ManuscriptAllWords" />
<text:user-field-decl office:value-type="float" office:value="3815" text:name="ManuscriptTextWords" />
<text:user-field-decl office:value-type="float" office:value="54" text:name="ManuscriptTitleWords" />
<text:user-field-decl office:value-type="float" office:value="27869" text:name="ManuscriptAllChars" />
<text:user-field-decl office:value-type="float" office:value="25549" text:name="ManuscriptTextChars" />
<text:user-field-decl office:value-type="float" office:value="27898" text:name="ManuscriptAllChars" />
<text:user-field-decl office:value-type="float" office:value="25578" text:name="ManuscriptTextChars" />
<text:user-field-decl office:value-type="float" office:value="310" text:name="ManuscriptTitleChars" />
<text:user-field-decl office:value-type="float" office:value="23801" text:name="ManuscriptAllWordChars" />
<text:user-field-decl office:value-type="float" office:value="21781" text:name="ManuscriptTextWordChars" />
<text:user-field-decl office:value-type="float" office:value="23826" text:name="ManuscriptAllWordChars" />
<text:user-field-decl office:value-type="float" office:value="21806" text:name="ManuscriptTextWordChars" />
<text:user-field-decl office:value-type="float" office:value="275" text:name="ManuscriptTitleWordChars" />
</text:user-field-decls>
<text:p text:style-name="Title">Lorem Ipsum</text:p>
<text:p text:style-name="P1"><text:span text:style-name="T1">By lipsum.com</text:span></text:p>
<text:p text:style-name="P1">Word Count: <text:user-field-get style:data-style-name="N0" text:name="ManuscriptAllWords">0</text:user-field-get></text:p>
<text:p text:style-name="P1">Character Count: <text:user-field-get style:data-style-name="N0" text:name="ManuscriptAllChars">0</text:user-field-get></text:p>
<text:p text:style-name="P1">“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”</text:p>
<text:p text:style-name="P1">“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”</text:p>
<text:p text:style-name="P2"><text:span text:style-name="T2">Comment:</text:span> <text:span text:style-name="T3">Exctracted from the lipsum.com website.</text:span></text:p>
@@ -2,6 +2,10 @@
**By lipsum.com**
Word Count: 4,169
Character Count: 27,898
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
+28 -2
View File
@@ -37,8 +37,8 @@ from novelwriter.common import (
formatTimeStamp, formatVersion, fuzzyTime, getFileSize, hexToInt, isHandle,
isItemClass, isItemLayout, isItemType, isListInstance, isTitleTag,
jsonEncode, makeFileNameSafe, minmax, numberToRoman, openExternalPath,
readTextFile, simplified, transferCase, uniqueCompact, xmlIndent,
xmlSubElem, yesNo
readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
xmlIndent, xmlSubElem, yesNo
)
from tests.mocked import causeOSError
@@ -634,6 +634,29 @@ def testBaseCommon_xmlIndent():
assert data == "foobar"
@pytest.mark.base
def testBaseCommon_xmlElement():
"""Test the xmlElement function."""
assert ET.tostring(
xmlElement("node", None, attrib={"a": "b"})
) == b'<node a="b" />'
assert ET.tostring(
xmlElement("node", "text", attrib={"a": "b"})
) == b'<node a="b">text</node>'
assert ET.tostring(
xmlElement("node", "text", tail="foo", attrib={"a": "b"})
) == b'<node a="b">text</node>foo'
assert ET.tostring(
xmlElement("node", 42, attrib={"a": "b"})
) == b'<node a="b">42</node>'
assert ET.tostring(
xmlElement("node", 3.14, attrib={"a": "b"})
) == b'<node a="b">3.14</node>'
assert ET.tostring(
xmlElement("node", True, attrib={"a": "b"})
) == b'<node a="b">true</node>'
@pytest.mark.base
def testBaseCommon_xmlSubElem():
"""Test the xmlSubElem function."""
@@ -643,6 +666,9 @@ def testBaseCommon_xmlSubElem():
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", "text", attrib={"a": "b"})
) == b'<node a="b">text</node>'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", "text", tail="foo", attrib={"a": "b"})
) == b'<node a="b">text</node>foo'
assert ET.tostring(
xmlSubElem(ET.Element("r"), "node", 42, attrib={"a": "b"})
) == b'<node a="b">42</node>'
+68 -14
View File
@@ -21,6 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import json
import zipfile
from pathlib import Path
from shutil import copyfile
@@ -130,6 +131,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
assert error == []
assert docFile.is_file()
assert zipfile.is_zipfile(docFile)
# Check Error Handling
# ====================
@@ -319,7 +321,72 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
@pytest.mark.core
def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
def testCoreDocBuild_DocX(mockGUI, prjLipsum, fncPath):
"""Test building a Word manuscript."""
project = NWProject()
project.openProject(prjLipsum)
build = BuildSettings()
build.unpack(BUILD_CONF)
docBuild = NWBuildDocument(project, build)
docBuild.queueAll()
assert len(docBuild) == 21
# Check Build
# ===========
docFile = fncPath / "Lorem Ipsum.docx"
count = 0
error = []
for _, success in docBuild.iterBuildDocument(docFile, nwBuildFmt.DOCX):
count += 1 if success else 0
if docBuild.error:
error.append(docBuild.error)
assert count == 19
assert error == []
assert docFile.is_file()
assert zipfile.is_zipfile(docFile)
@pytest.mark.core
def testCoreDocBuild_PDF(mockGUI, prjLipsum, fncPath):
"""Test building a PDF manuscript."""
project = NWProject()
project.openProject(prjLipsum)
build = BuildSettings()
build.unpack(BUILD_CONF)
docBuild = NWBuildDocument(project, build)
docBuild.queueAll()
assert len(docBuild) == 21
# Check Build
# ===========
docFile = fncPath / "Lorem Ipsum.pdf"
count = 0
error = []
for _, success in docBuild.iterBuildDocument(docFile, nwBuildFmt.PDF):
count += 1 if success else 0
if docBuild.error:
error.append(docBuild.error)
assert count == 19
assert error == []
assert docFile.is_file()
@pytest.mark.core
def testCoreDocBuild_NWD(mockGUI, prjLipsum, fncPath, tstPaths):
"""Test building a NWD manuscript."""
project = NWProject()
project.openProject(prjLipsum)
@@ -372,19 +439,6 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile, ignoreLines=[5, 6])
# Check Error Handling
# ====================
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
docFile = fncPath / "Lorem Ipsum Err.md"
for _ in docBuild.iterBuildDocument(docFile, nwBuildFmt.NWD):
pass
assert docBuild.error == "OSError: Mock OSError"
assert not docFile.is_file()
@pytest.mark.core
def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
+1
View File
@@ -1490,5 +1490,6 @@ def testCoreIndex_processComment():
# Padding with term
assert processComment("%note.term: Hi") == (nwComment.NOTE, "term", "Hi", 6, 11)
assert processComment("% note.term: Hi") == (nwComment.NOTE, "term", "Hi", 7, 12)
assert processComment("% note.term : Hi") == (nwComment.NOTE, "term", "Hi", 7, 13)
assert processComment("% note. term : Hi") == (nwComment.PLAIN, "", "note. term : Hi", 0, 0)
assert processComment("% note . term : Hi") == (nwComment.PLAIN, "", "note . term : Hi", 0, 0)
+45 -21
View File
@@ -32,28 +32,9 @@ from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.project import NWProject
from novelwriter.enum import nwBuildFmt
from novelwriter.formats.shared import BlockFmt, BlockTyp
from novelwriter.formats.todocx import ToDocX, _mkTag, _wTag
from novelwriter.formats.todocx import OOXML_SCM, ToDocX, _mkTag, _wTag
from tests.tools import DOCX_IGNORE, cmpFiles
OOXML_SCM = "http://schemas.openxmlformats.org"
XML_NS = [
f' xmlns:r="{OOXML_SCM}/officeDocument/2006/relationships"',
f' xmlns:w="{OOXML_SCM}/wordprocessingml/2006/main"',
f' xmlns:cp="{OOXML_SCM}/package/2006/metadata/core-properties"',
' xmlns:dc="http://purl.org/dc/elements/1.1/"',
' xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"',
' xmlns:xml="http://www.w3.org/XML/1998/namespace"',
' xmlns:dcterms="http://purl.org/dc/terms/"',
]
def xmlToText(xElem):
"""Get the text content of an XML element."""
rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for ns in XML_NS:
rTxt = rTxt.replace(ns, "")
return rTxt
from tests.tools import DOCX_IGNORE, cmpFiles, xmlToText
@pytest.mark.core
@@ -610,6 +591,49 @@ def testFmtToDocX_Footnotes(mockGUI):
)
@pytest.mark.core
def testFmtToDocX_Fields(mockGUI):
"""Test formatting of footnotes."""
project = NWProject()
doc = ToDocX(project)
doc.initDocument()
# Field Builder
xNode = doc._generateField("a:b", 0x00)
assert isinstance(xNode, ET.Element)
assert xmlToText(xNode) == "<w:r><w:rPr /><w:t>0</w:t></w:r>"
assert doc._usedFields == [(xNode.find(_wTag("t")), "b")]
assert doc._generateField("a", 0x00) is None
# Full Processing
doc._text = (
"Word Count: [field:allWords]\n"
"Character Count: [field:allChars]\n"
"Chicken Count: [field:allChickens]\n"
)
doc.tokenizeText()
doc.doConvert()
doc.countStats()
doc._documentXml(None, None)
assert xmlToText(doc._files["document.xml"].xml) == (
'<w:document><w:body><w:p><w:pPr><w:pStyle w:val="Normal" /></w:pPr>'
'<w:r><w:rPr /><w:t xml:space="preserve">Word Count: </w:t></w:r>'
'<w:r><w:rPr /><w:t>6</w:t></w:r>'
'<w:r><w:rPr /><w:br /><w:t xml:space="preserve">Character Count: </w:t></w:r>'
'<w:r><w:rPr /><w:t>46</w:t></w:r>'
'<w:r><w:rPr /><w:br /><w:t xml:space="preserve">Chicken Count: </w:t></w:r>'
'<w:r><w:rPr /><w:t>0</w:t></w:r>'
'</w:p><w:sectPr>'
'<w:footnotePr><w:numFmt w:val="decimal" /></w:footnotePr>'
'<w:pgSz w:w="11905" w:h="16837" w:orient="portrait" />'
'<w:pgMar w:top="1133" w:right="1133" w:bottom="1133" w:left="1133" '
'w:header="566" w:footer="0" w:gutter="0" />'
'<w:pgNumType w:start="1" w:fmt="decimal" /><w:titlePg />'
'</w:sectPr></w:body></w:document>'
)
@pytest.mark.core
def testFmtToDocX_SaveDocument(mockGUI, prjLipsum, fncPath, tstPaths):
"""Test document output."""
+1 -1
View File
@@ -315,7 +315,7 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
"or two<sup>ERR</sup> footnotes.</p>\n"
)
html.appendFootnotes()
html.closeDocument()
assert html._pages[-2] == (
"<p>Text with one<sup><a href='#footnote_1'>1</a></sup> "
"or two<sup>ERR</sup> footnotes.</p>\n"
+32
View File
@@ -44,6 +44,9 @@ class BareTokenizer(Tokenizer):
def doConvert(self):
super().doConvert() # type: ignore (deliberate check)
def closeDocument(self):
super().closeDocument() # type: ignore (deliberate check)
def saveDocument(self, path) -> None:
super().saveDocument(path) # type: ignore (deliberate check)
@@ -57,6 +60,9 @@ def testFmtToken_Abstracts(mockGUI, tstPaths):
with pytest.raises(NotImplementedError):
tokens.doConvert()
with pytest.raises(NotImplementedError):
tokens.closeDocument()
with pytest.raises(NotImplementedError):
tokens.saveDocument(tstPaths)
@@ -1241,6 +1247,32 @@ def testFmtToken_LineBreak(mockGUI):
]
@pytest.mark.core
def testFmtToken_ShortcodeValue(mockGUI):
"""Test processing of shortcodes with values."""
project = NWProject()
tokens = BareTokenizer(project)
tokens._handle = TMH
# Footnote
tokens._text = "Hello World[footnote:abcd] to you!"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "", "Hello World to you!", [
(11, TextFmt.FNOTE, f"{TMH}:abcd"),
], BlockFmt.NONE
)]
# Field
tokens._text = "Hello World: [field:abcd] times!"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.TEXT, "", "Hello World: times!", [
(13, TextFmt.FIELD, f"{TMH}:abcd"),
], BlockFmt.NONE
)]
@pytest.mark.core
def testFmtToken_Dialogue(mockGUI):
"""Test the tokenization of dialogue in the Tokenizer class."""
+1 -1
View File
@@ -205,7 +205,7 @@ def testFmtToMarkdown_ConvertParagraphs(mockGUI):
md.doConvert()
assert md._pages[-1] == "Text with one[1] or two[ERR] footnotes.\n\n"
md.appendFootnotes()
md.closeDocument()
assert md._pages[-2] == (
"Text with one[1] or two[ERR] footnotes.\n\n"
)
+59 -31
View File
@@ -35,26 +35,7 @@ from novelwriter.core.project import NWProject
from novelwriter.formats.shared import BlockFmt, BlockTyp, TextFmt
from novelwriter.formats.toodt import ODTParagraphStyle, ODTTextStyle, ToOdt, XMLParagraph, _mkTag
from tests.tools import ODT_IGNORE, cmpFiles
XML_NS = [
' xmlns:dc="http://purl.org/dc/elements/1.1/"',
' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"',
' xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"',
' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"',
' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"',
' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"',
' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"',
' xmlns:xlink="http://www.w3.org/1999/xlink"',
]
def xmlToText(xElem):
"""Get the text content of an XML element."""
rTxt = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for ns in XML_NS:
rTxt = rTxt.replace(ns, "")
return rTxt
from tests.tools import ODT_IGNORE, cmpFiles, xmlToText
@pytest.mark.core
@@ -237,6 +218,46 @@ def testFmtToOdt_TextFormatting(mockGUI):
)
@pytest.mark.core
def testFmtToOdt_Fields(mockGUI):
"""Test formatting of footnotes."""
project = NWProject()
odt = ToOdt(project, True)
odt.initDocument()
# Field Builder
xNode = odt._generateField("a:allWords", 0x00)
assert isinstance(xNode, ET.Element)
assert xmlToText(xNode) == (
'<text:user-field-getstyle:data-style-name="N0" text:name="ManuscriptAllWords">'
'0</text:user-field-get>'
)
# assert odt._usedFields == [(xNode.find(_wTag("t")), "b")]
assert odt._generateField("a", 0x00) is None
# Full Processing
odt._text = (
"Word Count: [field:allWords]\n"
"Character Count: [field:allChars]\n"
"Chicken Count: [field:allChickens]\n"
)
odt.tokenizeText()
odt.doConvert()
odt.countStats()
assert xmlToText(odt._xBody) == (
'<office:body><office:text>'
'<text:p text:style-name="Text_20_body">'
'Word Count: <text:user-field-get style:data-style-name="N0" '
'text:name="ManuscriptAllWords">0</text:user-field-get><text:line-break />'
'Character Count: <text:user-field-get style:data-style-name="N0" '
'text:name="ManuscriptAllChars">0</text:user-field-get><text:line-break />'
'Chicken Count: <text:user-field-get style:data-style-name="N0" '
'text:name="ManuscriptAllChickens">0</text:user-field-get></text:p>'
'</office:text></office:body>'
)
@pytest.mark.core
def testFmtToOdt_DialogueFormatting(mockGUI):
"""Test formatting of dialogue."""
@@ -770,7 +791,7 @@ def testFmtToOdt_ConvertDirect(mockGUI):
@pytest.mark.core
def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths, ipsumText):
"""Test the document save functions."""
project = NWProject()
project.data.setAuthor("Jane Smith")
@@ -780,11 +801,6 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
odt = ToOdt(project, isFlat=True)
odt._isNovel = True
odt._dLanguage = ""
odt.setLanguage(None) # type: ignore
assert odt._dLanguage == ""
odt.setLanguage("nb_NO")
assert odt._dLanguage == "nb"
odt.setHeaderFormat(nwHeadFmt.DOC_AUTO, 1)
assert odt._headerFormat == nwHeadFmt.DOC_AUTO
odt.setFirstLineIndent(True, 1.4, False)
@@ -801,14 +817,20 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
assert odt._mDocRight == "1.500cm"
odt._text = (
"#! My Novel\n\n"
"**Word Count: [field:allWords]**\n"
"[field:paragraphCount] paragrphs\n"
"Web: http://example.com\n\n"
"## Chapter One\n\n"
"Text\n\n"
f"{ipsumText[0]}\n\n"
"## Chapter Two\n\n"
"Text\n\n"
f"{ipsumText[1]}[footnote:abc]\n\n"
"%Footnote.abc: Lorem ipsum\n\n"
)
odt.tokenizeText()
odt.initDocument()
odt.doConvert()
odt.countStats()
odt.closeDocument()
flatFile = fncPath / "document.fodt"
@@ -823,7 +845,7 @@ def testFmtToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
@pytest.mark.core
def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths):
def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths, ipsumText):
"""Test the document save functions."""
project = NWProject()
project.data.setAuthor("Jane Smith")
@@ -838,14 +860,20 @@ def testFmtToOdt_SaveFull(mockGUI, fncPath, tstPaths):
odt.setHeaderFormat(f"{nwHeadFmt.DOC_PROJECT} - {nwHeadFmt.DOC_AUTHOR}", 0)
odt._text = (
"#! My Novel\n\n"
"**Word Count: [field:allWords]**\n"
"[field:paragraphCount] paragrphs\n"
"Web: http://example.com\n\n"
"## Chapter One\n\n"
"Text\n\n"
f"{ipsumText[0]}\n\n"
"## Chapter Two\n\n"
"Text\n\n"
f"{ipsumText[1]}[footnote:abc]\n\n"
"%Footnote.abc: Lorem ipsum\n\n"
)
odt.tokenizeText()
odt.initDocument()
odt.doConvert()
odt.countStats()
odt.closeDocument()
fullFile = fncPath / "document.odt"
+1 -1
View File
@@ -594,7 +594,7 @@ def testFmtToQTextDocument_Footnotes(mockGUI):
)
doc.tokenizeText()
doc.doConvert()
doc.appendFootnotes()
doc.closeDocument()
assert doc.document.blockCount() == 4
# 0: Scene
+4 -28
View File
@@ -2167,15 +2167,15 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
docEditor._lastFind = None
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == "a"
assert docEditor.getCursorPosition() == 92
assert docEditor.getCursorPosition() == 83
# Iterate through the rest
finds = [104, 123, 175, 197, 206, 211, 220, 238, 250, 250]
for i in range(10):
finds = [85, 105, 110, 141, 169, 181, 200, 252, 274, 283, 288, 297]
for i in range(len(finds)):
docEditor.replaceNext()
assert docEditor.textCursor().selectedText() == "a"
assert docEditor.getCursorPosition() == finds[i]
assert docEditor._lastFind == (249, 250)
assert docEditor._lastFind == (296, 297)
# Search for something that doesn't exist
docSearch.searchBox.setText("x")
@@ -2184,27 +2184,3 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
assert docEditor.textCursor().selectedText() == ""
# qtbot.stop()
@pytest.mark.gui
def testGuiEditor_StaticMethods():
"""Test the document editor's static methods."""
# Check the method that decides if it is allowed to insert a space
# before a colon using the French, Spanish, etc language feature
assert GuiDocEditor._allowSpaceBeforeColon("", "") is True
assert GuiDocEditor._allowSpaceBeforeColon("", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("some text", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("@:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("@>", ">") is True
assert GuiDocEditor._allowSpaceBeforeColon("%", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("%:", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("%synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("% Synopsis:", ":") is False
assert GuiDocEditor._allowSpaceBeforeColon("%synopsis :", ":") is True
assert GuiDocEditor._allowSpaceBeforeColon("%Synopsis :", ":") is True
+7 -2
View File
@@ -474,7 +474,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
# Special Formatting
# ==================
# Insert spaces before colon, but ignore tags and synopsis
# Insert spaces before colon, but ignore tags
docEditor._typPadBefore = ":"
for c in "@object: NoSpaceAdded":
@@ -482,7 +482,12 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
for c in "% synopsis: No space before this colon.":
for c in "% synopsis: Space before this is OK.":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
for c in "%Footnote.abc: A simple footnote.":
qtbot.keyClick(docEditor, c, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY)
+242 -263
View File
@@ -29,7 +29,7 @@ from PyQt5.QtGui import QDesktopServices, QTextBlock, QTextCursor
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode
from novelwriter.enum import nwDocAction, nwDocInsert
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.types import QtKeepAnchor, QtMoveRight
@@ -70,161 +70,163 @@ def testGuiMainMenu_Slots(qtbot, monkeypatch, nwGUI, projPath):
def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
"""Test the main menu Edit and Format entries."""
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *a: True)
mainMenu = nwGUI.mainMenu
docEditor = nwGUI.docEditor
# Test Document Action with No Project
assert nwGUI.docEditor.docAction(nwDocAction.COPY) is False
assert docEditor.docAction(nwDocAction.COPY) is False
assert nwGUI.openProject(prjLipsum) is True
# Split By Chapter
assert nwGUI.openDocument("4c4f28287af27") is True
nwGUI.docEditor.setCursorPosition(57)
cleanText = nwGUI.docEditor.getText()[54:101]
docEditor.setCursorPosition(57)
cleanText = docEditor.getText()[54:101]
# Bold
nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
fmtStr = "**Pellentesque** nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:105] == fmtStr
nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[54:105] == fmtStr
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Italic
nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
fmtStr = "_Pellentesque_ nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:103] == fmtStr
nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[54:103] == fmtStr
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Strikethrough
nwGUI.mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
fmtStr = "~~Pellentesque~~ nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:105] == fmtStr
nwGUI.mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[54:105] == fmtStr
mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Should get us back to plain
nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
nwGUI.mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
nwGUI.mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Double Quotes
nwGUI.mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger)
fmtStr = "“Pellentesque” nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:103] == fmtStr
nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[54:103] == fmtStr
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Single Quotes
nwGUI.mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger)
fmtStr = "Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:103] == fmtStr
nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[54:103] == fmtStr
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Block Formats
# =============
# cSpell:ignore Pellentesque erat nulla posuere commodo
nwGUI.docEditor.setCursorPosition(57)
docEditor.setCursorPosition(57)
# Header 1
nwGUI.mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger)
fmtStr = "# Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:103] == fmtStr
assert docEditor.getText()[54:103] == fmtStr
# Header 2
nwGUI.mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger)
fmtStr = "## Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:104] == fmtStr
assert docEditor.getText()[54:104] == fmtStr
# Header 3
nwGUI.mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger)
fmtStr = "### Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:105] == fmtStr
assert docEditor.getText()[54:105] == fmtStr
# Header 4
nwGUI.mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger)
fmtStr = "#### Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:106] == fmtStr
assert docEditor.getText()[54:106] == fmtStr
# Title Format
nwGUI.mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger)
fmtStr = "#! Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:104] == fmtStr
assert docEditor.getText()[54:104] == fmtStr
# Unnumbered Chapter
nwGUI.mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger)
fmtStr = "##! Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:105] == fmtStr
assert docEditor.getText()[54:105] == fmtStr
# Hard Scene
nwGUI.mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger)
fmtStr = "###! Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:106] == fmtStr
assert docEditor.getText()[54:106] == fmtStr
# Clear Format
nwGUI.mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Comment On
nwGUI.mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
fmtStr = "% Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:103] == fmtStr
assert docEditor.getText()[54:103] == fmtStr
# Comment Off
nwGUI.mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Check comment with no space before text
nwGUI.docEditor.setCursorPosition(54)
nwGUI.docEditor.insertText("%")
docEditor.setCursorPosition(54)
docEditor.insertText("%")
fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:102] == fmtStr
assert docEditor.getText()[54:102] == fmtStr
nwGUI.mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Undo/Redo
nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
assert nwGUI.docEditor.getText()[54:102] == fmtStr
nwGUI.mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[54:102] == fmtStr
mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
# Cut, Copy and Paste
nwGUI.docEditor.setCursorPosition(54)
nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor)
docEditor.setCursorPosition(54)
docEditor._makeSelection(QTextCursor.WordUnderCursor)
nwGUI.mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:104] == (
mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
" nec erat ut nulla posuere commodo. Curabitur nisi"
)
nwGUI.mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:104] == (
mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
"Pellentesque nec erat ut nulla posuere commodo. Cu"
)
nwGUI.docEditor.setCursorPosition(54)
nwGUI.docEditor._makeSelection(QTextCursor.WordUnderCursor)
docEditor.setCursorPosition(54)
docEditor._makeSelection(QTextCursor.WordUnderCursor)
nwGUI.mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:104] == (
mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
"Pellentesque nec erat ut nulla posuere commodo. Cu"
)
nwGUI.docEditor.setCursorPosition(54)
nwGUI.mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[54:104] == (
docEditor.setCursorPosition(54)
mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
"PellentesquePellentesque nec erat ut nulla posuere"
)
nwGUI.mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
# Select Paragraph/All
nwGUI.docEditor.setCursorPosition(57)
nwGUI.mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger)
cursor = nwGUI.docEditor.textCursor()
docEditor.setCursorPosition(57)
mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger)
cursor = docEditor.textCursor()
assert cursor.selectedText() == (
"Pellentesque nec erat ut nulla posuere commodo. Curabitur nisi augue, imperdiet et porta "
"imperdiet, efficitur id leo. Cras finibus arcu at nibh commodo congue. Proin suscipit "
@@ -235,78 +237,78 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
"nunc lacus, imperdiet nec posuere ac, interdum non lectus."
)
nwGUI.docEditor.setCursorPosition(57)
nwGUI.mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
cursor = nwGUI.docEditor.textCursor()
docEditor.setCursorPosition(57)
mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
cursor = docEditor.textCursor()
assert len(cursor.selectedText()) == 1910
# Clear the Text
nwGUI.docEditor.clear()
assert nwGUI.docEditor.isEmpty
docEditor.clear()
assert docEditor.isEmpty
# Alignment & Indent
# ==================
cleanText = "A single, short paragraph.\n\n"
nwGUI.docEditor.setPlainText(cleanText)
nwGUI.docEditor.setCursorPosition(0)
docEditor.setPlainText(cleanText)
docEditor.setCursorPosition(0)
# Left Align
nwGUI.mainMenu.aFmtAlignLeft.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtAlignLeft.activate(QAction.ActionEvent.Trigger)
fmtStr = "A single, short paragraph. <<"
assert nwGUI.docEditor.getText()[:29] == fmtStr
assert docEditor.getText()[:29] == fmtStr
# Right Align
nwGUI.mainMenu.aFmtAlignRight.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtAlignRight.activate(QAction.ActionEvent.Trigger)
fmtStr = ">> A single, short paragraph."
assert nwGUI.docEditor.getText()[:29] == fmtStr
assert docEditor.getText()[:29] == fmtStr
# Centre Align
nwGUI.mainMenu.aFmtAlignCentre.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtAlignCentre.activate(QAction.ActionEvent.Trigger)
fmtStr = ">> A single, short paragraph. <<"
assert nwGUI.docEditor.getText()[:32] == fmtStr
assert docEditor.getText()[:32] == fmtStr
# Left Indent
nwGUI.mainMenu.aFmtIndentLeft.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtIndentLeft.activate(QAction.ActionEvent.Trigger)
fmtStr = "> A single, short paragraph."
assert nwGUI.docEditor.getText()[:28] == fmtStr
assert docEditor.getText()[:28] == fmtStr
# Right Indent
nwGUI.mainMenu.aFmtIndentRight.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtIndentRight.activate(QAction.ActionEvent.Trigger)
fmtStr = "> A single, short paragraph. <"
assert nwGUI.docEditor.getText()[:30] == fmtStr
assert docEditor.getText()[:30] == fmtStr
# No Format
nwGUI.mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText()[:30] == cleanText
mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[:30] == cleanText
# Other Checks
# Replace Quotes
nwGUI.docEditor.setPlainText((
docEditor.setPlainText((
"### New Text\n\n"
"Text with 'single' quotes and 'tricky stuff's'.\n\n"
"Also text with \"double\" quotes which are \"less tricky\".\n\n"
))
nwGUI.mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
nwGUI.mainMenu.aFmtReplSng.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == (
mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtReplSng.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == (
"### New Text\n\n"
"Text with single quotes and tricky stuffs.\n\n"
"Also text with \"double\" quotes which are \"less tricky\".\n\n"
)
nwGUI.mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
nwGUI.mainMenu.aFmtReplDbl.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == (
mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtReplDbl.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == (
"### New Text\n\n"
"Text with single quotes and tricky stuffs.\n\n"
"Also text with “double” quotes which are “less tricky”.\n\n"
)
# Remove in-paragraph line breaks
nwGUI.docEditor.setPlainText((
docEditor.setPlainText((
"### New Text\n\n"
"@char: Someone\n"
"@location: Somewhere\n\n"
@@ -314,8 +316,8 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
"Here is some text\non multiple\nlines.\n\n"
"With another paragraph\nhere."
))
nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == (
mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == (
"### New Text\n\n"
"@char: Someone\n"
"@location: Somewhere\n\n"
@@ -324,7 +326,7 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
"With another paragraph here.\n"
)
nwGUI.docEditor.setPlainText((
docEditor.setPlainText((
"### New Text\n\n"
"@char: Someone\n"
"@location: Somewhere\n\n"
@@ -332,12 +334,12 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
"Here is some text\non multiple\nlines.\n\n"
"With another paragraph\nhere."
))
cursor = nwGUI.docEditor.textCursor()
cursor = docEditor.textCursor()
cursor.setPosition(74)
cursor.movePosition(QtMoveRight, QtKeepAnchor, 29)
nwGUI.docEditor.setTextCursor(cursor)
nwGUI.mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == (
docEditor.setTextCursor(cursor)
mainMenu.aFmtRmBreaks.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == (
"### New Text\n\n"
"@char: Someone\n"
"@location: Somewhere\n\n"
@@ -347,10 +349,10 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
)
# Test Invalid Document Action
assert not nwGUI.docEditor.docAction(nwDocAction.NO_ACTION)
assert not docEditor.docAction(nwDocAction.NO_ACTION)
# Test Invalid Formats
nwGUI.docEditor.setPlainText((
docEditor.setPlainText((
"### New Text\n\n"
"@tag: Bod\n\n"
"Text with 'single' quotes and 'tricky stuff's'.\n\n"
@@ -358,15 +360,15 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
))
# Cannot Format Tag
nwGUI.docEditor.setCursorPosition(17)
assert not nwGUI.docEditor._formatBlock(nwDocAction.BLOCK_TXT)
docEditor.setCursorPosition(17)
assert not docEditor._formatBlock(nwDocAction.BLOCK_TXT)
# Invalid Action
nwGUI.docEditor.setCursorPosition(30)
assert not nwGUI.docEditor._formatBlock(nwDocAction.NO_ACTION)
docEditor.setCursorPosition(30)
assert not docEditor._formatBlock(nwDocAction.NO_ACTION)
# Ensure No Changes
assert nwGUI.docEditor.getText() == (
assert docEditor.getText() == (
"### New Text\n\n"
"@tag: Bod\n\n"
"Text with 'single' quotes and 'tricky stuff's'.\n\n"
@@ -383,198 +385,175 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd
assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None
assert nwGUI.openDocument(C.hSceneDoc) is True
nwGUI.docEditor.clear()
mainMenu = nwGUI.mainMenu
docEditor = nwGUI.docEditor
docEditor.clear()
# Test Faulty Inserts
nwGUI.docEditor.insertText("hello world")
assert nwGUI.docEditor.getText() == "hello world"
nwGUI.docEditor.clear()
docEditor.insertText("hello world")
assert docEditor.getText() == "hello world"
docEditor.clear()
nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT)
assert nwGUI.docEditor.isEmpty
docEditor.insertText(nwDocInsert.NO_INSERT)
assert docEditor.isEmpty
nwGUI.docEditor.insertText(None)
assert nwGUI.docEditor.isEmpty
docEditor.insertText(None)
assert docEditor.isEmpty
# qtbot.stop()
nwGUI.docEditor.clear()
docEditor.clear()
# Check Menu Entries
nwGUI.mainMenu.aInsENDash.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_ENDASH
nwGUI.docEditor.clear()
mainMenu.aInsENDash.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_ENDASH
docEditor.clear()
nwGUI.mainMenu.aInsEMDash.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_EMDASH
nwGUI.docEditor.clear()
mainMenu.aInsEMDash.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_EMDASH
docEditor.clear()
nwGUI.mainMenu.aInsHorBar.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_HBAR
nwGUI.docEditor.clear()
mainMenu.aInsHorBar.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_HBAR
docEditor.clear()
nwGUI.mainMenu.aInsFigDash.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_FGDASH
nwGUI.docEditor.clear()
mainMenu.aInsFigDash.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_FGDASH
docEditor.clear()
nwGUI.mainMenu.aInsQuoteLS.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == CONFIG.fmtSQuoteOpen
nwGUI.docEditor.clear()
mainMenu.aInsQuoteLS.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == CONFIG.fmtSQuoteOpen
docEditor.clear()
nwGUI.mainMenu.aInsQuoteRS.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == CONFIG.fmtSQuoteClose
nwGUI.docEditor.clear()
mainMenu.aInsQuoteRS.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == CONFIG.fmtSQuoteClose
docEditor.clear()
nwGUI.mainMenu.aInsQuoteLD.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == CONFIG.fmtDQuoteOpen
nwGUI.docEditor.clear()
mainMenu.aInsQuoteLD.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == CONFIG.fmtDQuoteOpen
docEditor.clear()
nwGUI.mainMenu.aInsQuoteRD.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == CONFIG.fmtDQuoteClose
nwGUI.docEditor.clear()
mainMenu.aInsQuoteRD.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == CONFIG.fmtDQuoteClose
docEditor.clear()
nwGUI.mainMenu.aInsMSApos.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_MAPOS
nwGUI.docEditor.clear()
mainMenu.aInsMSApos.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_MAPOS
docEditor.clear()
nwGUI.mainMenu.aInsEllipsis.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_HELLIP
nwGUI.docEditor.clear()
mainMenu.aInsEllipsis.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_HELLIP
docEditor.clear()
nwGUI.mainMenu.aInsPrime.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_PRIME
nwGUI.docEditor.clear()
mainMenu.aInsPrime.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_PRIME
docEditor.clear()
nwGUI.mainMenu.aInsDPrime.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_DPRIME
nwGUI.docEditor.clear()
mainMenu.aInsDPrime.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_DPRIME
docEditor.clear()
nwGUI.mainMenu.aInsBullet.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_BULL
nwGUI.docEditor.clear()
mainMenu.aInsBullet.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_BULL
docEditor.clear()
nwGUI.mainMenu.aInsHyBull.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_HYBULL
nwGUI.docEditor.clear()
mainMenu.aInsHyBull.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_HYBULL
docEditor.clear()
nwGUI.mainMenu.aInsFlower.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_FLOWER
nwGUI.docEditor.clear()
mainMenu.aInsFlower.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_FLOWER
docEditor.clear()
nwGUI.mainMenu.aInsPerMille.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_PERMIL
nwGUI.docEditor.clear()
mainMenu.aInsPerMille.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_PERMIL
docEditor.clear()
nwGUI.mainMenu.aInsDegree.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_DEGREE
nwGUI.docEditor.clear()
mainMenu.aInsDegree.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_DEGREE
docEditor.clear()
nwGUI.mainMenu.aInsMinus.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_MINUS
nwGUI.docEditor.clear()
mainMenu.aInsMinus.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_MINUS
docEditor.clear()
nwGUI.mainMenu.aInsTimes.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_TIMES
nwGUI.docEditor.clear()
mainMenu.aInsTimes.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_TIMES
docEditor.clear()
nwGUI.mainMenu.aInsDivide.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_DIVIDE
nwGUI.docEditor.clear()
mainMenu.aInsDivide.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_DIVIDE
docEditor.clear()
nwGUI.mainMenu.aInsNBSpace.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_NBSP
nwGUI.docEditor.clear()
mainMenu.aInsNBSpace.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_NBSP
docEditor.clear()
nwGUI.mainMenu.aInsThinSpace.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_THSP
nwGUI.docEditor.clear()
mainMenu.aInsThinSpace.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_THSP
docEditor.clear()
nwGUI.mainMenu.aInsThinNBSpace.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == nwUnicode.U_THNBSP
nwGUI.docEditor.clear()
mainMenu.aInsThinNBSpace.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == nwUnicode.U_THNBSP
docEditor.clear()
# Insert Keywords
# ===============
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.TAG_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.TAG_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.POV_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.POV_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.FOCUS_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.FOCUS_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.CHAR_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.CHAR_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.PLOT_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.PLOT_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.TIME_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.TIME_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.WORLD_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.WORLD_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.OBJECT_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.OBJECT_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.ENTITY_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.ENTITY_KEY
nwGUI.docEditor.setPlainText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.CUSTOM_KEY][0].activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.CUSTOM_KEY
for action, key in zip(mainMenu.mInsKeywords.actions(), nwKeyWords.ALL_KEYS):
docEditor.setPlainText("Stuff")
action.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == f"Stuff\n{key}: "
# Faulty Keyword Inserts
assert not nwGUI.docEditor.insertKeyWord("blabla")
assert not docEditor.insertKeyWord("blabla")
with monkeypatch.context() as mp:
mp.setattr(QTextBlock, "isValid", lambda *a, **k: False)
assert not nwGUI.docEditor.insertKeyWord(nwKeyWords.TAG_KEY)
assert not docEditor.insertKeyWord(nwKeyWords.TAG_KEY)
nwGUI.docEditor.clear()
# Insert Fields
# =============
for action, field in zip(mainMenu.mInsField.actions(), nwStats.ALL_FIELDS):
value = nwShortcode.FIELD_VALUE.format(field)
docEditor.setPlainText("Stuff ")
docEditor.setCursorPosition(6)
action.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == f"Stuff {value}"
docEditor.clear()
# Insert Special Comments
# =======================
nwGUI.docEditor.setPlainText("Stuff\n")
nwGUI.mainMenu.aInsSynopsis.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%Synopsis: \n"
docEditor.setPlainText("Stuff\n")
mainMenu.aInsSynopsis.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == "Stuff\n%Synopsis: \n"
nwGUI.docEditor.setPlainText("Stuff\n")
nwGUI.mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%Short: \n"
docEditor.setPlainText("Stuff\n")
mainMenu.aInsShort.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == "Stuff\n%Short: \n"
# Breaks and Vertical Space
# =========================
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[newpage]\n### Stuff\n"
docEditor.setPlainText("### Stuff\n")
mainMenu.aInsNewPage.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == "[newpage]\n### Stuff\n"
nwGUI.docEditor.setPlainText("Line OneLine Two\n")
nwGUI.docEditor.setCursorPosition(8)
nwGUI.mainMenu.aInsLineBreak.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Line One[br]Line Two\n"
docEditor.setPlainText("Line OneLine Two\n")
docEditor.setCursorPosition(8)
mainMenu.aInsLineBreak.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == "Line One[br]Line Two\n"
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[vspace]\n### Stuff\n"
docEditor.setPlainText("### Stuff\n")
mainMenu.aInsVSpaceS.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == "[vspace]\n### Stuff\n"
nwGUI.docEditor.setPlainText("### Stuff\n")
nwGUI.mainMenu.aInsVSpaceM.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "[vspace:2]\n### Stuff\n"
docEditor.setPlainText("### Stuff\n")
mainMenu.aInsVSpaceM.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == "[vspace:2]\n### Stuff\n"
nwGUI.docEditor.clear()
docEditor.clear()
# Insert Text from File
# =====================
@@ -600,23 +579,23 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd
# Open the document from before, and add some text to it
nwGUI.openDocument(C.hSceneDoc)
nwGUI.docEditor.setPlainText("Bar")
assert nwGUI.docEditor.getText() == "Bar"
docEditor.setPlainText("Bar")
assert docEditor.getText() == "Bar"
# The document isn't empty, so the message box should pop
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.StandardButton.No)
assert not nwGUI.importDocument()
assert nwGUI.docEditor.getText() == "Bar"
assert docEditor.getText() == "Bar"
# Finally, accept the replaced text, this time we use the menu entry to trigger it
nwGUI.mainMenu.aImportFile.activate(QAction.ActionEvent.Trigger)
assert nwGUI.docEditor.getText() == "Foo"
mainMenu.aImportFile.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText() == "Foo"
# Reveal File Location
# ====================
nwGUI.mainMenu.aFileDetails.activate(QAction.ActionEvent.Trigger)
mainMenu.aFileDetails.activate(QAction.ActionEvent.Trigger)
path = str(projPath / "content" / "000000000000f.nwd")
assert SHARED.lastAlert.endswith(f"File Location: {path}")
+3 -3
View File
@@ -241,9 +241,9 @@ def testGuiOutline_Content(qtbot, monkeypatch, nwGUI, prjLipsum, fncPath, tstPat
assert outlineData.fileValue.text() == "Lorem Ipsum"
assert outlineData.itemValue.text() == "Finished"
assert outlineData.cCValue.text() == "230"
assert outlineData.wCValue.text() == "40"
assert outlineData.pCValue.text() == "3"
assert outlineData.cCValue.text() == "259"
assert outlineData.wCValue.text() == "44"
assert outlineData.pCValue.text() == "5"
# Scene One
selItem = outlineTree.topLevelItem(4)
+6 -6
View File
@@ -60,15 +60,15 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText):
# Check project data
assert overview.projName.text() == "Lorem Ipsum"
assert overview.projWords.text() == f"{4378:n}"
assert overview.projNovels.text() == f"{3640:n}"
assert overview.projWords.text() == f"{4382:n}"
assert overview.projNovels.text() == f"{3644:n}"
assert overview.projNotes.text() == f"{738:n}"
assert overview.projRevisions.text() != ""
assert overview.projEditTime.text() != ""
# Check novel data for "Novel"
assert overview.novelName.text() == "Novel"
assert overview.novelWords.text() == f"{3002:n}"
assert overview.novelWords.text() == f"{3006:n}"
assert overview.novelChapters.text() == f"{3:n}"
assert overview.novelScenes.text() == f"{5:n}"
@@ -87,7 +87,7 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText):
contents = details.contentsPage
# Check defaults
words = [f"{v:n}" for v in [40, 176, 94, 6, 1071, 1615, 0]]
words = [f"{v:n}" for v in [44, 176, 94, 6, 1071, 1615, 0]]
pages = [f"{v:n}" for v in [2, 2, 2, 2, 4, 6, 0]]
page = [f"{v:n}" for v in [1, 3, 5, 7, 9, 13, 19]]
for i in range(6):
@@ -100,7 +100,7 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText):
# Change Settings
contents.poValue.setValue(7)
contents.wpValue.setValue(50)
words = [f"{v:n}" for v in [40, 176, 94, 6, 1071, 1615, 0]]
words = [f"{v:n}" for v in [44, 176, 94, 6, 1071, 1615, 0]]
pages = [f"{v:n}" for v in [2, 4, 2, 2, 22, 34, 0]]
page = ["i", "iii"] + [f"{v:n}" for v in [1, 3, 5, 27, 61]]
for i in range(6):
@@ -114,7 +114,7 @@ def testToolNovelDetails_Main(qtbot, nwGUI, prjLipsum, ipsumText):
contents.dblValue.setChecked(False)
contents.poValue.setValue(0)
contents.wpValue.setValue(100)
words = [f"{v:n}" for v in [40, 176, 94, 6, 1071, 1615, 0]]
words = [f"{v:n}" for v in [44, 176, 94, 6, 1071, 1615, 0]]
pages = [f"{v:n}" for v in [1, 2, 1, 1, 11, 17, 0]]
page = [f"{v:n}" for v in [1, 2, 4, 5, 6, 17, 34]]
for i in range(6):
+10
View File
@@ -21,6 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
import shutil
import xml.etree.ElementTree as ET
from datetime import datetime
from pathlib import Path
@@ -110,6 +111,15 @@ def cmpFiles(
return not diffFound
def xmlToText(xElem):
"""Get the text content of an XML element."""
text = ET.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
bits = text.partition(">")
node = bits[0].partition(" ")
rest = " ".join(x for x in node[2].split() if not x.startswith("xmlns")).replace("/", " /")
return f"{node[0]}{rest}{bits[1]}{bits[2]}"
def readFile(fileName: str | Path):
"""Returns the content of a file as a string."""
with open(fileName, mode="r", encoding="utf-8") as inFile: