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