Merge main and 2.7.4

This commit is contained in:
Veronica Berglyd Olsen
2025-07-15 18:53:10 +02:00
190 changed files with 7781 additions and 2746 deletions
+27 -2
View File
@@ -34,11 +34,14 @@ from PyQt6.QtWidgets import QMessageBox
sys.path.insert(1, str(Path(__file__).parent.parent.absolute()))
from novelwriter import CONFIG, SHARED
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT
from novelwriter.enum import nwTheme
from tests.mocked import MockGuiMain, MockTheme
from tests.mocked import MockGuiMain
from tests.tools import cleanProject
_TST_ROOT = Path(__file__).parent
_SRC_ROOT = _TST_ROOT.parent
_TMP_ROOT = _TST_ROOT / "temp"
_TMP_CONF = _TMP_ROOT / "conf"
@@ -59,6 +62,14 @@ def resetConfigVars():
CONFIG._dLocale = QLocale("en_GB")
CONFIG._manuals = {"manual": _TMP_ROOT / "manual.pdf"}
CONFIG.guiLocale = "en_GB"
CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.lightTheme = DEF_GUI_LIGHT
CONFIG.themeMode = nwTheme.LIGHT
# Enable a few settings to ensure better coverage
CONFIG.emphLabels = True
CONFIG.lineHighlight = True
return
@@ -75,6 +86,8 @@ def sessionFixture():
_TMP_ROOT.mkdir()
_TMP_CONF.mkdir()
(_TMP_ROOT / "manual.pdf").touch()
(_SRC_ROOT / "novelwriter" / "assets"/ "manual.pdf").touch()
(_SRC_ROOT / "novelwriter" / "assets"/ "manual_fr.pdf").touch()
return
@@ -148,15 +161,27 @@ def projPath(fncPath):
@pytest.fixture(scope="function")
def mockGUI(qtbot, monkeypatch):
"""Create a mock instance of novelWriter's main GUI class."""
from novelwriter.gui.theme import GuiTheme
monkeypatch.setattr(QMessageBox, "exec", lambda *a: None)
monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes)
gui = MockGuiMain()
theme = MockTheme()
theme = GuiTheme()
monkeypatch.setattr(SHARED, "_gui", gui)
monkeypatch.setattr(SHARED, "_theme", theme)
return gui
@pytest.fixture(scope="function")
def mockGUIwithTheme(mockGUI):
"""Create a mock instance of novelWriter's main GUI class with the
theme instance initialised.
"""
SHARED.theme.initThemes()
return mockGUI
@pytest.fixture(scope="function")
def nwGUI(qtbot, monkeypatch, functionFixture):
"""Create an instance of the novelWriter GUI."""
+114
View File
@@ -0,0 +1,114 @@
[
"alert_error",
"alert_info",
"alert_question",
"alert_warn",
"cls_archive",
"cls_character",
"cls_custom",
"cls_entity",
"cls_none",
"cls_novel",
"cls_object",
"cls_plot",
"cls_template",
"cls_timeline",
"cls_trash",
"cls_world",
"prj_folder",
"prj_document",
"prj_title",
"prj_chapter",
"prj_scene",
"prj_note",
"fmt_bold",
"fmt_italic",
"fmt_mark",
"fmt_strike",
"fmt_subscript",
"fmt_superscript",
"fmt_underline",
"fmt_toolbar",
"search",
"search_cancel",
"search_case",
"search_loop",
"search_preserve",
"search_project",
"search_regex",
"search_replace",
"search_word",
"bullet-off",
"bullet-on",
"unfold-hide",
"unfold-show",
"sb_build",
"sb_details",
"sb_novel",
"sb_outline",
"sb_project",
"sb_search",
"sb_stats",
"theme_light",
"theme_dark",
"theme_auto",
"add",
"bookmarks",
"browse",
"build_settings",
"cancel",
"checked",
"chevron_down",
"chevron_left",
"chevron_right",
"chevron_up",
"close",
"copy",
"document_add",
"document",
"edit",
"exclude",
"export",
"filter",
"fit_height",
"fit_width",
"folder",
"font",
"import",
"language",
"lines",
"list",
"margin_bottom",
"margin_left",
"margin_right",
"margin_top",
"maximise",
"minimise",
"more_arrow",
"more_vertical",
"noncheckable",
"open",
"panel",
"pin",
"project_copy",
"quote",
"refresh",
"remove",
"revert",
"settings",
"star",
"stats",
"text",
"timer_off",
"timer",
"unchecked",
"view"
]
+13 -13
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:07:59">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="6" timeStamp="2025-04-29 22:07:59">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="2179" autoCount="285" editTime="1000">
<name>Sample Project</name>
<author>Jane Smith</author>
@@ -20,20 +20,20 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="sf12341" count="8" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="sf24ce6" count="2" red="200" green="50" blue="0" shape="SQUARE">Notes</entry>
<entry key="sc24b8f" count="3" red="182" green="60" blue="0" shape="BARS_1">Started</entry>
<entry key="s90e6c9" count="5" red="193" green="129" blue="0" shape="BARS_2">1st Draft</entry>
<entry key="sd51c5b" count="1" red="193" green="129" blue="0" shape="BARS_3">2nd Draft</entry>
<entry key="s8ae72a" count="1" red="193" green="129" blue="0" shape="BARS_4">3rd Draft</entry>
<entry key="s78ea90" count="1" red="58" green="180" blue="58" shape="STAR">Finished</entry>
<entry key="sf12341" count="8" color="#646464" shape="SQUARE">New</entry>
<entry key="sf24ce6" count="2" color="#c83200" shape="SQUARE">Notes</entry>
<entry key="sc24b8f" count="3" color="#b63c00" shape="BARS_1">Started</entry>
<entry key="s90e6c9" count="5" color="#c18100" shape="BARS_2">1st Draft</entry>
<entry key="sd51c5b" count="1" color="#c18100" shape="BARS_3">2nd Draft</entry>
<entry key="s8ae72a" count="1" color="#c18100" shape="BARS_4">3rd Draft</entry>
<entry key="s78ea90" count="1" color="#3ab43a" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="ia857f0" count="5" red="100" green="100" blue="100" shape="SQUARE">None</entry>
<entry key="i4a1d39" count="1" red="220" green="138" blue="221" shape="BLOCK_1">Background</entry>
<entry key="icfb3a5" count="1" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i2d7a54" count="2" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i56be10" count="1" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="ia857f0" count="5" color="#646464" shape="SQUARE">None</entry>
<entry key="i4a1d39" count="1" color="#dc8add" shape="BLOCK_1">Background</entry>
<entry key="icfb3a5" count="1" color="#dc8add" shape="BLOCK_2">Minor</entry>
<entry key="i2d7a54" count="2" color="#dc8add" shape="BLOCK_3">Major</entry>
<entry key="i56be10" count="1" color="#dc8add" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="31" novelWords="1016" notesWords="416" novelChars="5602" notesChars="2285">
+4 -2
View File
@@ -1,12 +1,14 @@
%%~name: Ancient Europe
%%~path: 60bdf227455cc/04468803b92e1
%%~kind: WORLD/NOTE
%%~hash: 7b746485a1d06c64a5e0bf671e3141806ed51a54
%%~date: Unknown/2024-01-30 21:36:44
%%~hash: 9a0a98cda777f557ea10670ea1618cde253084b7
%%~date: Unknown/2025-06-19 21:02:43
# Ancient Europe
@tag: Europe | Ancient Europe
%Short: A strange place
Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.
Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.
+4 -2
View File
@@ -1,12 +1,14 @@
%%~name: Main
%%~path: 6c6afb1247750/2426c6f0ca922
%%~kind: PLOT/NOTE
%%~hash: db3897d166e246acdb5e25e9bd98c5a40a699ed0
%%~date: Unknown/Unknown
%%~hash: ffc82e98d37621d83a808a75f06bd36923356706
%%~date: Unknown/2025-06-19 21:02:33
# Main Plot
@tag: Main
%Short: Where it all happens
Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.
Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.
+3 -3
View File
@@ -1,14 +1,14 @@
%%~name: Chapter Two
%%~path: 6bd935d2490cd/441420a886d82
%%~kind: NOVEL/DOCUMENT
%%~hash: fd6d46708faa1333f8f7ba0442fc5b35bf1e3f85
%%~date: Unknown/Unknown
%%~hash: 81d30d3448202c782a6e252c5f7ef030bca3bf7f
%%~date: Unknown/2025-06-19 21:01:48
## Chapter Two
@pov: Bod
@plot: Main
@location: Europe
% Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.
%Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.
Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at.
+3 -3
View File
@@ -1,15 +1,15 @@
%%~name: Scene Five
%%~path: 6bd935d2490cd/47666c91c7ccf
%%~kind: NOVEL/DOCUMENT
%%~hash: d210c26966da6f9edea8726567abf4860b7bb9b7
%%~date: Unknown/Unknown
%%~hash: 680e26046056165f1560ed1468cf316a3c51f90e
%%~date: Unknown/2025-06-19 21:01:59
### Scene Five
@pov: Bod
@plot: Main
@location: Europe
% Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
%Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet.
+4 -2
View File
@@ -1,13 +1,15 @@
%%~name: Mr. Nobody
%%~path: 67a8707f2f249/4c4f28287af27
%%~kind: CHARACTER/NOTE
%%~hash: 2a9b9751e8207b719bfa6233ebe04e24928ff8a8
%%~date: Unknown/2024-01-30 21:36:26
%%~hash: 84639e78d265b3a1f2fb8e3211a08c223d95500a
%%~date: Unknown/2025-06-19 21:02:21
# Nobody Owens
@tag: Bod | Nobody Owens
@plot: Main
%Short: The Hero
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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.
Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.
+3 -3
View File
@@ -1,15 +1,15 @@
%%~name: Scene One
%%~path: 45e6b01ca35c1/88243afbe5ed8
%%~kind: NOVEL/DOCUMENT
%%~hash: a09245a7a772bbe02850b5db109977e336cd9cc1
%%~date: Unknown/Unknown
%%~hash: d660202d320b3c99dbcc3bd9dd39d464d4f422ae
%%~date: Unknown/2025-06-19 21:01:38
### Scene One
@pov: Bod
@plot: Main
@location: Europe
% Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.
%Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.
Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada.
+4 -3
View File
@@ -1,11 +1,12 @@
%%~name: Prologue
%%~path: b3643d0f92e32/88d59a277361b
%%~kind: NOVEL/DOCUMENT
%%~hash: 377a72ba340ff458a00d98af745e9fa0c79fb26c
%%~date: Unknown/2024-10-25 19:16:49
%%~hash: d43324a30d3f1e5608711ca71515dee429b30925
%%~date: Unknown/2025-06-19 21:01:07
##! Prologue
% Synopsis: Explanation from the lipsum.com website.
%Synopsis: Explanation from the lipsum.com website.
%Note.Purpose: Introduction
_Lorem Ipsum_ is simply dummy text[footnote:f9kgf] of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
+3 -3
View File
@@ -1,15 +1,15 @@
%%~name: Scene Three
%%~path: 6bd935d2490cd/eb103bc70c90c
%%~kind: NOVEL/DOCUMENT
%%~hash: c4eda49e4fe81dc450d547eee0bdabe77fdaaa98
%%~date: Unknown/Unknown
%%~hash: 59eeb405f5b26cbe1056d1f7684f7ed40ed6685a
%%~date: Unknown/2025-06-19 21:01:52
### Scene Three
@pov: Bod
@plot: Main
@location: Europe
% Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
%Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis.
+3 -3
View File
@@ -1,15 +1,15 @@
%%~name: Scene Four
%%~path: 6bd935d2490cd/f8c0562e50f1b
%%~kind: NOVEL/DOCUMENT
%%~hash: 9461a279b9fb6ef005ee4d432fcda77ff5bfbd42
%%~date: Unknown/Unknown
%%~hash: f4848b56d1f41a04de1579d772a4d1ddd3e28e55
%%~date: Unknown/2025-06-19 21:01:55
### Scene Four
@pov: Bod
@plot: Main
@location: Europe
% Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.
%Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.
Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien.
+3 -3
View File
@@ -1,15 +1,15 @@
%%~name: Scene Two
%%~path: 45e6b01ca35c1/f96ec11c6a3da
%%~kind: NOVEL/DOCUMENT
%%~hash: ebe3fbaa16d9d81bc1a139822e3bf39bb357866d
%%~date: Unknown/Unknown
%%~hash: e95ff5b633e74c9cab8dc32b6a77b3e40d265f2b
%%~date: Unknown/2025-06-19 21:01:41
### Scene Two
@pov: Bod
@plot: Main
@location: Europe
% Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.
%Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien.
+3 -3
View File
@@ -1,14 +1,14 @@
%%~name: Chapter One
%%~path: 45e6b01ca35c1/fb609cd8319dc
%%~kind: NOVEL/DOCUMENT
%%~hash: 5dabeaa7a58238a6ad99ce73176b1bdfad1a71b7
%%~date: Unknown/2024-01-30 21:35:57
%%~hash: 73a1008ed17a84a48b14b41109a842a6ed300eec
%%~date: Unknown/2025-06-19 21:01:19
## Chapter One
@pov: Bod
@plot: Main
@location: Europe
% Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.
%Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar.
+24 -24
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:45:09">
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="52" autoCount="29" editTime="2465">
<novelWriterXML appVersion="2.8a1" hexVersion="0x020800a1" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-19 21:05:02">
<project id="5ac0df12-8b8b-476b-9905-21d33685687c" saveCount="55" autoCount="33" editTime="2746">
<name>Lorem Ipsum</name>
<author>lipsum.com</author>
</project>
@@ -9,7 +9,7 @@
<language>en_GB</language>
<spellChecking auto="no">None</spellChecking>
<lastHandle>
<entry key="editor">7a992350f3eb6</entry>
<entry key="editor">04468803b92e1</entry>
<entry key="viewer">None</entry>
<entry key="novel">b3643d0f92e32</entry>
<entry key="outline">None</entry>
@@ -19,16 +19,16 @@
<entry key="Rep2">Replace Text 2</entry>
</autoReplace>
<status>
<entry key="sbaa94f" count="3" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="s27bf7c" count="0" red="200" green="50" blue="0" shape="SQUARE">Note</entry>
<entry key="s92a87b" count="5" red="200" green="150" blue="0" shape="SQUARE">Draft</entry>
<entry key="sedd043" count="7" red="50" green="200" blue="0" shape="SQUARE">Finished</entry>
<entry key="sbaa94f" count="3" color="base" shape="CIRCLE">New</entry>
<entry key="s27bf7c" count="0" color="red" shape="CIRCLE">Note</entry>
<entry key="s92a87b" count="5" color="yellow" shape="CIRCLE">Draft</entry>
<entry key="sedd043" count="7" color="green" shape="CIRCLE">Finished</entry>
</status>
<importance>
<entry key="i613591" count="7" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="i560cbf" count="0" red="200" green="50" blue="0" shape="SQUARE">Minor</entry>
<entry key="i37861c" count="0" red="200" green="150" blue="0" shape="SQUARE">Major</entry>
<entry key="id6b1d0" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
<entry key="i613591" count="4" color="base" shape="SQUARE">New</entry>
<entry key="i560cbf" count="0" color="red" shape="SQUARE">Minor</entry>
<entry key="i37861c" count="1" color="yellow" shape="SQUARE">Major</entry>
<entry key="id6b1d0" count="2" color="green" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="22" novelWords="3115" notesWords="738" novelChars="20774" notesChars="5003">
@@ -45,7 +45,7 @@
<name status="sedd043" import="i613591" active="yes">Front Matter</name>
</item>
<item handle="88d59a277361b" parent="b3643d0f92e32" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="605" wordCount="94" paraCount="2" cursorPos="47" />
<meta expanded="no" heading="H2" charCount="605" wordCount="94" paraCount="2" cursorPos="94" />
<name status="s92a87b" import="i613591" active="yes">Prologue</name>
</item>
<item handle="db7e733775d4d" parent="b3643d0f92e32" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -57,7 +57,7 @@
<name status="s92a87b" import="i613591">Chapter One</name>
</item>
<item handle="fb609cd8319dc" parent="45e6b01ca35c1" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="419" wordCount="67" paraCount="1" cursorPos="59" />
<meta expanded="no" heading="H2" charCount="419" wordCount="67" paraCount="1" cursorPos="155" />
<name status="s92a87b" import="i613591" active="yes">Chapter One</name>
</item>
<item handle="88243afbe5ed8" parent="45e6b01ca35c1" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -65,7 +65,7 @@
<name status="sedd043" import="i613591" active="yes">Scene One</name>
</item>
<item handle="f96ec11c6a3da" parent="45e6b01ca35c1" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="4043" wordCount="600" paraCount="6" cursorPos="2335" />
<meta expanded="no" heading="H3" charCount="4043" wordCount="600" paraCount="6" cursorPos="57" />
<name status="sedd043" import="i613591" active="yes">Scene Two</name>
</item>
<item handle="846352075de7d" parent="b3643d0f92e32" root="b3643d0f92e32" order="5" type="FILE" class="NOVEL" layout="DOCUMENT">
@@ -77,19 +77,19 @@
<name status="s92a87b" import="i613591">Chapter Two</name>
</item>
<item handle="441420a886d82" parent="6bd935d2490cd" root="b3643d0f92e32" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="477" wordCount="70" paraCount="1" cursorPos="56" />
<meta expanded="no" heading="H2" charCount="477" wordCount="70" paraCount="1" cursorPos="58" />
<name status="s92a87b" import="i613591" active="yes">Chapter Two</name>
</item>
<item handle="eb103bc70c90c" parent="6bd935d2490cd" root="b3643d0f92e32" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="3006" wordCount="439" paraCount="4" cursorPos="57" />
<meta expanded="no" heading="H3" charCount="3006" wordCount="439" paraCount="4" cursorPos="59" />
<name status="sedd043" import="i613591" active="yes">Scene Three</name>
</item>
<item handle="f8c0562e50f1b" parent="6bd935d2490cd" root="b3643d0f92e32" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="3839" wordCount="563" paraCount="6" cursorPos="56" />
<meta expanded="no" heading="H3" charCount="3839" wordCount="563" paraCount="6" cursorPos="58" />
<name status="sedd043" import="i613591" active="yes">Scene Four</name>
</item>
<item handle="47666c91c7ccf" parent="6bd935d2490cd" root="b3643d0f92e32" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="3644" wordCount="543" paraCount="5" cursorPos="351" />
<meta expanded="no" heading="H3" charCount="3644" wordCount="543" paraCount="5" cursorPos="58" />
<name status="sedd043" import="i613591" active="yes">Scene Five</name>
</item>
<item handle="67a8707f2f249" parent="None" root="67a8707f2f249" order="1" type="ROOT" class="CHARACTER">
@@ -97,24 +97,24 @@
<name status="sbaa94f" import="i613591">Characters</name>
</item>
<item handle="4c4f28287af27" parent="67a8707f2f249" root="67a8707f2f249" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="1864" wordCount="284" paraCount="3" cursorPos="40" />
<name status="sbaa94f" import="i613591" active="yes">Mr. Nobody</name>
<meta expanded="no" heading="H1" charCount="1864" wordCount="284" paraCount="3" cursorPos="70" />
<name status="sbaa94f" import="id6b1d0" active="yes">Mr. Nobody</name>
</item>
<item handle="6c6afb1247750" parent="None" root="6c6afb1247750" order="2" type="ROOT" class="PLOT">
<meta expanded="yes" />
<name status="sbaa94f" import="i613591">Plot</name>
</item>
<item handle="2426c6f0ca922" parent="6c6afb1247750" root="6c6afb1247750" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta expanded="no" heading="H1" charCount="1369" wordCount="195" paraCount="2" cursorPos="1387" />
<name status="sbaa94f" import="i613591" active="yes">Main</name>
<meta expanded="no" heading="H1" charCount="1369" wordCount="195" paraCount="2" cursorPos="53" />
<name status="sbaa94f" import="id6b1d0" active="yes">Main</name>
</item>
<item handle="60bdf227455cc" parent="None" root="60bdf227455cc" order="3" type="ROOT" class="WORLD">
<meta expanded="yes" />
<name status="sbaa94f" import="i613591">World</name>
</item>
<item handle="04468803b92e1" parent="60bdf227455cc" root="60bdf227455cc" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="no" heading="H1" charCount="1770" wordCount="259" paraCount="3" cursorPos="47" />
<name status="sbaa94f" import="i613591" active="yes">Ancient Europe</name>
<meta expanded="no" heading="H1" charCount="1770" wordCount="259" paraCount="3" cursorPos="1119" />
<name status="sbaa94f" import="i37861c" active="yes">Ancient Europe</name>
</item>
<item handle="1ace7ab1a0fc6" parent="None" root="1ace7ab1a0fc6" order="4" type="ROOT" class="TRASH">
<meta expanded="no" />
+3
View File
@@ -65,6 +65,9 @@ class MockTheme:
self.guiFontBU = QFont()
return
def initThemes(self) -> None:
return
def getPixmap(self, *a) -> QPixmap:
return QPixmap()
+10 -6
View File
@@ -1,10 +1,11 @@
[Meta]
timestamp = 2025-05-11 13:28:11
timestamp = 2025-06-14 17:03:01
[Main]
font =
theme = default
syntax = default_light
lighttheme = default_light
darktheme = default_dark
thememode = AUTO
icons = material_rounded_normal
iconcoltree = theme
iconcoldocs = False
@@ -26,9 +27,9 @@ outlinepane = 500, 150
[Project]
autosaveproject = 60
autosavedoc = 30
emphlabels = True
emphlabels = False
backuppath =
backuponclose = False
backuponclose = True
askbeforebackup = True
askbeforeexit = True
lastauthor =
@@ -39,6 +40,7 @@ width = 700
margin = 40
tabwidth = 40
cursorwidth = 1
linehighlight = False
focuswidth = 800
hidefocusfooter = False
justify = False
@@ -48,7 +50,7 @@ repsquotes = True
repdquotes = True
repdash = True
repdots = True
autoscroll = False
autoscroll = True
autoscrollpos = 30
scrollpastend = True
fmtsquoteopen =
@@ -81,6 +83,7 @@ showedittoolbar = False
showsessiontime = True
viewcomments = True
viewsynopsis = True
viewnotes = True
searchcase = False
searchword = False
searchregex = False
@@ -90,3 +93,4 @@ searchmatchcap = False
searchprojcase = False
searchprojword = False
searchprojregex = False
@@ -18,7 +18,8 @@
"88d59a277361b": {
"T0001": {
"meta": {"level": "H2", "title": "Prologue", "line": 1, "tag": "", "counts": [605, 94, 2]},
"summary": "Explanation from the lipsum.com website."
"summary": "Explanation from the lipsum.com website.",
"note.Purpose": "Introduction"
},
"document": {
"footnotes": ["f9kgf"]
@@ -92,17 +93,20 @@
"4c4f28287af27": {
"T0001": {
"meta": {"level": "H1", "title": "Nobody Owens", "line": 1, "tag": "bod", "counts": [1864, 284, 3]},
"refs": {"main": "@plot"}
"refs": {"main": "@plot"},
"summary": "The Hero"
}
},
"2426c6f0ca922": {
"T0001": {
"meta": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "main", "counts": [1369, 195, 2]}
"meta": {"level": "H1", "title": "Main Plot", "line": 1, "tag": "main", "counts": [1369, 195, 2]},
"summary": "Where it all happens"
}
},
"04468803b92e1": {
"T0001": {
"meta": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "europe", "counts": [1770, 259, 3]}
"meta": {"level": "H1", "title": "Ancient Europe", "line": 1, "tag": "europe", "counts": [1770, 259, 3]},
"summary": "A strange place"
}
}
}
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:54">
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 18:02:57">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -16,16 +16,16 @@
</lastHandle>
<autoReplace />
<status>
<entry key="s000000" count="7" red="120" green="120" blue="120" shape="STAR">New</entry>
<entry key="s000001" count="0" red="205" green="171" blue="143" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" red="143" green="240" blue="164" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" red="249" green="240" blue="107" shape="STAR">Finished</entry>
<entry key="s000000" count="7" color="faded" shape="STAR">New</entry>
<entry key="s000001" count="0" color="red" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" color="yellow" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" color="green" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="i000004" count="5" red="120" green="120" blue="120" shape="SQUARE">New</entry>
<entry key="i000005" count="0" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="i000004" count="5" color="purple" shape="SQUARE">New</entry>
<entry key="i000005" count="0" color="purple" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" color="purple" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="12" novelWords="10" notesWords="6" novelChars="45" notesChars="22">
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:54">
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 18:02:57">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -16,16 +16,16 @@
</lastHandle>
<autoReplace />
<status>
<entry key="s000000" count="6" red="120" green="120" blue="120" shape="STAR">New</entry>
<entry key="s000001" count="0" red="205" green="171" blue="143" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" red="143" green="240" blue="164" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" red="249" green="240" blue="107" shape="STAR">Finished</entry>
<entry key="s000000" count="6" color="faded" shape="STAR">New</entry>
<entry key="s000001" count="0" color="red" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" color="yellow" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" color="green" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="i000004" count="10" red="120" green="120" blue="120" shape="SQUARE">New</entry>
<entry key="i000005" count="0" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="i000004" count="10" color="purple" shape="SQUARE">New</entry>
<entry key="i000005" count="0" color="purple" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" color="purple" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="16" novelWords="9" notesWords="0" novelChars="40" notesChars="0">
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:52">
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 18:02:55">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -16,16 +16,16 @@
</lastHandle>
<autoReplace />
<status>
<entry key="s000000" count="16" red="120" green="120" blue="120" shape="STAR">New</entry>
<entry key="s000001" count="0" red="205" green="171" blue="143" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" red="143" green="240" blue="164" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" red="249" green="240" blue="107" shape="STAR">Finished</entry>
<entry key="s000000" count="16" color="faded" shape="STAR">New</entry>
<entry key="s000001" count="0" color="red" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" color="yellow" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" color="green" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="i000004" count="3" red="120" green="120" blue="120" shape="SQUARE">New</entry>
<entry key="i000005" count="0" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="i000004" count="3" color="purple" shape="SQUARE">New</entry>
<entry key="i000005" count="0" color="purple" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" color="purple" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="19" novelWords="28" notesWords="0" novelChars="129" notesChars="0">
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:52">
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 18:02:55">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project A</name>
<author>Jane Doe</author>
@@ -16,16 +16,16 @@
</lastHandle>
<autoReplace />
<status>
<entry key="s000000" count="15" red="120" green="120" blue="120" shape="STAR">New</entry>
<entry key="s000001" count="0" red="205" green="171" blue="143" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" red="143" green="240" blue="164" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" red="249" green="240" blue="107" shape="STAR">Finished</entry>
<entry key="s000000" count="15" color="faded" shape="STAR">New</entry>
<entry key="s000001" count="0" color="red" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" color="yellow" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" color="green" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="i000004" count="7" red="120" green="120" blue="120" shape="SQUARE">New</entry>
<entry key="i000005" count="0" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="i000004" count="7" color="purple" shape="SQUARE">New</entry>
<entry key="i000005" count="0" color="purple" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" color="purple" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="22" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:42:52">
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 18:02:55">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="0" editTime="0">
<name>Test Project B</name>
<author>Jane Doe</author>
@@ -16,16 +16,16 @@
</lastHandle>
<autoReplace />
<status>
<entry key="s000000" count="9" red="120" green="120" blue="120" shape="STAR">New</entry>
<entry key="s000001" count="0" red="205" green="171" blue="143" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" red="143" green="240" blue="164" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" red="249" green="240" blue="107" shape="STAR">Finished</entry>
<entry key="s000000" count="9" color="faded" shape="STAR">New</entry>
<entry key="s000001" count="0" color="red" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" color="yellow" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" color="green" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="i000004" count="7" red="120" green="120" blue="120" shape="SQUARE">New</entry>
<entry key="i000005" count="0" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="i000004" count="7" color="purple" shape="SQUARE">New</entry>
<entry key="i000005" count="0" color="purple" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" color="purple" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="16" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
@@ -1,8 +1,8 @@
<?xml version='1.0' encoding='utf-8'?>
<ns0:Properties xmlns:ns0="http://schemas.openxmlformats.org/officeDocument/2006/extended-properties">
<ns0:TotalTime>41</ns0:TotalTime>
<ns0:Application>novelWriter/2.7b1</ns0:Application>
<ns0:Words>4035</ns0:Words>
<ns0:TotalTime>45</ns0:TotalTime>
<ns0:Application>novelWriter/2.8a1</ns0:Application>
<ns0:Words>4050</ns0:Words>
<ns0:Characters>21296</ns0:Characters>
<ns0:CharactersWithSpaces>24964</ns0:CharactersWithSpaces>
<ns0:Paragraphs>45</ns0:Paragraphs>
@@ -1,10 +1,10 @@
<?xml version='1.0' encoding='utf-8'?>
<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<dcterms:created xsi:type="dcterms:W3CDTF">2025-04-29T22:46:36</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2025-04-29T22:46:36</dcterms:modified>
<dcterms:created xsi:type="dcterms:W3CDTF">2025-06-19T21:08:59</dcterms:created>
<dcterms:modified xsi:type="dcterms:W3CDTF">2025-06-19T21:08:59</dcterms:modified>
<dc:creator>lipsum.com</dc:creator>
<dc:title>Lorem Ipsum</dc:title>
<dc:language>en_GB</dc:language>
<cp:revision>52</cp:revision>
<cp:revision>55</cp:revision>
<cp:lastModifiedBy>lipsum.com</cp:lastModifiedBy>
</cp:coreProperties>
@@ -34,7 +34,7 @@
</w:r>
<w:r>
<w:rPr />
<w:t>4,035</w:t>
<w:t>4,050</w:t>
</w:r>
</w:p>
<w:p>
@@ -48,7 +48,7 @@
</w:r>
<w:r>
<w:rPr />
<w:t>27,064</w:t>
<w:t>27,164</w:t>
</w:r>
</w:p>
<w:p>
@@ -1250,6 +1250,28 @@
<w:t>Main</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="MetaText" />
</w:pPr>
<w:r>
<w:rPr>
<w:b />
<w:color w:val="813709" />
</w:rPr>
<w:t>Short Description:</w:t>
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> </w:t>
</w:r>
<w:r>
<w:rPr>
<w:color w:val="813709" />
</w:rPr>
<w:t>The Hero</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Normal" />
@@ -1323,6 +1345,28 @@
<w:t>Main</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="MetaText" />
</w:pPr>
<w:r>
<w:rPr>
<w:b />
<w:color w:val="813709" />
</w:rPr>
<w:t>Short Description:</w:t>
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> </w:t>
</w:r>
<w:r>
<w:rPr>
<w:color w:val="813709" />
</w:rPr>
<w:t>Where it all happens</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Normal" />
@@ -1396,6 +1440,28 @@
<w:t>Ancient Europe</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="MetaText" />
</w:pPr>
<w:r>
<w:rPr>
<w:b />
<w:color w:val="813709" />
</w:rPr>
<w:t>Short Description:</w:t>
</w:r>
<w:r>
<w:rPr />
<w:t xml:space="preserve"> </w:t>
</w:r>
<w:r>
<w:rPr>
<w:color w:val="813709" />
</w:rPr>
<w:t>A strange place</w:t>
</w:r>
</w:p>
<w:p>
<w:pPr>
<w:pStyle w:val="Normal" />
@@ -9,13 +9,13 @@
<w:docVars>
<w:docVar w:name="ManuscriptTitleCount" w:val="11" />
<w:docVar w:name="ManuscriptParagraphCount" w:val="45" />
<w:docVar w:name="ManuscriptAllWords" w:val="4035" />
<w:docVar w:name="ManuscriptAllWords" w:val="4050" />
<w:docVar w:name="ManuscriptTextWords" w:val="3711" />
<w:docVar w:name="ManuscriptTitleWords" w:val="21" />
<w:docVar w:name="ManuscriptAllChars" w:val="27064" />
<w:docVar w:name="ManuscriptAllChars" w:val="27164" />
<w:docVar w:name="ManuscriptTextChars" w:val="24964" />
<w:docVar w:name="ManuscriptTitleChars" w:val="123" />
<w:docVar w:name="ManuscriptAllWordChars" w:val="23120" />
<w:docVar w:name="ManuscriptAllWordChars" w:val="23208" />
<w:docVar w:name="ManuscriptTextWordChars" w:val="21296" />
<w:docVar w:name="ManuscriptTitleWordChars" w:val="113" />
</w:docVars>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:08:41">
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 18:34:07">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="4">
<name>New Project</name>
<author>Jane Doe</author>
@@ -16,16 +16,16 @@
</lastHandle>
<autoReplace />
<status>
<entry key="s000000" count="5" red="120" green="120" blue="120" shape="STAR">New</entry>
<entry key="s000001" count="0" red="205" green="171" blue="143" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" red="143" green="240" blue="164" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" red="249" green="240" blue="107" shape="STAR">Finished</entry>
<entry key="s000000" count="5" color="faded" shape="STAR">New</entry>
<entry key="s000001" count="0" color="red" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" color="yellow" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" color="green" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="i000004" count="7" red="120" green="120" blue="120" shape="SQUARE">New</entry>
<entry key="i000005" count="0" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="i000004" count="7" color="purple" shape="SQUARE">New</entry>
<entry key="i000005" count="0" color="purple" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" color="purple" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="12" novelWords="173" notesWords="27" novelChars="1007" notesChars="133">
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2025-04-29 22:01:24">
<novelWriterXML appVersion="2.8a0" hexVersion="0x020800a0" fileVersion="1.5" fileRevision="6" timeStamp="2025-06-12 18:03:05">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="2" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -16,16 +16,16 @@
</lastHandle>
<autoReplace />
<status>
<entry key="s000000" count="5" red="120" green="120" blue="120" shape="STAR">New</entry>
<entry key="s000001" count="0" red="205" green="171" blue="143" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" red="143" green="240" blue="164" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" red="249" green="240" blue="107" shape="STAR">Finished</entry>
<entry key="s000000" count="5" color="faded" shape="STAR">New</entry>
<entry key="s000001" count="0" color="red" shape="TRIANGLE">Note</entry>
<entry key="s000002" count="0" color="yellow" shape="CIRCLE_T">Draft</entry>
<entry key="s000003" count="0" color="green" shape="STAR">Finished</entry>
</status>
<importance>
<entry key="i000004" count="3" red="120" green="120" blue="120" shape="SQUARE">New</entry>
<entry key="i000005" count="0" red="220" green="138" blue="221" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" red="220" green="138" blue="221" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" red="220" green="138" blue="221" shape="BLOCK_4">Main</entry>
<entry key="i000004" count="3" color="purple" shape="SQUARE">New</entry>
<entry key="i000005" count="0" color="purple" shape="BLOCK_2">Minor</entry>
<entry key="i000006" count="0" color="purple" shape="BLOCK_3">Major</entry>
<entry key="i000007" count="0" color="purple" shape="BLOCK_4">Main</entry>
</importance>
</settings>
<content items="8" novelWords="9" notesWords="0" novelChars="40" notesChars="0">
+13 -13
View File
@@ -1,13 +1,13 @@
"H","Title","Document","Line","Status","Chars","Words","Pars","POV","Focus","Characters","Plot","Timeline","Locations","Objects","Entities","Custom","Story","Mentions","Synopsis"
"H1","Lorem Ipsum","Lorem Ipsum","1","Finished","259","44","5","","","","","","","","","","","",""
"H2","Prologue","Prologue","1","Draft","605","94","2","","","","","","","","","","","","Explanation from the lipsum.com website."
"H1","Act One","Act One","1","New","35","6","1","","","","","","","","","","","",""
"H2","Chapter One","Chapter One","1","Draft","419","67","1","Bod","","","Main","","Europe","","","","","","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam."
"H3","Scene One","Scene One","1","Finished","1197","174","2","Bod","","","Main","","Europe","","","","","","Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur."
"H4","Scene One, Section Two","Scene One","13","Finished","1561","230","2","","","","","","","","","","","",""
"H3","Scene Two","Scene Two","1","Finished","2034","299","3","Bod","","","Main","","Europe","","","","","","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci."
"H4","Scene Two, Section Two","Scene Two","15","Finished","2009","301","3","","","","","","","","","","","",""
"H2","Chapter Two","Chapter Two","1","Draft","477","70","1","Bod","","","Main","","Europe","","","","","","Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue."
"H3","Scene Three","Scene Three","1","Finished","3006","439","4","Bod","","","Main","","Europe","","","","","","Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos."
"H3","Scene Four","Scene Four","1","Finished","3839","563","6","Bod","","","Main","","Europe","","","","","","Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo."
"H3","Scene Five","Scene Five","1","Finished","3644","543","5","Bod","","","Main","","Europe","","","","","","Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus."
"H","Title","Document","Line","Status","Chars","Words","Pars","POV","Focus","Characters","Plot","Timeline","Locations","Objects","Entities","Custom","Story","Mentions","Synopsis","Note (Purpose)"
"H1","Lorem Ipsum","Lorem Ipsum","1","Finished","259","44","5","","","","","","","","","","","","",""
"H2","Prologue","Prologue","1","Draft","605","94","2","","","","","","","","","","","","Explanation from the lipsum.com website.","Introduction"
"H1","Act One","Act One","1","New","35","6","1","","","","","","","","","","","","",""
"H2","Chapter One","Chapter One","1","Draft","419","67","1","Bod","","","Main","","Europe","","","","","","Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.",""
"H3","Scene One","Scene One","1","Finished","1197","174","2","Bod","","","Main","","Europe","","","","","","Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.",""
"H4","Scene One, Section Two","Scene One","13","Finished","1561","230","2","","","","","","","","","","","","",""
"H3","Scene Two","Scene Two","1","Finished","2034","299","3","Bod","","","Main","","Europe","","","","","","Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.",""
"H4","Scene Two, Section Two","Scene Two","15","Finished","2009","301","3","","","","","","","","","","","","",""
"H2","Chapter Two","Chapter Two","1","Draft","477","70","1","Bod","","","Main","","Europe","","","","","","Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.",""
"H3","Scene Three","Scene Three","1","Finished","3006","439","4","Bod","","","Main","","Europe","","","","","","Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.",""
"H3","Scene Four","Scene Four","1","Finished","3839","563","6","Bod","","","Main","","Europe","","","","","","Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.",""
"H3","Scene Five","Scene Five","1","Finished","3644","543","5","Bod","","","Main","","Europe","","","","","","Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.",""
1 H Title Document Line Status Chars Words Pars POV Focus Characters Plot Timeline Locations Objects Entities Custom Story Mentions Synopsis Note (Purpose)
2 H1 Lorem Ipsum Lorem Ipsum 1 Finished 259 44 5
3 H2 Prologue Prologue 1 Draft 605 94 2 Explanation from the lipsum.com website. Introduction
4 H1 Act One Act One 1 New 35 6 1
5 H2 Chapter One Chapter One 1 Draft 419 67 1 Bod Main Europe Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.
6 H3 Scene One Scene One 1 Finished 1197 174 2 Bod Main Europe Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.
7 H4 Scene One, Section Two Scene One 13 Finished 1561 230 2
8 H3 Scene Two Scene Two 1 Finished 2034 299 3 Bod Main Europe Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.
9 H4 Scene Two, Section Two Scene Two 15 Finished 2009 301 3
10 H2 Chapter Two Chapter Two 1 Draft 477 70 1 Bod Main Europe Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.
11 H3 Scene Three Scene Three 1 Finished 3006 439 4 Bod Main Europe Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
12 H3 Scene Four Scene Four 1 Finished 3839 563 6 Bod Main Europe Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.
13 H3 Scene Five Scene Five 1 Finished 3644 543 5 Bod Main Europe Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
@@ -3,9 +3,9 @@ Lorem Ipsum
**By lipsum.com**
Word Count: 4,169
Word Count: 4,184
Character Count: 27,898
Character Count: 27,998
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
@@ -162,6 +162,8 @@ Notes: Characters
**Tag:** Bod | Nobody Owens
**Plot:** Main
**Short Description:** The Hero
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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.
Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.
@@ -175,6 +177,8 @@ Notes: Plot
**Tag:** Main
**Short Description:** Where it all happens
Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.
Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.
@@ -186,6 +190,8 @@ Notes: World
**Tag:** Europe | Ancient Europe
**Short Description:** A strange place
Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.
Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.
@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>Lorem Ipsum</title>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width, initial-scale=1'>
<style>
body {color: #000000; font-family: 'Arial'; font-size: 12pt; font-weight: 400; font-style: normal;}
p {text-align: left; line-height: 150%; margin-top: 0.00em; margin-bottom: 0.60em;}
@@ -20,8 +21,8 @@ h4 {font-size: 1.25em; margin-top: 1.20em; margin-bottom: 0.60em;}
<body>
<h1 class='title' style='text-align: center;'>Lorem Ipsum</h1>
<p style='text-align: center;'><strong>By lipsum.com</strong></p>
<p style='text-align: center;'>Word Count: 4,169</p>
<p style='text-align: center;'>Character Count: 27,898</p>
<p style='text-align: center;'>Word Count: 4,184</p>
<p style='text-align: center;'>Character Count: 27,998</p>
<p style='text-align: center;'>“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”</p>
<p style='text-align: center;'>“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”</p>
<p class='comment' style='text-align: justify; page-break-before: always;'><strong><span style='color: #646464'>Comment:</span></strong> <span style='color: #646464'>Exctracted from the lipsum.com website.</span></p>
@@ -106,17 +107,20 @@ h4 {font-size: 1.25em; margin-top: 1.20em; margin-bottom: 0.60em;}
<h1>Nobody Owens</h1>
<p class='meta meta-tag' style='margin-bottom: 0;'><strong><span style='color: #f5871f'>Tag:</span></strong> <span style='color: #4271ae'><a name='tag_bod'>Bod</a></span> | <span style='color: #4271ae'>Nobody Owens</span></p>
<p class='meta meta-plot' style='margin-top: 0;'><strong><span style='color: #f5871f'>Plot:</span></strong> <span style='color: #4271ae'><a href='#tag_main'>Main</a></span></p>
<p class='comment' style='text-align: justify;'><strong><span style='color: #813709'>Short Description:</span></strong> <span style='color: #813709'>The Hero</span></p>
<p style='text-align: justify;'>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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.</p>
<p style='text-align: justify; text-indent: 1.40em;'>Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.</p>
<p style='text-align: justify; text-indent: 1.40em;'>Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque.</p>
<h1 class='title' style='text-align: center; page-break-before: always;'>Notes: Plot</h1>
<h1>Main Plot</h1>
<p class='meta meta-tag'><strong><span style='color: #f5871f'>Tag:</span></strong> <span style='color: #4271ae'><a name='tag_main'>Main</a></span></p>
<p class='comment' style='text-align: justify;'><strong><span style='color: #813709'>Short Description:</span></strong> <span style='color: #813709'>Where it all happens</span></p>
<p style='text-align: justify;'>Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.</p>
<p style='text-align: justify; text-indent: 1.40em;'>Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.</p>
<h1 class='title' style='text-align: center; page-break-before: always;'>Notes: World</h1>
<h1>Ancient Europe</h1>
<p class='meta meta-tag'><strong><span style='color: #f5871f'>Tag:</span></strong> <span style='color: #4271ae'><a name='tag_europe'>Europe</a></span> | <span style='color: #4271ae'>Ancient Europe</span></p>
<p class='comment' style='text-align: justify;'><strong><span style='color: #813709'>Short Description:</span></strong> <span style='color: #813709'>A strange place</span></p>
<p style='text-align: justify;'>Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.</p>
<p style='text-align: justify; text-indent: 1.40em;'>Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.</p>
<p style='text-align: justify; text-indent: 1.40em;'>Nunc ullamcorper magna quis elit condimentum rhoncus. Aenean dictum pulvinar dolor suscipit interdum. Aliquam elit massa, elementum nec cursus eu, maximus nec ipsum. Donec ullamcorper iaculis dolor eu commodo. Nunc eget tortor quis turpis consectetur varius. Vestibulum nec justo vel tellus venenatis condimentum. Duis auctor iaculis massa. Nunc risus magna, rutrum vitae eros non, tristique mollis enim.</p>
@@ -2,8 +2,8 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1737835371,
"buildTimeStr": "2025-01-25 21:02:51"
"buildTime": 1750360261,
"buildTimeStr": "2025-06-19 21:11:01"
},
"text": {
"css": [
@@ -23,8 +23,8 @@
[
"<h1 class='title' style='text-align: center;'>Lorem Ipsum</h1>",
"<p style='text-align: center;'><strong>By lipsum.com</strong></p>",
"<p style='text-align: center;'>Word Count: 4,169</p>",
"<p style='text-align: center;'>Character Count: 27,898</p>",
"<p style='text-align: center;'>Word Count: 4,184</p>",
"<p style='text-align: center;'>Character Count: 27,998</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>"
],
@@ -135,6 +135,7 @@
"<h1>Nobody Owens</h1>",
"<p class='meta meta-tag' style='margin-bottom: 0;'><strong><span style='color: #f5871f'>Tag:</span></strong> <span style='color: #4271ae'><a name='tag_bod'>Bod</a></span> | <span style='color: #4271ae'>Nobody Owens</span></p>",
"<p class='meta meta-plot' style='margin-top: 0;'><strong><span style='color: #f5871f'>Plot:</span></strong> <span style='color: #4271ae'><a href='#tag_main'>Main</a></span></p>",
"<p class='comment' style='text-align: justify;'><strong><span style='color: #813709'>Short Description:</span></strong> <span style='color: #813709'>The Hero</span></p>",
"<p style='text-align: justify;'>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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.</p>",
"<p style='text-align: justify; text-indent: 1.40em;'>Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.</p>",
"<p style='text-align: justify; text-indent: 1.40em;'>Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque.</p>"
@@ -145,6 +146,7 @@
[
"<h1>Main Plot</h1>",
"<p class='meta meta-tag'><strong><span style='color: #f5871f'>Tag:</span></strong> <span style='color: #4271ae'><a name='tag_main'>Main</a></span></p>",
"<p class='comment' style='text-align: justify;'><strong><span style='color: #813709'>Short Description:</span></strong> <span style='color: #813709'>Where it all happens</span></p>",
"<p style='text-align: justify;'>Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.</p>",
"<p style='text-align: justify; text-indent: 1.40em;'>Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.</p>"
],
@@ -154,6 +156,7 @@
[
"<h1>Ancient Europe</h1>",
"<p class='meta meta-tag'><strong><span style='color: #f5871f'>Tag:</span></strong> <span style='color: #4271ae'><a name='tag_europe'>Europe</a></span> | <span style='color: #4271ae'>Ancient Europe</span></p>",
"<p class='comment' style='text-align: justify;'><strong><span style='color: #813709'>Short Description:</span></strong> <span style='color: #813709'>A strange place</span></p>",
"<p style='text-align: justify;'>Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.</p>",
"<p style='text-align: justify; text-indent: 1.40em;'>Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.</p>",
"<p style='text-align: justify; text-indent: 1.40em;'>Nunc ullamcorper magna quis elit condimentum rhoncus. Aenean dictum pulvinar dolor suscipit interdum. Aliquam elit massa, elementum nec cursus eu, maximus nec ipsum. Donec ullamcorper iaculis dolor eu commodo. Nunc eget tortor quis turpis consectetur varius. Vestibulum nec justo vel tellus venenatis condimentum. Duis auctor iaculis massa. Nunc risus magna, rutrum vitae eros non, tristique mollis enim.</p>"
@@ -2,8 +2,8 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1731001720,
"buildTimeStr": "2024-11-07 18:48:40"
"buildTime": 1750360135,
"buildTimeStr": "2025-06-19 21:08:55"
},
"text": {
"nwd": [
@@ -32,7 +32,8 @@
[
"##! Prologue",
"",
"% Synopsis: Explanation from the lipsum.com website.",
"%Synopsis: Explanation from the lipsum.com website.",
"%Note.Purpose: Introduction",
"",
"_Lorem Ipsum_ is simply dummy text[footnote:f9kgf] of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
"",
@@ -52,7 +53,7 @@
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.",
"%Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.",
"",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar."
],
@@ -63,7 +64,7 @@
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.",
"%Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.",
"",
"Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada.",
"",
@@ -82,7 +83,7 @@
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. ",
"%Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. ",
"",
"Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien.",
"",
@@ -116,7 +117,7 @@
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.",
"%Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.",
"",
"Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at."
],
@@ -127,7 +128,7 @@
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.",
"%Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.",
"",
"Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis.",
"",
@@ -144,7 +145,7 @@
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.",
"%Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.",
"",
"Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien.",
"",
@@ -165,7 +166,7 @@
"@plot: Main",
"@location: Europe",
"",
"% Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.",
"%Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.",
"",
"Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet.",
"",
@@ -186,6 +187,8 @@
"@tag: Bod | Nobody Owens",
"@plot: Main",
"",
"%Short: The Hero",
"",
"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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.",
"",
"Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.",
@@ -200,6 +203,8 @@
"",
"@tag: Main",
"",
"%Short: Where it all happens",
"",
"Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.",
"",
"Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur."
@@ -212,6 +217,8 @@
"",
"@tag: Europe | Ancient Europe",
"",
"%Short: A strange place",
"",
"Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.",
"",
"Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.",
@@ -20,7 +20,8 @@ The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for t
##! Prologue
% Synopsis: Explanation from the lipsum.com website.
%Synopsis: Explanation from the lipsum.com website.
%Note.Purpose: Introduction
_Lorem Ipsum_ is simply dummy text[footnote:f9kgf] of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
@@ -38,7 +39,7 @@ See http://lipsum.com
@plot: Main
@location: Europe
% Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.
%Synopsis: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam. Praesent magna nunc, lacinia sit amet quam eget, aliquet ultrices justo. Morbi ornare enim et lorem rutrum finibus ut eu dolor. Aliquam a orci odio. Ut ultrices sem quis massa placerat, eget mollis nisl cursus. Cras vel sagittis justo. Ut non ultricies leo. Maecenas rutrum velit in est varius, et egestas massa pulvinar.
@@ -48,7 +49,7 @@ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam
@plot: Main
@location: Europe
% Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.
%Synopsis: Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur.
Aenean ut placerat velit. Etiam laoreet ullamcorper risus, eget lobortis enim scelerisque non. Suspendisse id maximus nunc, et mollis sapien. Curabitur vel semper sapien, non pulvinar dolor. Etiam finibus nisi vel mi molestie consectetur. Donec quis ante nunc. Mauris ut leo ipsum. Vestibulum est neque, hendrerit nec neque a, ullamcorper lobortis tellus. Fusce sollicitudin purus quis congue bibendum. Aliquam condimentum ipsum tristique blandit tristique. Donec pulvinar neque ac suscipit malesuada.
@@ -66,7 +67,7 @@ Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien,
@plot: Main
@location: Europe
% Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.
%Synopsis: Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci.
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Integer sapien nulla, dictum at lacus a, dignissim consectetur dolor. Nunc vel eleifend lacus, eu dapibus orci. Vestibulum facilisis bibendum aliquam. Aliquam posuere, turpis ac bibendum varius, sem tellus venenatis risus, in elementum massa enim ac lorem. Integer in sem ac diam blandit ultricies ut in nulla. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet erat est. Curabitur vitae cursus justo, sit amet placerat dolor. Vivamus eu felis hendrerit, tincidunt massa rutrum, maximus arcu. Pellentesque commodo justo odio, vel rutrum nulla tincidunt eu. Integer non neque condimentum, convallis diam non, varius ligula. Aliquam eget sapien mauris. Aenean pharetra nunc nisi, vel maximus ante tristique sit amet. Aliquam risus metus, interdum non odio eu, consectetur lacinia sapien.
@@ -98,7 +99,7 @@ Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et,
@plot: Main
@location: Europe
% Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.
%Synopsis: Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.
Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue. Vestibulum pharetra fermentum leo, sed faucibus eros placerat quis. In hac habitasse platea dictumst. Donec metus massa, rutrum quis consequat et, tincidunt ac felis. Duis mollis metus ac nunc tincidunt blandit. Ut aliquet velit eu odio pharetra condimentum. Integer rutrum lacus orci, id venenatis libero accumsan at.
@@ -108,7 +109,7 @@ Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis phar
@plot: Main
@location: Europe
% Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
%Synopsis: Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
Aenean ut libero ut lectus porttitor rhoncus vel et massa. Nam pretium, nibh et varius vehicula, urna metus blandit eros, euismod pharetra diam diam et libero. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean tincidunt lacus vitae nibh elementum eleifend. Sed rutrum condimentum sem quis blandit. Duis imperdiet libero metus, quis convallis quam faucibus a. Nulla ligula est, semper quis sollicitudin et, pretium id justo. Curabitur pharetra risus eget consectetur commodo. Duis mattis arcu non est condimentum, id venenatis risus volutpat. Pellentesque aliquet mauris non mauris porttitor ultrices. Phasellus ut vestibulum mi. Suspendisse malesuada metus lorem, a malesuada orci rhoncus a. Praesent euismod convallis ante, lacinia tincidunt ex egestas id. Praesent sit amet efficitur sapien. Morbi tincidunt volutpat nunc sed dictum. Aliquam ultrices metus id fermentum lobortis.
@@ -124,7 +125,7 @@ Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci va
@plot: Main
@location: Europe
% Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.
%Synopsis: Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo.
Nam tempor blandit magna laoreet aliquet. Vestibulum auctor posuere leo, ac gravida nisi rhoncus varius. Aenean posuere dolor vitae condimentum volutpat. Donec egestas volutpat risus, quis luctus justo. Nullam viverra dui et auctor pretium. Ut ullamcorper velit urna, sed imperdiet massa convallis a. Suspendisse efficitur, ipsum nec cursus pulvinar, eros urna posuere diam, nec elementum mi felis vitae sapien.
@@ -144,7 +145,7 @@ Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lob
@plot: Main
@location: Europe
% Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
%Synopsis: Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
Praesent eget est porta, dictum ante in, egestas risus. Mauris risus mauris, consequat aliquam mauris et, feugiat iaculis ipsum. Aliquam arcu ipsum, fermentum ut arcu sed, lobortis euismod sem. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. In sed felis auctor, rhoncus dui ac, consequat dolor. Integer volutpat libero sed nisl aliquet varius. Suspendisse et lorem sapien. Proin id ultrices nibh, ac suscipit diam. Suspendisse placerat varius porttitor. Curabitur elementum sed enim ultrices imperdiet.
@@ -163,6 +164,8 @@ Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacin
@tag: Bod | Nobody Owens
@plot: Main
%Short: The Hero
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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.
Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.
@@ -175,6 +178,8 @@ Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales
@tag: Main
%Short: Where it all happens
Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.
Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.
@@ -185,6 +190,8 @@ Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam.
@tag: Europe | Ancient Europe
%Short: A strange place
Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.
Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<office:document xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
<office:meta>
<meta:creation-date>2025-01-25T16:15:30</meta:creation-date>
<meta:generator>novelWriter/2.6rc1</meta:generator>
<meta:creation-date>2025-06-19T21:05:16</meta:creation-date>
<meta:generator>novelWriter/2.8a1</meta:generator>
<meta:initial-creator>lipsum.com</meta:initial-creator>
<meta:editing-cycles>51</meta:editing-cycles>
<meta:editing-duration>P0DT0H40M56S</meta:editing-duration>
<meta:editing-cycles>55</meta:editing-cycles>
<meta:editing-duration>P0DT0H45M46S</meta:editing-duration>
<dc:title>Lorem Ipsum</dc:title>
<dc:date>2025-01-25T16:15:30</dc:date>
<dc:date>2025-06-19T21:05:16</dc:date>
<dc:creator>lipsum.com</dc:creator>
</office:meta>
<office:font-face-decls>
@@ -152,13 +152,13 @@
<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="48" text:name="ManuscriptParagraphCount" />
<text:user-field-decl office:value-type="float" office:value="4169" text:name="ManuscriptAllWords" />
<text:user-field-decl office:value-type="float" office:value="4184" text:name="ManuscriptAllWords" />
<text:user-field-decl office:value-type="float" office:value="3815" text:name="ManuscriptTextWords" />
<text:user-field-decl office:value-type="float" office:value="54" text:name="ManuscriptTitleWords" />
<text:user-field-decl office:value-type="float" office:value="27898" text:name="ManuscriptAllChars" />
<text:user-field-decl office:value-type="float" office:value="27998" text:name="ManuscriptAllChars" />
<text:user-field-decl office:value-type="float" office:value="25578" text:name="ManuscriptTextChars" />
<text:user-field-decl office:value-type="float" office:value="310" text:name="ManuscriptTitleChars" />
<text:user-field-decl office:value-type="float" office:value="23826" text:name="ManuscriptAllWordChars" />
<text:user-field-decl office:value-type="float" office:value="23914" text:name="ManuscriptAllWordChars" />
<text:user-field-decl office:value-type="float" office:value="21806" text:name="ManuscriptTextWordChars" />
<text:user-field-decl office:value-type="float" office:value="275" text:name="ManuscriptTitleWordChars" />
</text:user-field-decls>
@@ -255,17 +255,20 @@
<text:h text:style-name="Heading_20_1" text:outline-level="1">Nobody Owens</text:h>
<text:p text:style-name="P8"><text:span text:style-name="T8">Tag:</text:span> <text:span text:style-name="T9">Bod</text:span> | <text:span text:style-name="T10">Nobody Owens</text:span></text:p>
<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T8">Plot:</text:span> <text:span text:style-name="T9">Main</text:span></text:p>
<text:p text:style-name="P5"><text:span text:style-name="T4">Short Description:</text:span> <text:span text:style-name="T5">The Hero</text:span></text:p>
<text:p text:style-name="P6">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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.</text:p>
<text:p text:style-name="P3">Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.</text:p>
<text:p text:style-name="P3">Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque.</text:p>
<text:p text:style-name="P7">Notes: Plot</text:p>
<text:h text:style-name="Heading_20_1" text:outline-level="1">Main Plot</text:h>
<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T8">Tag:</text:span> <text:span text:style-name="T9">Main</text:span></text:p>
<text:p text:style-name="P5"><text:span text:style-name="T4">Short Description:</text:span> <text:span text:style-name="T5">Where it all happens</text:span></text:p>
<text:p text:style-name="P6">Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.</text:p>
<text:p text:style-name="P3">Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.</text:p>
<text:p text:style-name="P7">Notes: World</text:p>
<text:h text:style-name="Heading_20_1" text:outline-level="1">Ancient Europe</text:h>
<text:p text:style-name="Text_20_Meta"><text:span text:style-name="T8">Tag:</text:span> <text:span text:style-name="T9">Europe</text:span> | <text:span text:style-name="T10">Ancient Europe</text:span></text:p>
<text:p text:style-name="P5"><text:span text:style-name="T4">Short Description:</text:span> <text:span text:style-name="T5">A strange place</text:span></text:p>
<text:p text:style-name="P6">Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.</text:p>
<text:p text:style-name="P3">Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.</text:p>
<text:p text:style-name="P3">Nunc ullamcorper magna quis elit condimentum rhoncus. Aenean dictum pulvinar dolor suscipit interdum. Aliquam elit massa, elementum nec cursus eu, maximus nec ipsum. Donec ullamcorper iaculis dolor eu commodo. Nunc eget tortor quis turpis consectetur varius. Vestibulum nec justo vel tellus venenatis condimentum. Duis auctor iaculis massa. Nunc risus magna, rutrum vitae eros non, tristique mollis enim.</text:p>
@@ -3,9 +3,9 @@ Lorem Ipsum
**By lipsum.com**
Word Count: 4,169
Word Count: 4,184
Character Count: 27,898
Character Count: 27,998
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
@@ -162,6 +162,8 @@ Notes: Characters
**Tag:** Bod | Nobody Owens
**Plot:** Main
**Short Description:** The Hero
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 placerat condimentum. Aenean ante enim, cursus id lorem a, blandit venenatis nibh. Maecenas suscipit porta elit, sit amet porta felis porttitor eu. Sed a dui nibh. Phasellus sed faucibus dui. Pellentesque felis nulla, ultrices non efficitur quis, rutrum id mi. Mauris tempus auctor nisl, in bibendum enim pellentesque sit amet. Proin nunc lacus, imperdiet nec posuere ac, interdum non lectus.
Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque. Interdum et malesuada fames ac ante ipsum primis in faucibus. Donec sodales rutrum mattis. In in sem ornare, consequat nulla ac, convallis arcu. Duis ac metus id felis commodo commodo sit amet eget diam. Curabitur rhoncus lacinia leo at sodales. Etiam finibus porta diam a viverra. Praesent nisi urna, volutpat sit amet odio at, vehicula vehicula leo. In non enim eget nisl luctus commodo. Pellentesque pellentesque at lectus at luctus. Quisque nec felis bibendum, lacinia libero ut, lacinia eros. Integer finibus ultricies nibh sit amet placerat.
@@ -175,6 +177,8 @@ Notes: Plot
**Tag:** Main
**Short Description:** Where it all happens
Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vitae ultricies felis ullamcorper eu. Duis luctus risus mi, in accumsan velit cursus ut. Vestibulum eleifend leo in magna eleifend fermentum. Proin nec ornare elit. Phasellus nec interdum risus. In a volutpat augue, quis egestas justo. Morbi porta mauris mattis bibendum imperdiet.
Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.
@@ -186,6 +190,8 @@ Notes: World
**Tag:** Europe | Ancient Europe
**Short Description:** A strange place
Vivamus sodales risus ac accumsan posuere. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nunc vel enim felis. Vestibulum dignissim massa nunc, a auctor magna eleifend et. Proin dignissim sodales erat vitae convallis. Aliquam id tellus dui. Curabitur sollicitudin scelerisque ex sit amet posuere. Nam rutrum felis id rhoncus feugiat. Duis sagittis quam quis purus efficitur, quis rutrum odio iaculis. Maecenas semper ante turpis, at vulputate mi consectetur non. Sed rutrum nibh turpis, quis rhoncus purus ornare quis. Vestibulum at rutrum mauris. Integer dolor nisi, tincidunt eget vehicula ac, ultricies at ligula.
Aenean semper turpis quis varius rhoncus. Vivamus ac mi eget felis euismod vulputate. Nam eu tempus velit. Etiam ut est porta, finibus erat sit amet, consectetur felis. Nullam consequat felis ut lacus pharetra, in lobortis urna mollis. Nulla varius eros nec lorem rhoncus, sed venenatis risus ultrices. Phasellus pellentesque laoreet neque, ut ultricies lacus vulputate quis. In malesuada dui sit amet est interdum, eget consectetur mi gravida. Cras vel bibendum purus. Quisque commodo tempor arcu, non lacinia sem blandit eleifend. Quisque at neque gravida, porttitor metus a, suscipit diam. Quisque convallis sodales lacus et condimentum. Donec a suscipit diam. Pellentesque eget cursus neque.
+12 -12
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2020-05-28 09:59:15">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="6" timeStamp="2020-05-28 09:59:15">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="0" autoCount="0" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -20,19 +20,19 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="s000000" count="0" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" red="182" green="60" blue="0" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" red="193" green="129" blue="0" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" red="193" green="129" blue="0" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" red="193" green="129" blue="0" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" red="58" green="180" blue="58" shape="SQUARE">Finished</entry>
<entry key="s000000" count="0" color="#646464" shape="SQUARE">New</entry>
<entry key="s000001" count="0" color="#c83200" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" color="#b63c00" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" color="#c18100" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" color="#c18100" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" color="#c18100" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" color="#3ab43a" shape="SQUARE">Finished</entry>
</status>
<importance>
<entry key="i000007" count="0" red="100" green="100" blue="100" shape="SQUARE">None</entry>
<entry key="i000008" count="0" red="0" green="122" blue="188" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" red="21" green="0" blue="180" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
<entry key="i000007" count="0" color="#646464" shape="SQUARE">None</entry>
<entry key="i000008" count="0" color="#007abc" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" color="#1500b4" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" color="#7500af" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="22" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
+12 -12
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2020-06-26 21:20:24">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="6" timeStamp="2020-06-26 21:20:24">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -20,19 +20,19 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="s000000" count="0" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" red="182" green="60" blue="0" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" red="193" green="129" blue="0" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" red="193" green="129" blue="0" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" red="193" green="129" blue="0" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" red="58" green="180" blue="58" shape="SQUARE">Finished</entry>
<entry key="s000000" count="0" color="#646464" shape="SQUARE">New</entry>
<entry key="s000001" count="0" color="#c83200" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" color="#b63c00" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" color="#c18100" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" color="#c18100" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" color="#c18100" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" color="#3ab43a" shape="SQUARE">Finished</entry>
</status>
<importance>
<entry key="i000007" count="0" red="100" green="100" blue="100" shape="SQUARE">None</entry>
<entry key="i000008" count="0" red="0" green="122" blue="188" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" red="21" green="0" blue="180" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
<entry key="i000007" count="0" color="#646464" shape="SQUARE">None</entry>
<entry key="i000008" count="0" color="#007abc" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" color="#1500b4" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" color="#7500af" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="22" novelWords="0" notesWords="0" novelChars="0" notesChars="0">
+12 -12
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2021-08-30 23:33:44">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="6" timeStamp="2021-08-30 23:33:44">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -20,19 +20,19 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="s000000" count="0" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" red="182" green="60" blue="0" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" red="193" green="129" blue="0" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" red="193" green="129" blue="0" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" red="193" green="129" blue="0" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" red="58" green="180" blue="58" shape="SQUARE">Finished</entry>
<entry key="s000000" count="0" color="#646464" shape="SQUARE">New</entry>
<entry key="s000001" count="0" color="#c83200" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" color="#b63c00" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" color="#c18100" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" color="#c18100" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" color="#c18100" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" color="#3ab43a" shape="SQUARE">Finished</entry>
</status>
<importance>
<entry key="i000007" count="0" red="100" green="100" blue="100" shape="SQUARE">None</entry>
<entry key="i000008" count="0" red="0" green="122" blue="188" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" red="21" green="0" blue="180" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
<entry key="i000007" count="0" color="#646464" shape="SQUARE">None</entry>
<entry key="i000008" count="0" color="#007abc" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" color="#1500b4" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" color="#7500af" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="25" novelWords="840" notesWords="376" novelChars="0" notesChars="0">
+12 -12
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2022-10-25 18:26:15">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="6" timeStamp="2022-10-25 18:26:15">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -20,19 +20,19 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="s000000" count="0" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="s000001" count="0" red="200" green="50" blue="0" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" red="182" green="60" blue="0" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" red="193" green="129" blue="0" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" red="193" green="129" blue="0" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" red="193" green="129" blue="0" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" red="58" green="180" blue="58" shape="SQUARE">Finished</entry>
<entry key="s000000" count="0" color="#646464" shape="SQUARE">New</entry>
<entry key="s000001" count="0" color="#c83200" shape="SQUARE">Notes</entry>
<entry key="s000002" count="0" color="#b63c00" shape="SQUARE">Started</entry>
<entry key="s000003" count="0" color="#c18100" shape="SQUARE">1st Draft</entry>
<entry key="s000004" count="0" color="#c18100" shape="SQUARE">2nd Draft</entry>
<entry key="s000005" count="0" color="#c18100" shape="SQUARE">3rd Draft</entry>
<entry key="s000006" count="0" color="#3ab43a" shape="SQUARE">Finished</entry>
</status>
<importance>
<entry key="i000007" count="0" red="100" green="100" blue="100" shape="SQUARE">None</entry>
<entry key="i000008" count="0" red="0" green="122" blue="188" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" red="21" green="0" blue="180" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
<entry key="i000007" count="0" color="#646464" shape="SQUARE">None</entry>
<entry key="i000008" count="0" color="#007abc" shape="SQUARE">Minor</entry>
<entry key="i000009" count="0" color="#1500b4" shape="SQUARE">Major</entry>
<entry key="i00000a" count="0" color="#7500af" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="25" novelWords="830" notesWords="376" novelChars="0" notesChars="0">
+12 -12
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="5" timeStamp="2022-10-15 12:12:59">
<novelWriterXML appVersion="2.7b1" hexVersion="0x020700b1" fileVersion="1.5" fileRevision="6" timeStamp="2022-10-15 12:12:59">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="5" autoCount="10" editTime="1000">
<name>Sample Project</name>
<author>Jay Doh</author>
@@ -20,19 +20,19 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="sf12341" count="4" red="100" green="100" blue="100" shape="SQUARE">New</entry>
<entry key="sf24ce6" count="2" red="200" green="50" blue="0" shape="SQUARE">Notes</entry>
<entry key="sc24b8f" count="3" red="182" green="60" blue="0" shape="SQUARE">Started</entry>
<entry key="s90e6c9" count="7" red="193" green="129" blue="0" shape="SQUARE">1st Draft</entry>
<entry key="sd51c5b" count="0" red="193" green="129" blue="0" shape="SQUARE">2nd Draft</entry>
<entry key="s8ae72a" count="0" red="193" green="129" blue="0" shape="SQUARE">3rd Draft</entry>
<entry key="s78ea90" count="1" red="58" green="180" blue="58" shape="SQUARE">Finished</entry>
<entry key="sf12341" count="4" color="#646464" shape="SQUARE">New</entry>
<entry key="sf24ce6" count="2" color="#c83200" shape="SQUARE">Notes</entry>
<entry key="sc24b8f" count="3" color="#b63c00" shape="SQUARE">Started</entry>
<entry key="s90e6c9" count="7" color="#c18100" shape="SQUARE">1st Draft</entry>
<entry key="sd51c5b" count="0" color="#c18100" shape="SQUARE">2nd Draft</entry>
<entry key="s8ae72a" count="0" color="#c18100" shape="SQUARE">3rd Draft</entry>
<entry key="s78ea90" count="1" color="#3ab43a" shape="SQUARE">Finished</entry>
</status>
<importance>
<entry key="ia857f0" count="5" red="100" green="100" blue="100" shape="SQUARE">None</entry>
<entry key="icfb3a5" count="2" red="0" green="122" blue="188" shape="SQUARE">Minor</entry>
<entry key="i2d7a54" count="2" red="21" green="0" blue="180" shape="SQUARE">Major</entry>
<entry key="i56be10" count="1" red="117" green="0" blue="175" shape="SQUARE">Main</entry>
<entry key="ia857f0" count="5" color="#646464" shape="SQUARE">None</entry>
<entry key="icfb3a5" count="2" color="#007abc" shape="SQUARE">Minor</entry>
<entry key="i2d7a54" count="2" color="#1500b4" shape="SQUARE">Major</entry>
<entry key="i56be10" count="1" color="#7500af" shape="SQUARE">Main</entry>
</importance>
</settings>
<content items="27" novelWords="954" notesWords="409" novelChars="0" notesChars="0">
+16 -2
View File
@@ -38,9 +38,10 @@ from novelwriter.common import (
fuzzyTime, getFileSize, hexToInt, isHandle, isItemClass, isItemLayout,
isItemType, isListInstance, isTitleTag, jsonEncode, makeFileNameSafe,
minmax, numberToRoman, openExternalPath, processDialogSymbols,
readTextFile, simplified, transferCase, uniqueCompact, xmlElement,
xmlIndent, xmlSubElem, yesNo
readTextFile, simplified, transferCase, uniqueCompact, utf16CharMap,
xmlElement, xmlIndent, xmlSubElem, yesNo
)
from novelwriter.enum import nwItemClass
from tests.mocked import causeOSError
from tests.tools import writeFile
@@ -556,6 +557,14 @@ def testBaseCommon_encodeDecodeMimeHandles(monkeypatch):
assert decodeMimeHandles(mimeData) == handles
@pytest.mark.base
def testBaseCommon_utf16CharMap(monkeypatch):
"""Test the utf16CharMap function."""
assert utf16CharMap("abc") == [0, 1, 2, 3]
assert utf16CharMap("a\u2014b\u2014c") == [0, 1, 2, 3, 4, 5]
assert utf16CharMap("a\U0001F605b\U0001F605c") == [0, 1, 3, 4, 6, 7]
@pytest.mark.base
def testBaseCommon_jsonEncode():
"""Test the jsonEncode function."""
@@ -804,6 +813,7 @@ def testBaseCommon_NWConfigParser(fncPath):
"list1 = a, b, c\n"
"list2 = 17, 18, 19\n"
"float1 = 4.2\n"
"enum1 = NOVEL\n"
f"path1 = {fncPath}\n"
))
@@ -875,3 +885,7 @@ def testBaseCommon_NWConfigParser(fncPath):
assert cfgParser.rdIntList("nope", "list2", [1]) == [1]
assert cfgParser.rdIntList("main", "blabla", [1]) == [1]
# Read Enum
assert cfgParser.rdEnum("main", "enum1", nwItemClass.NO_CLASS) == nwItemClass.NOVEL
assert cfgParser.rdEnum("main", "blabla", nwItemClass.NO_CLASS) == nwItemClass.NO_CLASS
+4 -4
View File
@@ -131,15 +131,15 @@ def testBaseConfig_InitLoadSave(monkeypatch, fncPath, tstPaths):
assert tstConf.errorText().startswith("Could not load config file")
# Change a few settings, save, reset, and reload
tstConf.guiTheme = "foo"
tstConf.guiSyntax = "bar"
tstConf.lightTheme = "foo"
tstConf.darkTheme = "bar"
assert tstConf.saveConfig() is True
newConf = Config()
newConf.initConfig(confPath=fncPath, dataPath=fncPath)
newConf.loadConfig()
assert newConf.guiTheme == "foo"
assert newConf.guiSyntax == "bar"
assert newConf.lightTheme == "foo"
assert newConf.darkTheme == "bar"
# Test Correcting Quote Settings
tstConf.fmtDQuoteOpen = '"'
+2 -2
View File
@@ -547,8 +547,8 @@ def testCoreItem_ClassDefaults(mockGUI):
def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
"""Test packing and unpacking entries for the NWItem class."""
project = NWProject()
project.data.itemStatus.add(None, "New", (100, 100, 100), "SQUARE", 0)
project.data.itemImport.add(None, "New", (100, 100, 100), "SQUARE", 0)
project.data.itemStatus.add(None, "New", "#646464", "SQUARE", 0)
project.data.itemImport.add(None, "New", "#646464", "SQUARE", 0)
# Invalid
item = NWItem(project, "0000000000000")
+146 -70
View File
@@ -27,12 +27,11 @@ from shutil import copyfile
import pytest
from PyQt6.QtGui import QColor
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
from novelwriter.core.projectdata import NWProjectData
from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState
from novelwriter.core.status import CUSTOM_COL
from novelwriter.enum import nwStatusShape
from tests.mocked import causeOSError
@@ -137,7 +136,7 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, mockGUI, tstPaths, fncPath):
assert xmlReader.state == XMLReadState.PARSED_OK
assert xmlReader.xmlRoot == "novelWriterXML"
assert xmlReader.xmlVersion == 0x0105
assert xmlReader.xmlRevision == 5
assert xmlReader.xmlRevision == 6
assert xmlReader.appVersion == "2.7b1"
assert xmlReader.hexVersion == 0x020700b1
@@ -173,19 +172,31 @@ def testCoreProjectXML_ReadCurrent(monkeypatch, mockGUI, tstPaths, fncPath):
assert data.itemImport["i2d7a54"].name == "Major"
assert data.itemImport["i56be10"].name == "Main"
assert data.itemStatus["sf12341"].color == QColor(100, 100, 100)
assert data.itemStatus["sf24ce6"].color == QColor(200, 50, 0)
assert data.itemStatus["sc24b8f"].color == QColor(182, 60, 0)
assert data.itemStatus["s90e6c9"].color == QColor(193, 129, 0)
assert data.itemStatus["sd51c5b"].color == QColor(193, 129, 0)
assert data.itemStatus["s8ae72a"].color == QColor(193, 129, 0)
assert data.itemStatus["s78ea90"].color == QColor(58, 180, 58)
assert data.itemStatus["sf12341"].color.getRgb() == (100, 100, 100, 255)
assert data.itemStatus["sf24ce6"].color.getRgb() == (200, 50, 0, 255)
assert data.itemStatus["sc24b8f"].color.getRgb() == (182, 60, 0, 255)
assert data.itemStatus["s90e6c9"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["sd51c5b"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s8ae72a"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s78ea90"].color.getRgb() == (58, 180, 58, 255)
assert data.itemImport["ia857f0"].color == QColor(100, 100, 100)
assert data.itemImport["i4a1d39"].color == QColor(220, 138, 221)
assert data.itemImport["icfb3a5"].color == QColor(220, 138, 221)
assert data.itemImport["i2d7a54"].color == QColor(220, 138, 221)
assert data.itemImport["i56be10"].color == QColor(220, 138, 221)
assert data.itemImport["ia857f0"].color.getRgb() == (100, 100, 100, 255)
assert data.itemImport["icfb3a5"].color.getRgb() == (220, 138, 221, 255)
assert data.itemImport["i2d7a54"].color.getRgb() == (220, 138, 221, 255)
assert data.itemImport["i56be10"].color.getRgb() == (220, 138, 221, 255)
assert data.itemStatus["sf12341"].theme == CUSTOM_COL
assert data.itemStatus["sf24ce6"].theme == CUSTOM_COL
assert data.itemStatus["sc24b8f"].theme == CUSTOM_COL
assert data.itemStatus["s90e6c9"].theme == CUSTOM_COL
assert data.itemStatus["sd51c5b"].theme == CUSTOM_COL
assert data.itemStatus["s8ae72a"].theme == CUSTOM_COL
assert data.itemStatus["s78ea90"].theme == CUSTOM_COL
assert data.itemImport["ia857f0"].theme == CUSTOM_COL
assert data.itemImport["icfb3a5"].theme == CUSTOM_COL
assert data.itemImport["i2d7a54"].theme == CUSTOM_COL
assert data.itemImport["i56be10"].theme == CUSTOM_COL
assert data.itemStatus["sf12341"].shape == nwStatusShape.SQUARE
assert data.itemStatus["sf24ce6"].shape == nwStatusShape.SQUARE
@@ -305,18 +316,31 @@ def testCoreProjectXML_ReadLegacy10(tstPaths, fncPath, mockGUI, mockRnd):
assert data.itemImport["i000009"].name == "Major"
assert data.itemImport["i00000a"].name == "Main"
assert data.itemStatus["s000000"].color == QColor(100, 100, 100)
assert data.itemStatus["s000001"].color == QColor(200, 50, 0)
assert data.itemStatus["s000002"].color == QColor(182, 60, 0)
assert data.itemStatus["s000003"].color == QColor(193, 129, 0)
assert data.itemStatus["s000004"].color == QColor(193, 129, 0)
assert data.itemStatus["s000005"].color == QColor(193, 129, 0)
assert data.itemStatus["s000006"].color == QColor(58, 180, 58)
assert data.itemStatus["s000000"].color.getRgb() == (100, 100, 100, 255)
assert data.itemStatus["s000001"].color.getRgb() == (200, 50, 0, 255)
assert data.itemStatus["s000002"].color.getRgb() == (182, 60, 0, 255)
assert data.itemStatus["s000003"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000004"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000005"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000006"].color.getRgb() == (58, 180, 58, 255)
assert data.itemImport["i000007"].color == QColor(100, 100, 100)
assert data.itemImport["i000008"].color == QColor(0, 122, 188)
assert data.itemImport["i000009"].color == QColor(21, 0, 180)
assert data.itemImport["i00000a"].color == QColor(117, 0, 175)
assert data.itemImport["i000007"].color.getRgb() == (100, 100, 100, 255)
assert data.itemImport["i000008"].color.getRgb() == (0, 122, 188, 255)
assert data.itemImport["i000009"].color.getRgb() == (21, 0, 180, 255)
assert data.itemImport["i00000a"].color.getRgb() == (117, 0, 175, 255)
assert data.itemStatus["s000000"].theme == CUSTOM_COL
assert data.itemStatus["s000001"].theme == CUSTOM_COL
assert data.itemStatus["s000002"].theme == CUSTOM_COL
assert data.itemStatus["s000003"].theme == CUSTOM_COL
assert data.itemStatus["s000004"].theme == CUSTOM_COL
assert data.itemStatus["s000005"].theme == CUSTOM_COL
assert data.itemStatus["s000006"].theme == CUSTOM_COL
assert data.itemImport["i000007"].theme == CUSTOM_COL
assert data.itemImport["i000008"].theme == CUSTOM_COL
assert data.itemImport["i000009"].theme == CUSTOM_COL
assert data.itemImport["i00000a"].theme == CUSTOM_COL
assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE
assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE
@@ -450,18 +474,31 @@ def testCoreProjectXML_ReadLegacy11(tstPaths, fncPath, mockGUI, mockRnd):
assert data.itemImport["i000009"].name == "Major"
assert data.itemImport["i00000a"].name == "Main"
assert data.itemStatus["s000000"].color == QColor(100, 100, 100)
assert data.itemStatus["s000001"].color == QColor(200, 50, 0)
assert data.itemStatus["s000002"].color == QColor(182, 60, 0)
assert data.itemStatus["s000003"].color == QColor(193, 129, 0)
assert data.itemStatus["s000004"].color == QColor(193, 129, 0)
assert data.itemStatus["s000005"].color == QColor(193, 129, 0)
assert data.itemStatus["s000006"].color == QColor(58, 180, 58)
assert data.itemStatus["s000000"].color.getRgb() == (100, 100, 100, 255)
assert data.itemStatus["s000001"].color.getRgb() == (200, 50, 0, 255)
assert data.itemStatus["s000002"].color.getRgb() == (182, 60, 0, 255)
assert data.itemStatus["s000003"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000004"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000005"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000006"].color.getRgb() == (58, 180, 58, 255)
assert data.itemImport["i000007"].color == QColor(100, 100, 100)
assert data.itemImport["i000008"].color == QColor(0, 122, 188)
assert data.itemImport["i000009"].color == QColor(21, 0, 180)
assert data.itemImport["i00000a"].color == QColor(117, 0, 175)
assert data.itemImport["i000007"].color.getRgb() == (100, 100, 100, 255)
assert data.itemImport["i000008"].color.getRgb() == (0, 122, 188, 255)
assert data.itemImport["i000009"].color.getRgb() == (21, 0, 180, 255)
assert data.itemImport["i00000a"].color.getRgb() == (117, 0, 175, 255)
assert data.itemStatus["s000000"].theme == CUSTOM_COL
assert data.itemStatus["s000001"].theme == CUSTOM_COL
assert data.itemStatus["s000002"].theme == CUSTOM_COL
assert data.itemStatus["s000003"].theme == CUSTOM_COL
assert data.itemStatus["s000004"].theme == CUSTOM_COL
assert data.itemStatus["s000005"].theme == CUSTOM_COL
assert data.itemStatus["s000006"].theme == CUSTOM_COL
assert data.itemImport["i000007"].theme == CUSTOM_COL
assert data.itemImport["i000008"].theme == CUSTOM_COL
assert data.itemImport["i000009"].theme == CUSTOM_COL
assert data.itemImport["i00000a"].theme == CUSTOM_COL
assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE
assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE
@@ -595,18 +632,31 @@ def testCoreProjectXML_ReadLegacy12(tstPaths, fncPath, mockGUI, mockRnd):
assert data.itemImport["i000009"].name == "Major"
assert data.itemImport["i00000a"].name == "Main"
assert data.itemStatus["s000000"].color == QColor(100, 100, 100)
assert data.itemStatus["s000001"].color == QColor(200, 50, 0)
assert data.itemStatus["s000002"].color == QColor(182, 60, 0)
assert data.itemStatus["s000003"].color == QColor(193, 129, 0)
assert data.itemStatus["s000004"].color == QColor(193, 129, 0)
assert data.itemStatus["s000005"].color == QColor(193, 129, 0)
assert data.itemStatus["s000006"].color == QColor(58, 180, 58)
assert data.itemStatus["s000000"].color.getRgb() == (100, 100, 100, 255)
assert data.itemStatus["s000001"].color.getRgb() == (200, 50, 0, 255)
assert data.itemStatus["s000002"].color.getRgb() == (182, 60, 0, 255)
assert data.itemStatus["s000003"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000004"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000005"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000006"].color.getRgb() == (58, 180, 58, 255)
assert data.itemImport["i000007"].color == QColor(100, 100, 100)
assert data.itemImport["i000008"].color == QColor(0, 122, 188)
assert data.itemImport["i000009"].color == QColor(21, 0, 180)
assert data.itemImport["i00000a"].color == QColor(117, 0, 175)
assert data.itemImport["i000007"].color.getRgb() == (100, 100, 100, 255)
assert data.itemImport["i000008"].color.getRgb() == (0, 122, 188, 255)
assert data.itemImport["i000009"].color.getRgb() == (21, 0, 180, 255)
assert data.itemImport["i00000a"].color.getRgb() == (117, 0, 175, 255)
assert data.itemStatus["s000000"].theme == CUSTOM_COL
assert data.itemStatus["s000001"].theme == CUSTOM_COL
assert data.itemStatus["s000002"].theme == CUSTOM_COL
assert data.itemStatus["s000003"].theme == CUSTOM_COL
assert data.itemStatus["s000004"].theme == CUSTOM_COL
assert data.itemStatus["s000005"].theme == CUSTOM_COL
assert data.itemStatus["s000006"].theme == CUSTOM_COL
assert data.itemImport["i000007"].theme == CUSTOM_COL
assert data.itemImport["i000008"].theme == CUSTOM_COL
assert data.itemImport["i000009"].theme == CUSTOM_COL
assert data.itemImport["i00000a"].theme == CUSTOM_COL
assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE
assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE
@@ -743,18 +793,31 @@ def testCoreProjectXML_ReadLegacy13(tstPaths, fncPath, mockGUI, mockRnd):
assert data.itemImport["i000009"].name == "Major"
assert data.itemImport["i00000a"].name == "Main"
assert data.itemStatus["s000000"].color == QColor(100, 100, 100)
assert data.itemStatus["s000001"].color == QColor(200, 50, 0)
assert data.itemStatus["s000002"].color == QColor(182, 60, 0)
assert data.itemStatus["s000003"].color == QColor(193, 129, 0)
assert data.itemStatus["s000004"].color == QColor(193, 129, 0)
assert data.itemStatus["s000005"].color == QColor(193, 129, 0)
assert data.itemStatus["s000006"].color == QColor(58, 180, 58)
assert data.itemStatus["s000000"].color.getRgb() == (100, 100, 100, 255)
assert data.itemStatus["s000001"].color.getRgb() == (200, 50, 0, 255)
assert data.itemStatus["s000002"].color.getRgb() == (182, 60, 0, 255)
assert data.itemStatus["s000003"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000004"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000005"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s000006"].color.getRgb() == (58, 180, 58, 255)
assert data.itemImport["i000007"].color == QColor(100, 100, 100)
assert data.itemImport["i000008"].color == QColor(0, 122, 188)
assert data.itemImport["i000009"].color == QColor(21, 0, 180)
assert data.itemImport["i00000a"].color == QColor(117, 0, 175)
assert data.itemImport["i000007"].color.getRgb() == (100, 100, 100, 255)
assert data.itemImport["i000008"].color.getRgb() == (0, 122, 188, 255)
assert data.itemImport["i000009"].color.getRgb() == (21, 0, 180, 255)
assert data.itemImport["i00000a"].color.getRgb() == (117, 0, 175, 255)
assert data.itemStatus["s000000"].theme == CUSTOM_COL
assert data.itemStatus["s000001"].theme == CUSTOM_COL
assert data.itemStatus["s000002"].theme == CUSTOM_COL
assert data.itemStatus["s000003"].theme == CUSTOM_COL
assert data.itemStatus["s000004"].theme == CUSTOM_COL
assert data.itemStatus["s000005"].theme == CUSTOM_COL
assert data.itemStatus["s000006"].theme == CUSTOM_COL
assert data.itemImport["i000007"].theme == CUSTOM_COL
assert data.itemImport["i000008"].theme == CUSTOM_COL
assert data.itemImport["i000009"].theme == CUSTOM_COL
assert data.itemImport["i00000a"].theme == CUSTOM_COL
assert data.itemStatus["s000000"].shape == nwStatusShape.SQUARE
assert data.itemStatus["s000001"].shape == nwStatusShape.SQUARE
@@ -891,18 +954,31 @@ def testCoreProjectXML_ReadLegacy14(tstPaths, fncPath, mockGUI, mockRnd):
assert data.itemImport["i2d7a54"].name == "Major"
assert data.itemImport["i56be10"].name == "Main"
assert data.itemStatus["sf12341"].color == QColor(100, 100, 100)
assert data.itemStatus["sf24ce6"].color == QColor(200, 50, 0)
assert data.itemStatus["sc24b8f"].color == QColor(182, 60, 0)
assert data.itemStatus["s90e6c9"].color == QColor(193, 129, 0)
assert data.itemStatus["sd51c5b"].color == QColor(193, 129, 0)
assert data.itemStatus["s8ae72a"].color == QColor(193, 129, 0)
assert data.itemStatus["s78ea90"].color == QColor(58, 180, 58)
assert data.itemStatus["sf12341"].color.getRgb() == (100, 100, 100, 255)
assert data.itemStatus["sf24ce6"].color.getRgb() == (200, 50, 0, 255)
assert data.itemStatus["sc24b8f"].color.getRgb() == (182, 60, 0, 255)
assert data.itemStatus["s90e6c9"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["sd51c5b"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s8ae72a"].color.getRgb() == (193, 129, 0, 255)
assert data.itemStatus["s78ea90"].color.getRgb() == (58, 180, 58, 255)
assert data.itemImport["ia857f0"].color == QColor(100, 100, 100)
assert data.itemImport["icfb3a5"].color == QColor(0, 122, 188)
assert data.itemImport["i2d7a54"].color == QColor(21, 0, 180)
assert data.itemImport["i56be10"].color == QColor(117, 0, 175)
assert data.itemImport["ia857f0"].color.getRgb() == (100, 100, 100, 255)
assert data.itemImport["icfb3a5"].color.getRgb() == (0, 122, 188, 255)
assert data.itemImport["i2d7a54"].color.getRgb() == (21, 0, 180, 255)
assert data.itemImport["i56be10"].color.getRgb() == (117, 0, 175, 255)
assert data.itemStatus["sf12341"].theme == CUSTOM_COL
assert data.itemStatus["sf24ce6"].theme == CUSTOM_COL
assert data.itemStatus["sc24b8f"].theme == CUSTOM_COL
assert data.itemStatus["s90e6c9"].theme == CUSTOM_COL
assert data.itemStatus["sd51c5b"].theme == CUSTOM_COL
assert data.itemStatus["s8ae72a"].theme == CUSTOM_COL
assert data.itemStatus["s78ea90"].theme == CUSTOM_COL
assert data.itemImport["ia857f0"].theme == CUSTOM_COL
assert data.itemImport["icfb3a5"].theme == CUSTOM_COL
assert data.itemImport["i2d7a54"].theme == CUSTOM_COL
assert data.itemImport["i56be10"].theme == CUSTOM_COL
assert data.itemStatus["sf12341"].shape == nwStatusShape.SQUARE
assert data.itemStatus["sf24ce6"].shape == nwStatusShape.SQUARE
+97 -49
View File
@@ -24,7 +24,7 @@ import pytest
from PyQt6.QtGui import QColor, QIcon
from novelwriter.core.status import NWStatus, StatusEntry, _ShapeCache
from novelwriter.core.status import CUSTOM_COL, NWStatus, StatusEntry, _ShapeCache
from novelwriter.enum import nwStatusShape
from tests.tools import C
@@ -38,11 +38,12 @@ def testCoreStatus_StatusEntry():
"""Test the StatusEntry class."""
color = QColor(255, 0, 0)
icon = NWStatus.createIcon(24, color, nwStatusShape.CIRCLE)
entry = StatusEntry("Test", color, nwStatusShape.CIRCLE, icon, 42)
entry = StatusEntry("Test", color, CUSTOM_COL, nwStatusShape.CIRCLE, icon, 42)
# Check values
assert entry.name == "Test"
assert entry.color is color
assert entry.theme == CUSTOM_COL
assert entry.shape == nwStatusShape.CIRCLE
assert entry.icon is icon
assert entry.count == 42
@@ -55,6 +56,7 @@ def testCoreStatus_StatusEntry():
assert other.name == "Test"
assert other.color is not color # Not the same object
assert other.color == color # But same colours
assert entry.theme == CUSTOM_COL
assert other.shape == nwStatusShape.CIRCLE
assert other.icon is not icon # Not the same icon, but a copy
assert other.count == 42
@@ -73,14 +75,14 @@ def testCoreStatus_Internal(mockGUI, mockRnd):
assert nStatus._newKey() == statusKeys[1]
# Key collision, should move to key 3
nStatus.add(statusKeys[2], "Crash", (0, 0, 0), "SQUARE", 0)
nStatus.add(statusKeys[2], "Crash", "#000000", "SQUARE", 0)
assert nStatus._newKey() == statusKeys[3]
assert nImport._newKey() == importKeys[0]
assert nImport._newKey() == importKeys[1]
# Key collision, should move to key 3
nImport.add(importKeys[2], "Crash", (0, 0, 0), "SQUARE", 0)
nImport.add(importKeys[2], "Crash", "#000000", "SQUARE", 0)
assert nImport._newKey() == importKeys[3]
# Check Key
@@ -119,14 +121,14 @@ def testCoreStatus_Internal(mockGUI, mockRnd):
def testCoreStatus_Iterator(mockGUI, mockRnd):
"""Test the iterator functions of the NWStatus class."""
nStatus = NWStatus(NWStatus.STATUS)
nStatus.add(None, "New", (100, 100, 100), "SQUARE", 0)
nStatus.add(None, "Note", (200, 50, 0), "CIRCLE", 1)
nStatus.add(None, "Draft", (200, 150, 0), "SQUARE", 2)
nStatus.add(None, "Finished", (50, 200, 0), "CIRCLE", 3)
nStatus.add(None, "New", "#646464", "SQUARE", 0)
nStatus.add(None, "Note", "#ff3f00", "CIRCLE", 1)
nStatus.add(None, "Draft", "#ffaf00", "SQUARE", 2)
nStatus.add(None, "Finished", "#3fff00", "CIRCLE", 3)
# Direct access
entry = nStatus[statusKeys[0]]
assert entry.color == QColor(100, 100, 100)
assert entry.color.getRgb() == (100, 100, 100, 255)
assert entry.name == "New"
assert entry.count == 0
assert isinstance(entry.icon, QIcon)
@@ -146,8 +148,8 @@ def testCoreStatus_Iterator(mockGUI, mockRnd):
]
# Content : Colours
assert [e.color for _, e in nStatus.iterItems()] == [
QColor(100, 100, 100), QColor(200, 50, 0), QColor(200, 150, 0), QColor(50, 200, 0)
assert [e.color.getRgb() for _, e in nStatus.iterItems()] == [
(100, 100, 100, 255), (255, 63, 0, 255), (255, 175, 0, 255), (63, 255, 0, 255)
]
# Content : Shape
@@ -168,27 +170,31 @@ def testCoreStatus_Entries(mockGUI, mockRnd):
# ===
# Has a key
nStatus.add(statusKeys[0], "Entry 1", (200, 100, 50), "SQUARE", 0)
nStatus.add(statusKeys[0], "Entry 1", "200, 100, 50", "SQUARE", 0)
assert nStatus[statusKeys[0]].name == "Entry 1"
assert nStatus[statusKeys[0]].color == QColor(200, 100, 50)
assert nStatus[statusKeys[0]].color.getRgb() == (200, 100, 50, 255)
assert nStatus[statusKeys[0]].theme == CUSTOM_COL
assert nStatus[statusKeys[0]].shape == nwStatusShape.SQUARE
# Doesn't have a key
nStatus.add(None, "Entry 2", (210, 110, 60), "SQUARE", 0)
nStatus.add(None, "Entry 2", "210, 110, 60", "SQUARE", 0)
assert nStatus[statusKeys[1]].name == "Entry 2"
assert nStatus[statusKeys[1]].color == QColor(210, 110, 60)
assert nStatus[statusKeys[1]].color.getRgb() == (210, 110, 60, 255)
assert nStatus[statusKeys[1]].theme == CUSTOM_COL
assert nStatus[statusKeys[1]].shape == nwStatusShape.SQUARE
# Wrong colour spec, unknown shape
nStatus.add(None, "Entry 3", "what?", "", 0) # type: ignore
nStatus.add(None, "Entry 3", "what?", "", 0)
assert nStatus[statusKeys[2]].name == "Entry 3"
assert nStatus[statusKeys[2]].color == QColor(100, 100, 100)
assert nStatus[statusKeys[2]].color.getRgb() == (0, 0, 0, 255)
assert nStatus[statusKeys[2]].theme == CUSTOM_COL
assert nStatus[statusKeys[2]].shape == nwStatusShape.SQUARE
# Wrong colour count
nStatus.add(None, "Entry 4", (10, 20), "CIRCLE", 0) # type: ignore
# Wrong colour definition
nStatus.add(None, "Entry 4", "#stuff#", "CIRCLE", 0)
assert nStatus[statusKeys[3]].name == "Entry 4"
assert nStatus[statusKeys[3]].color == QColor(100, 100, 100)
assert nStatus[statusKeys[3]].color.getRgb() == (0, 0, 0, 255)
assert nStatus[statusKeys[3]].theme == CUSTOM_COL
assert nStatus[statusKeys[3]].shape == nwStatusShape.CIRCLE
# Check
@@ -202,8 +208,6 @@ def testCoreStatus_Entries(mockGUI, mockRnd):
assert nStatus.check("s987654") == statusKeys[0]
# Name Access
# ===========
assert nStatus[statusKeys[0]].name == "Entry 1"
assert nStatus[statusKeys[1]].name == "Entry 2"
assert nStatus[statusKeys[2]].name == "Entry 3"
@@ -211,17 +215,20 @@ def testCoreStatus_Entries(mockGUI, mockRnd):
assert nStatus["blablabla"].name == "Entry 1"
# Colour Access
# =============
assert nStatus[statusKeys[0]].color.getRgb() == (200, 100, 50, 255)
assert nStatus[statusKeys[1]].color.getRgb() == (210, 110, 60, 255)
assert nStatus[statusKeys[2]].color.getRgb() == (0, 0, 0, 255)
assert nStatus[statusKeys[3]].color.getRgb() == (0, 0, 0, 255)
assert nStatus["blablabla"].color.getRgb() == (200, 100, 50, 255)
assert nStatus[statusKeys[0]].color == QColor(200, 100, 50)
assert nStatus[statusKeys[1]].color == QColor(210, 110, 60)
assert nStatus[statusKeys[2]].color == QColor(100, 100, 100)
assert nStatus[statusKeys[3]].color == QColor(100, 100, 100)
assert nStatus["blablabla"].color == QColor(200, 100, 50)
# Theme Access
assert nStatus[statusKeys[0]].theme == CUSTOM_COL
assert nStatus[statusKeys[1]].theme == CUSTOM_COL
assert nStatus[statusKeys[2]].theme == CUSTOM_COL
assert nStatus[statusKeys[3]].theme == CUSTOM_COL
assert nStatus["blablabla"].theme == CUSTOM_COL
# Icon Access
# ===========
assert isinstance(nStatus[statusKeys[0]].icon, QIcon)
assert isinstance(nStatus[statusKeys[1]].icon, QIcon)
assert isinstance(nStatus[statusKeys[2]].icon, QIcon)
@@ -229,8 +236,6 @@ def testCoreStatus_Entries(mockGUI, mockRnd):
assert isinstance(nStatus["blablabla"].icon, QIcon)
# Shape Access
# ============
assert nStatus[statusKeys[0]].shape == nwStatusShape.SQUARE
assert nStatus[statusKeys[1]].shape == nwStatusShape.SQUARE
assert nStatus[statusKeys[2]].shape == nwStatusShape.SQUARE
@@ -319,13 +324,64 @@ def testCoreStatus_Entries(mockGUI, mockRnd):
@pytest.mark.core
def testCoreStatus_Pack(mockGUI, mockRnd):
def testCoreStatus_RefreshIcons_Theme(mockGUIwithTheme, mockRnd):
"""Test refreshing the icons with theme colours."""
nStatus = NWStatus(NWStatus.STATUS)
nStatus.add(None, "New", "default", "SQUARE", 0)
nStatus.add(None, "Note", "red", "CIRCLE", 0)
nStatus.add(None, "Draft", "yellow", "SQUARE", 0)
nStatus.add(None, "Finished", "green", "SQUARE", 0)
iconsA = [nStatus[statusKeys[i]].icon for i in range(4)]
themeA = [nStatus[statusKeys[i]].theme for i in range(4)]
# Refreshing the icons should generate new ones
nStatus.refreshIcons()
iconsB = [nStatus[statusKeys[i]].icon for i in range(4)]
themeB = [nStatus[statusKeys[i]].theme for i in range(4)]
for before, after in zip(iconsA, iconsB, strict=False):
assert before is not after
assert themeA == themeB
@pytest.mark.core
def testCoreStatus_RefreshIcons_Custom(mockGUIwithTheme, mockRnd):
"""Test refreshing the icons with custom colours."""
nStatus = NWStatus(NWStatus.STATUS)
nStatus.add(None, "New", "#707070", "SQUARE", 0)
nStatus.add(None, "Note", "#ff0000", "CIRCLE", 0)
nStatus.add(None, "Draft", "#ffff00", "SQUARE", 0)
nStatus.add(None, "Finished", "#00ff00", "SQUARE", 0)
iconsA = [nStatus[statusKeys[i]].icon for i in range(4)]
themeA = [nStatus[statusKeys[i]].theme for i in range(4)]
colorA = [nStatus[statusKeys[i]].color.getRgb() for i in range(4)]
# Refreshing the icons should generate new ones
nStatus.refreshIcons()
iconsB = [nStatus[statusKeys[i]].icon for i in range(4)]
themeB = [nStatus[statusKeys[i]].theme for i in range(4)]
colorB = [nStatus[statusKeys[i]].color.getRgb() for i in range(4)]
for before, after in zip(iconsA, iconsB, strict=False):
assert before is not after
# But they should have the same colour value (#2452)
assert themeA == [CUSTOM_COL, CUSTOM_COL, CUSTOM_COL, CUSTOM_COL]
assert themeB == [CUSTOM_COL, CUSTOM_COL, CUSTOM_COL, CUSTOM_COL]
assert colorA == colorB
@pytest.mark.core
def testCoreStatus_Pack(mockGUIwithTheme, mockRnd):
"""Test data packing of the NWStatus class."""
nStatus = NWStatus(NWStatus.STATUS)
nStatus.add(None, "New", (100, 100, 100), "SQUARE", 0)
nStatus.add(None, "Note", (200, 50, 0), "CIRCLE", 0)
nStatus.add(None, "Draft", (200, 150, 0), "SQUARE", 0)
nStatus.add(None, "Finished", (50, 200, 0), "SQUARE", 0)
nStatus.add(None, "New", "#646464", "SQUARE", 0)
nStatus.add(None, "Note", "#c83200", "CIRCLE", 0)
nStatus.add(None, "Draft", "#c89600", "SQUARE", 0)
nStatus.add(None, "Finished", "default", "SQUARE", 0)
countTo = [3, 5, 7, 9]
for i, n in enumerate(countTo):
@@ -337,33 +393,25 @@ def testCoreStatus_Pack(mockGUI, mockRnd):
("New", {
"key": statusKeys[0],
"count": "3",
"red": "100",
"green": "100",
"blue": "100",
"color": "#646464",
"shape": "SQUARE",
}),
("Note", {
"key": statusKeys[1],
"count": "5",
"red": "200",
"green": "50",
"blue": "0",
"color": "#c83200",
"shape": "CIRCLE",
}),
("Draft", {
"key": statusKeys[2],
"count": "7",
"red": "200",
"green": "150",
"blue": "0",
"color": "#c89600",
"shape": "SQUARE",
}),
("Finished", {
"key": statusKeys[3],
"count": "9",
"red": "50",
"green": "200",
"blue": "0",
"color": "default",
"shape": "SQUARE",
}),
]
+26 -20
View File
@@ -27,10 +27,11 @@ from PyQt6.QtGui import QAction, QFont, QFontDatabase, QKeyEvent
from PyQt6.QtWidgets import QFileDialog, QFontDialog
from novelwriter import CONFIG, SHARED
from novelwriter.config import DEF_GUI
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_TREECOL
from novelwriter.constants import nwUnicode
from novelwriter.dialogs.preferences import GuiPreferences
from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.gui.theme import ThemeEntry
from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone
KEY_DELAY = 1
@@ -55,15 +56,11 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
assert "en_GB" in languages
# Check GUI Themes
themes = [prefs.guiTheme.itemData(i) for i in range(prefs.guiTheme.count())]
assert len(themes) >= 5
assert DEF_GUI in themes
themes = [prefs.lightTheme.itemData(i) for i in range(prefs.lightTheme.count())]
assert DEF_GUI_LIGHT in themes
# Check GUI Syntax
syntax = [prefs.guiSyntax.itemData(i) for i in range(prefs.guiSyntax.count())]
assert len(syntax) >= 10
assert "default_dark" in syntax
assert "default_light" in syntax
themes = [prefs.darkTheme.itemData(i) for i in range(prefs.darkTheme.count())]
assert DEF_GUI_DARK in themes
# Check Spell Checking
spelling = [prefs.spellLanguage.itemData(i) for i in range(prefs.spellLanguage.count())]
@@ -124,7 +121,7 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI):
button = prefs.buttonBox.button(QtDialogSave)
assert button is not None
button.click()
assert signal.args == [False, False, False, False]
assert len(signal.args) == 4
# Check Close Button
prefs.show()
@@ -151,6 +148,12 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
(fncPath / "nw_en_US.qm").touch()
(fncPath / "project_en_US.json").touch()
CONFIG._nwLangPath = fncPath
SHARED.theme._allThemes = {
"theme1": ThemeEntry(name="Theme 1", dark=False, path=fncPath),
"theme2": ThemeEntry(name="Theme 2", dark=False, path=fncPath),
"theme3": ThemeEntry(name="Theme 3", dark=True, path=fncPath),
"theme4": ThemeEntry(name="Theme 4", dark=True, path=fncPath),
}
prefs = GuiPreferences(nwGUI)
with qtbot.waitExposed(prefs):
@@ -158,7 +161,8 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
# Appearance
prefs.guiLocale.setCurrentIndex(prefs.guiLocale.findData("en_US"))
prefs.guiTheme.setCurrentIndex(prefs.guiTheme.findData("default_dark"))
prefs.lightTheme.setCurrentIndex(prefs.lightTheme.findData("theme1"))
prefs.darkTheme.setCurrentIndex(prefs.darkTheme.findData("theme3"))
with monkeypatch.context() as mp:
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(True) # Use OS font dialog
@@ -169,14 +173,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.useCharCount.setChecked(True)
assert CONFIG.guiLocale != "en_US"
assert CONFIG.guiTheme != "default_dark"
assert CONFIG.lightTheme == "default_light"
assert CONFIG.darkTheme == "default_dark"
assert CONFIG.guiFont.family() != ""
assert CONFIG.hideVScroll is False
assert CONFIG.hideHScroll is False
assert CONFIG.useCharCount is False
# Document Style
prefs.guiSyntax.setCurrentIndex(prefs.guiSyntax.findData("default_dark"))
with monkeypatch.context() as mp:
mp.setattr(QFontDialog, "getFont", lambda *a, **k: (QFont(), True))
prefs.nativeFont.setChecked(False) # Use Qt font dialog
@@ -185,7 +189,6 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.showFullPath.setChecked(False)
prefs.incNotesWCount.setChecked(False)
assert CONFIG.guiSyntax != "default_dark"
assert CONFIG.textFont.family() != ""
assert CONFIG.showFullPath is True
assert CONFIG.incNotesWCount is True
@@ -195,7 +198,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.iconColDocs.setChecked(True)
prefs.emphLabels.setChecked(False)
assert CONFIG.iconColTree == "theme"
assert CONFIG.iconColTree == DEF_TREECOL
assert CONFIG.iconColDocs is False
assert CONFIG.emphLabels is True
@@ -247,22 +250,24 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
prefs.spellLanguage.setCurrentIndex(prefs.spellLanguage.findData("de"))
prefs.autoSelect.setChecked(False)
prefs.cursorWidth.setValue(5)
prefs.lineHighlight.setChecked(False)
prefs.showTabsNSpaces.setChecked(True)
prefs.showLineEndings.setChecked(True)
assert CONFIG.spellLanguage != "de"
assert CONFIG.autoSelect is True
assert CONFIG.cursorWidth == 1
assert CONFIG.lineHighlight is True
assert CONFIG.showTabsNSpaces is False
assert CONFIG.showLineEndings is False
# Editor Scrolling
prefs.scrollPastEnd.setChecked(False)
prefs.autoScroll.setChecked(True)
prefs.autoScroll.setChecked(False)
prefs.autoScrollPos.stepUp()
assert CONFIG.scrollPastEnd is True
assert CONFIG.autoScroll is False
assert CONFIG.autoScroll is True
assert CONFIG.autoScrollPos == 30
# Text Highlighting
@@ -347,14 +352,14 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
# Appearance
assert CONFIG.guiLocale == "en_US"
assert CONFIG.guiTheme == "default_dark"
assert CONFIG.lightTheme == "theme1"
assert CONFIG.darkTheme == "theme3"
assert CONFIG.guiFont == QFont()
assert CONFIG.hideVScroll is True
assert CONFIG.hideHScroll is True
assert CONFIG.useCharCount is True
# Document Style
assert CONFIG.guiSyntax == "default_dark"
assert CONFIG.textFont == QFont()
assert CONFIG.showFullPath is False
assert CONFIG.incNotesWCount is False
@@ -390,12 +395,13 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths):
assert CONFIG.spellLanguage == "de"
assert CONFIG.autoSelect is False
assert CONFIG.cursorWidth == 5
assert CONFIG.lineHighlight is False
assert CONFIG.showTabsNSpaces is True
assert CONFIG.showLineEndings is True
# Editor Scrolling
assert CONFIG.scrollPastEnd is False
assert CONFIG.autoScroll is True
assert CONFIG.autoScroll is False
assert CONFIG.autoScrollPos == 31
# Text Highlighting
+12 -12
View File
@@ -214,22 +214,22 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn
assert update[0][0] == C.sNew
assert update[0][1].name == "New"
assert update[0][1].color == QColor(120, 120, 120)
assert update[0][1].color.getRgb() == (108, 108, 108, 255)
assert update[0][1].shape == nwStatusShape.STAR
assert update[1][0] == C.sDraft
assert update[1][1].name == "Draft"
assert update[1][1].color == QColor(143, 240, 164)
assert update[1][1].color.getRgb() == (163, 156, 52, 255)
assert update[1][1].shape == nwStatusShape.CIRCLE_T
assert update[2][0] == C.sFinished
assert update[2][1].name == "Finished"
assert update[2][1].color == QColor(249, 240, 107)
assert update[2][1].color.getRgb() == (41, 102, 41, 255)
assert update[2][1].shape == nwStatusShape.STAR
assert update[3][0] is None
assert update[3][1].name == "Final"
assert update[3][1].color == QColor(20, 30, 40)
assert update[3][1].color.getRgb() == (20, 30, 40, 255)
assert update[3][1].shape == nwStatusShape.CIRCLE
# Move items, none selected -> no change
@@ -289,22 +289,22 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, projPath, mockRn
assert update[0][0] == C.iNew
assert update[0][1].name == "New"
assert update[0][1].color == QColor(120, 120, 120)
assert update[0][1].color.getRgb() == (179, 90, 179, 255)
assert update[0][1].shape == nwStatusShape.SQUARE
assert update[1][0] == C.iMajor
assert update[1][1].name == "Major"
assert update[1][1].color == QColor(220, 138, 221)
assert update[1][1].color.getRgb() == (179, 90, 179, 255)
assert update[1][1].shape == nwStatusShape.BLOCK_3
assert update[2][0] == C.iMain
assert update[2][1].name == "Main"
assert update[2][1].color == QColor(220, 138, 221)
assert update[2][1].color.getRgb() == (179, 90, 179, 255)
assert update[2][1].shape == nwStatusShape.BLOCK_4
assert update[3][0] is None
assert update[3][1].name == "Final"
assert update[3][1].color == QColor(20, 30, 40)
assert update[3][1].color.getRgb() == (20, 30, 40, 255)
assert update[3][1].shape == nwStatusShape.TRIANGLE
# Check Project
@@ -354,10 +354,10 @@ def testDlgProjSettings_StatusImportExport(qtbot, monkeypatch, nwGUI, projPath,
assert expFile.is_file() is True
assert expFile.read_text().split() == [
"STAR,#787878,New",
"TRIANGLE,#cdab8f,Note",
"CIRCLE_T,#8ff0a4,Draft",
"STAR,#f9f06b,Finished",
"STAR,#6c6c6c,New",
"TRIANGLE,#a62a2d,Note",
"CIRCLE_T,#a39c34,Draft",
"STAR,#296629,Finished",
]
# Import Error
+85 -24
View File
@@ -222,12 +222,24 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
"</p>\n"
)
# Keywords
@pytest.mark.core
def testFmtToHtml_ConvertMeta(mockGUI):
"""Test meta data formats in the ToHtml class."""
project = NWProject()
html = ToHtml(project)
html.initDocument()
html._isNovel = True
html._isFirst = True
# Keywords Disabled
html.setKeywords(False)
html._text = "@char: Bod, Jane\n"
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == ""
# Keywords Enabled
html.setKeywords(True)
html._text = "@char: Bod, Jane\n"
html.tokenizeText()
@@ -239,7 +251,8 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
"<span style='color: #4271ae'><a href='#tag_jane'>Jane</a></span></p>\n"
)
# Tags
# Tags w/Colour
html.setStyles(True)
html._text = "@tag: Bod\n"
html.tokenizeText()
html.doConvert()
@@ -257,7 +270,25 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
"<span style='color: #4271ae'>Nobody Owens</span></p>\n"
)
# Multiple Keywords
# Tags wo/Colour
html.setStyles(False)
html._text = "@tag: Bod\n"
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == (
"<p class='meta meta-tag'><strong>Tag:</strong> <a name='tag_bod'>Bod</a></p>\n"
)
html._text = "@tag: Bod | Nobody Owens\n"
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == (
"<p class='meta meta-tag'><strong>Tag:</strong> "
"<a name='tag_bod'>Bod</a> | Nobody Owens</p>\n"
)
# Multiple Keywords w/Colour
html.setStyles(True)
html._isFirst = False
html.setKeywords(True)
html._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
@@ -276,6 +307,23 @@ def testFmtToHtml_ConvertParagraphs(mockGUI):
"<span style='color: #4271ae'><a href='#tag_europe'>Europe</a></span></p>\n"
)
# Multiple Keywords wo/Colour
html.setStyles(False)
html._isFirst = False
html.setKeywords(True)
html._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
html.tokenizeText()
html.doConvert()
assert html._pages[-1] == (
"<h1 style='page-break-before: always;'>Chapter</h1>\n"
"<p class='meta meta-pov' style='margin-bottom: 0;'>"
"<strong>Point of View:</strong> <a href='#tag_bod'>Bod</a></p>\n"
"<p class='meta meta-plot' style='margin-bottom: 0; margin-top: 0;'>"
"<strong>Plot:</strong> <a href='#tag_main'>Main</a></p>\n"
"<p class='meta meta-location' style='margin-top: 0;'>"
"<strong>Locations:</strong> <a href='#tag_europe'>Europe</a></p>\n"
)
@pytest.mark.core
def testFmtToHtml_Alignment(mockGUI):
@@ -485,17 +533,6 @@ def testFmtToHtml_ConvertDirect(mockGUI):
# =========
html.setLinkHeadings(False)
# Align Left
html.setStyles(False)
html._blocks = [
(BlockTyp.PART, tMeta, "A Title", [], BlockFmt.LEFT),
]
html.doConvert()
assert html._pages[-1] == (
"<h1 class='title'>A Title</h1>\n"
)
html.setStyles(True)
# Align Left
@@ -709,33 +746,57 @@ def testFmtToHtml_Save(mockGUI, fncPath):
# Check Files
# ===========
# HTML
hStyle = html.getStyleSheet()
htmlDoc = (
# HTML w/Styles
html.setStyles(True)
htmlDocCSS = (
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<meta charset='utf-8'>\n"
"<title></title>\n"
"<meta charset='utf-8'>\n"
"<meta name='viewport' content='width=device-width, initial-scale=1'>\n"
"<style>\n"
"{htmlStyle:s}\n"
"{style}\n"
"</style>\n"
"</head>\n"
"<body>\n"
"{bodyText:s}\n"
"{body}\n"
"</body>\n"
"</html>\n"
).format(
htmlStyle="\n".join(hStyle),
bodyText="".join(resText).rstrip()
style="\n".join(hStyle),
body="".join(resText).rstrip()
)
saveFile = fncPath / "outFile.htm"
html.saveDocument(saveFile)
assert saveFile.read_text(encoding="utf-8") == htmlDoc
assert saveFile.read_text(encoding="utf-8") == htmlDocCSS
# JSON + HTML
# HTML wo/Styles
html.setStyles(False)
htmlDocCSS = (
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<title></title>\n"
"<meta charset='utf-8'>\n"
"</head>\n"
"<body>\n"
"{body}\n"
"</body>\n"
"</html>\n"
).format(
body="".join(resText).rstrip()
)
saveFile = fncPath / "outFile.htm"
html.saveDocument(saveFile)
assert saveFile.read_text(encoding="utf-8") == htmlDocCSS
# JSON + HTML w/Styles
html.setStyles(True)
saveFile = fncPath / "outFile.json"
html.saveDocument(saveFile)
data = json.loads(saveFile.read_text(encoding="utf-8"))
+47 -3
View File
@@ -783,7 +783,7 @@ def testFmtToken_MetaFormat(mockGUI):
tokens._text = "% synopsis: The synopsis\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.COMMENT, "", "Synopsis: The synopsis", [
BlockTyp.SUMMARY, "", "Synopsis: The synopsis", [
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
(9, TextFmt.COL_E, ""), (9, TextFmt.B_E, ""),
(10, TextFmt.COL_B, "note"), (22, TextFmt.COL_E, "")
@@ -800,13 +800,52 @@ def testFmtToken_MetaFormat(mockGUI):
tokens._text = "% short: A short description\n"
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.COMMENT, "", "Short Description: A short description", [
BlockTyp.SUMMARY, "", "Short Description: A short description", [
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
(18, TextFmt.COL_E, ""), (18, TextFmt.B_E, ""),
(19, TextFmt.COL_B, "note"), (38, TextFmt.COL_E, ""),
], BlockFmt.NONE
)]
# Story Comments
tokens.setCommentType(nwComment.STORY, False)
tokens.setCommentType(nwComment.NOTE, False)
tokens._text = (
"%Story.Stuff: Stuff happens\n"
"%Note.More: That stuff that happened\n"
"%Note.Other: That other stuff that happened\n"
)
tokens.tokenizeText()
assert tokens._blocks == []
tokens.setCommentType(nwComment.STORY, True)
tokens.setCommentType(nwComment.NOTE, True)
tokens._text = (
"%Story.Stuff: Stuff happens\n"
"%Note.More: That stuff that happened\n"
"%Note.Other: That other stuff that happened\n"
)
tokens.tokenizeText()
assert tokens._blocks == [(
BlockTyp.NOTE, "", "Story Structure (Stuff): Stuff happens", [
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
(24, TextFmt.COL_E, ""), (24, TextFmt.B_E, ""),
(25, TextFmt.COL_B, "note"), (38, TextFmt.COL_E, "")
], BlockFmt.Z_BTM
), (
BlockTyp.NOTE, "", "Note (More): That stuff that happened", [
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
(12, TextFmt.COL_E, ""), (12, TextFmt.B_E, ""),
(13, TextFmt.COL_B, "note"), (37, TextFmt.COL_E, "")
], BlockFmt.Z_TOP | BlockFmt.Z_BTM
), (
BlockTyp.NOTE, "", "Note (Other): That other stuff that happened", [
(0, TextFmt.B_B, ""), (0, TextFmt.COL_B, "modifier"),
(13, TextFmt.COL_E, ""), (13, TextFmt.B_E, ""),
(14, TextFmt.COL_B, "note"), (44, TextFmt.COL_E, "")
], BlockFmt.Z_TOP
)]
# Keyword
tokens.setKeywords(False)
tokens._text = "@char: Bod\n"
@@ -919,6 +958,11 @@ def testFmtToken_ExtractFormats(mockGUI):
assert text == "Text with strikethrough in it."
assert fmt == [(10, TextFmt.D_B, ""), (23, TextFmt.D_E, "")]
# Plain mark
text, fmt = tokens._extractFormats("Text with ==a highlight== in it.")
assert text == "Text with a highlight in it."
assert fmt == [(10, TextFmt.M_B, ""), (21, TextFmt.M_E, "")]
# Nested bold/italics
text, fmt = tokens._extractFormats("Text with **bold and _italics_** in it.")
assert text == "Text with bold and italics in it."
@@ -1632,7 +1676,7 @@ def testFmtToken_TextIndent(mockGUI):
]
assert tokens._blocks == [
(BlockTyp.HEAD3, TM1, "Scene Two", [], BlockFmt.NONE),
(BlockTyp.COMMENT, "", "Synopsis: Stuff happens.", tFmt, BlockFmt.NONE),
(BlockTyp.SUMMARY, "", "Synopsis: Stuff happens.", tFmt, BlockFmt.NONE),
]
assert tokens._noIndent is True
+42 -31
View File
@@ -514,32 +514,36 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
# Run SpellCheck
# ==============
SHARED.project.data.setSpellCheck(True)
LORAX = "Lorax\U0001F03A"
cursor = docEditor.textCursor()
cursor.setPosition(16)
data = cursor.block().userData()
assert cursor.block().text().startswith("Lorem")
assert isinstance(data, TextBlockData)
data._spellErrors = [(0, 5)]
data._spellErrors = [(0, 5, "Lorem")]
# No known position
assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, -1, [])
assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, [])
# With Suggestion
with monkeypatch.context() as mp:
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: ["Lorax"])
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [LORAX])
ctxMenu = getMenuForPos(docEditor, 16)
assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert "Spelling Suggestion(s)" in actions
assert f"{nwUnicode.U_ENDASH} Lorax" in actions
assert f"{nwUnicode.U_ENDASH} {LORAX}" in actions
ctxMenu.actions()[7].trigger()
QApplication.processEvents()
assert docEditor.getText() == text.replace("Lorem", "Lorax", 1)
assert docEditor.getText() == text.replace("Lorem", LORAX, 1)
ctxMenu.setObjectName("")
ctxMenu.deleteLater()
# Update Entry
data._spellErrors = [(0, 7, LORAX)]
# Without Suggestion
with monkeypatch.context() as mp:
mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [])
@@ -548,7 +552,7 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
assert ctxMenu is not None
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert f"{nwUnicode.U_ENDASH} No Suggestions" in actions
assert docEditor.getText() == text.replace("Lorem", "Lorax", 1)
assert docEditor.getText() == text.replace("Lorem", LORAX, 1)
ctxMenu.setObjectName("")
ctxMenu.deleteLater()
@@ -562,11 +566,11 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText,
assert "Ignore Word" in actions
assert "Add Word to Dictionary" in actions
assert "Lorax" not in SHARED.spelling._userDict
assert LORAX not in SHARED.spelling._userDict
ctxMenu.actions()[7].trigger() # Ignore
assert "Lorax" not in SHARED.spelling._userDict
assert LORAX not in SHARED.spelling._userDict
ctxMenu.actions()[8].trigger() # Add
assert "Lorax" in SHARED.spelling._userDict
assert LORAX in SHARED.spelling._userDict
ctxMenu.setObjectName("")
ctxMenu.deleteLater()
@@ -674,9 +678,16 @@ def testGuiEditor_Actions(qtbot, nwGUI, projPath, ipsumText, mockRnd):
assert docEditor.docAction(nwDocAction.UNDO) is True
assert docEditor.getText() == text
# Mark
docEditor.setCursorPosition(50)
assert docEditor.docAction(nwDocAction.MD_MARK) is True
assert docEditor.getText() == text.replace("consectetur", "==consectetur==")
assert docEditor.docAction(nwDocAction.UNDO) is True
assert docEditor.getText() == text
# Redo
assert docEditor.docAction(nwDocAction.REDO) is True
assert docEditor.getText() == text.replace("consectetur", "~~consectetur~~")
assert docEditor.getText() == text.replace("consectetur", "==consectetur==")
assert docEditor.docAction(nwDocAction.UNDO) is True
assert docEditor.getText() == text
@@ -2067,7 +2078,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
origText = docEditor.getText()
# Select the Word "est"
docEditor.setCursorPosition(645)
docEditor.setCursorPosition(663)
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
@@ -2080,11 +2091,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Find next by enter key
monkeypatch.setattr(docSearch.searchBox, "hasFocus", lambda: True)
qtbot.keyClick(docSearch.searchBox, Qt.Key.Key_Return, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 1299) < 3
assert abs(docEditor.getCursorPosition() - 1317) < 3
# Find next by button
qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 1513) < 3
assert abs(docEditor.getCursorPosition() - 1531) < 3
# Activate loop search
docSearch.toggleLoop.activate(QAction.ActionEvent.Trigger)
@@ -2093,7 +2104,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Find next by menu Search > Find Next
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 647) < 3
assert abs(docEditor.getCursorPosition() - 665) < 3
# Close search
docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger)
@@ -2132,13 +2143,13 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Set valid RegEx
docSearch.setSearchText(r"\bSus")
qtbot.mouseClick(docSearch.searchButton, QtMouseLeft, delay=KEY_DELAY)
assert abs(docEditor.getCursorPosition() - 223) < 3
assert abs(docEditor.getCursorPosition() - 241) < 3
# Find next and then prev
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 324) < 3
assert abs(docEditor.getCursorPosition() - 342) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 223) < 3
assert abs(docEditor.getCursorPosition() - 241) < 3
# Make RegEx case sensitive
docSearch.toggleCase.activate(QAction.ActionEvent.Trigger)
@@ -2147,11 +2158,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Find next/prev (one result)
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 626) < 3
assert abs(docEditor.getCursorPosition() - 644) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 626) < 3
assert abs(docEditor.getCursorPosition() - 644) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 626) < 3
assert abs(docEditor.getCursorPosition() - 644) < 3
# Trigger replace
nwGUI.mainMenu.aReplace.activate(QAction.ActionEvent.Trigger)
@@ -2168,22 +2179,22 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
assert CONFIG.searchMatchCap is True
# Replace "Sus" with "Foo" via menu
docEditor.setCursorPosition(605)
docEditor.setCursorPosition(623)
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
nwGUI.mainMenu.aReplaceNext.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[623:634] == "Foopendisse"
assert docEditor.getText()[641:652] == "Foopendisse"
# Find next/prev to loop file
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 223) < 3
assert abs(docEditor.getCursorPosition() - 241) < 3
nwGUI.mainMenu.aFindPrev.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 1805) < 3
assert abs(docEditor.getCursorPosition() - 1823) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 223) < 3
assert abs(docEditor.getCursorPosition() - 241) < 3
# Replace "sus" with "foo" via replace button
qtbot.mouseClick(docSearch.replaceButton, QtMouseLeft, delay=KEY_DELAY)
assert docEditor.getText()[220:228] == "foocipit"
assert docEditor.getText()[238:246] == "foocipit"
# Revert last two replaces
assert docEditor.docAction(nwDocAction.UNDO)
@@ -2197,7 +2208,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Close search and select "est" again
docSearch.cancelSearch.activate(QAction.ActionEvent.Trigger)
docEditor.setCursorPosition(645)
docEditor.setCursorPosition(663)
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
cursor = docEditor.textCursor()
assert cursor.selectedText() == "est"
@@ -2214,9 +2225,9 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Only one match
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 647) < 3
assert abs(docEditor.getCursorPosition() - 665) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 647) < 3
assert abs(docEditor.getCursorPosition() - 665) < 3
# Enable next doc search
docSearch.toggleProject.activate(QAction.ActionEvent.Trigger)
@@ -2227,9 +2238,9 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert docEditor.docHandle == "2426c6f0ca922" # Next document
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 620) < 3
assert abs(docEditor.getCursorPosition() - 651) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.ActionEvent.Trigger)
assert abs(docEditor.getCursorPosition() - 1127) < 3
assert abs(docEditor.getCursorPosition() - 1157) < 3
# Next doc, no match
assert CONFIG.searchNextFile is True
+631
View File
@@ -0,0 +1,631 @@
"""
novelWriter GUI Syntax Highlighter Tester
===========================================
This file is a part of novelWriter
Copyright (C) 2020 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from PyQt6.QtGui import QTextCharFormat, QTextCursor, QTextDocument
from novelwriter import CONFIG, SHARED
from novelwriter.core.item import NWItem
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE, GuiDocHighlighter, TextBlockData
from novelwriter.types import QtKeepAnchor
R_HANDLE = "3456789abcdef"
T_HANDLE = "0123456789abc"
@pytest.fixture()
def syntax(nwGUI):
"""Create a syntax object for use with testing."""
CONFIG.lightTheme = "tomorrow"
CONFIG.themeMode = nwTheme.LIGHT
CONFIG.dialogStyle = 3
CONFIG.fmtDQuoteOpen = "\u201c"
CONFIG.fmtDQuoteClose = "\u201d"
CONFIG.altDialogOpen = "::"
CONFIG.altDialogClose = "::"
theme = SHARED.theme
theme.loadTheme(force=True)
assert theme._guiPalette.base().color().getRgb() == (0xff, 0xff, 0xff, 0xff)
assert theme.syntaxTheme.text.getRgb() == (0x4d, 0x4d, 0x4c, 0xff)
doc = QTextDocument()
syntax = GuiDocHighlighter(doc)
# Add a Mock Item
tRoot = NWItem(SHARED.project, R_HANDLE)
tRoot.setClass(nwItemClass.NOVEL)
tRoot.setType(nwItemType.ROOT)
tItem = NWItem(SHARED.project, T_HANDLE)
tItem.setParent(R_HANDLE)
tItem.setLayout(nwItemLayout.NOTE)
tItem.setClass(nwItemClass.NOVEL)
tItem.setType(nwItemType.FILE)
SHARED.project.tree.add(tRoot)
SHARED.project.tree.add(tItem)
yield syntax
def getFragments(
syntax: GuiDocHighlighter
) -> tuple[list[tuple[int, str]], list[QTextCharFormat]]:
"""Extract all syntax highlighter fragments from a document."""
pieces = []
formats = []
doc = syntax.document()
cursor = QTextCursor(doc)
assert doc is not None
for b in range(doc.blockCount()):
block = doc.findBlockByNumber(b)
first = block.position()
syntax.rehighlightBlock(block)
if layout := block.layout():
for fmt in layout.formats():
cursor.setPosition(first + fmt.start)
cursor.setPosition(first + fmt.start + fmt.length, QtKeepAnchor)
pieces.append((b, fmt.start, fmt.length, cursor.selectedText()))
formats.append(fmt.format)
return pieces, formats
def maxOrd(text: str) -> int:
"""Get the max character value of a string."""
return max(ord(c) for c in text)
@pytest.mark.gui
def testGuiDocHighlighter_Basic(syntax):
"""Test the basic functionality of the syntax highlighter."""
# Alternate Spell Check
assert syntax._spellCheck is False
syntax.setSpellCheck(True)
assert syntax._spellCheck is True
# Check Handle
assert syntax._tHandle is None
syntax.setHandle(T_HANDLE)
assert syntax._tHandle == T_HANDLE
assert syntax._isNovel is False
assert syntax._isInactive is False
tItem = SHARED.project.tree[T_HANDLE]
assert tItem is not None
tItem.setLayout(nwItemLayout.DOCUMENT)
tItem.setClass(nwItemClass.ARCHIVE)
syntax.setHandle(T_HANDLE)
assert syntax._tHandle == T_HANDLE
assert syntax._isNovel is True
assert syntax._isInactive is True
@pytest.mark.gui
def testGuiDocHighlighter_Keywords(syntax):
"""Test highlighting of keywords."""
theme = SHARED.theme
doc = syntax.document()
assert doc is not None
# Settings
syntax._tHandle = T_HANDLE
colKey = theme.syntaxTheme.key.getRgb()
colTag = theme.syntaxTheme.tag.getRgb()
colOpt = theme.syntaxTheme.opt.getRgb()
colErr = theme.syntaxTheme.error.getRgb()
# Ascii
doc.setPlainText(
"@tag: Bob | Robert\n"
"@char: Someone\n"
)
syntax.rehighlightByType(BLOCK_META)
assert maxOrd(doc.toPlainText()) <= 0x7f
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 4, "@tag"), (0, 6, 3, "Bob"), (0, 12, 6, "Robert"),
(1, 0, 5, "@char"), (1, 7, 7, "Someone"),
]
assert formats[0].foreground().color().getRgb() == colKey
assert formats[1].foreground().color().getRgb() == colTag
assert formats[2].foreground().color().getRgb() == colOpt
assert formats[3].foreground().color().getRgb() == colKey
assert formats[4].underlineColor().getRgb() == colErr
# # Unicode <= 0xFFFF
doc.setPlainText(
"@tag: Zoë | Zoë Smith\n"
"@char: Олексій\n"
)
syntax.rehighlightByType(BLOCK_META)
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 4, "@tag"), (0, 6, 3, "Zoë"), (0, 12, 9, "Zoë Smith"),
(1, 0, 5, "@char"), (1, 7, 7, "Олексій"),
]
assert formats[0].foreground().color().getRgb() == colKey
assert formats[1].foreground().color().getRgb() == colTag
assert formats[2].foreground().color().getRgb() == colOpt
assert formats[3].foreground().color().getRgb() == colKey
assert formats[4].underlineColor().getRgb() == colErr
# # Unicode > 0xFFFF
doc.setPlainText(
"@tag: 😄 | Smiley 😄\n"
"@char: 😎😎😎\n"
)
syntax.rehighlightByType(BLOCK_META)
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 4, "@tag"), (0, 6, 2, "😄"), (0, 11, 9, "Smiley 😄"),
(1, 0, 5, "@char"), (1, 7, 6, "😎😎😎"),
]
assert formats[0].foreground().color().getRgb() == colKey
assert formats[1].foreground().color().getRgb() == colTag
assert formats[2].foreground().color().getRgb() == colOpt
assert formats[3].foreground().color().getRgb() == colKey
assert formats[4].underlineColor().getRgb() == colErr
@pytest.mark.gui
def testGuiDocHighlighter_Titles(syntax):
"""Test highlighting of titles."""
theme = SHARED.theme
doc = syntax.document()
assert doc is not None
# Settings
syntax._tHandle = T_HANDLE
colHeadMark = theme.syntaxTheme.headH.getRgb()
colHeadText = theme.syntaxTheme.head.getRgb()
# Ascii
doc.setPlainText(
"# Heading 1\n\n"
"## Heading 2\n\n"
"### Heading 3\n\n"
"#### Heading 4\n\n"
"#! Heading A1\n\n"
"##! Heading A2\n\n"
"###! Heading A3\n\n"
)
syntax.rehighlightByType(BLOCK_TITLE)
assert maxOrd(doc.toPlainText()) <= 0x7f
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 1, "#"), (0, 1, 10, " Heading 1"),
(2, 0, 2, "##"), (2, 2, 10, " Heading 2"),
(4, 0, 3, "###"), (4, 3, 10, " Heading 3"),
(6, 0, 4, "####"), (6, 4, 10, " Heading 4"),
(8, 0, 2, "#!"), (8, 2, 11, " Heading A1"),
(10, 0, 3, "##!"), (10, 3, 11, " Heading A2"),
(12, 0, 4, "###!"), (12, 4, 11, " Heading A3"),
]
for i in range(0, len(formats), 2):
assert formats[i].foreground().color().getRgb() == colHeadMark
assert formats[i+1].foreground().color().getRgb() == colHeadText
# Unicode <= 0xFFFF
doc.setPlainText(
"# Ȟǣđ 1\n\n"
"## Ȟǣđ 2\n\n"
"### Ȟǣđ 3\n\n"
"#### Ȟǣđ 4\n\n"
"#! Ȟǣđ A1\n\n"
"##! Ȟǣđ A2\n\n"
"###! Ȟǣđ A3\n\n"
)
syntax.rehighlightByType(BLOCK_TITLE)
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 1, "#"), (0, 1, 6, " Ȟǣđ 1"),
(2, 0, 2, "##"), (2, 2, 6, " Ȟǣđ 2"),
(4, 0, 3, "###"), (4, 3, 6, " Ȟǣđ 3"),
(6, 0, 4, "####"), (6, 4, 6, " Ȟǣđ 4"),
(8, 0, 2, "#!"), (8, 2, 7, " Ȟǣđ A1"),
(10, 0, 3, "##!"), (10, 3, 7, " Ȟǣđ A2"),
(12, 0, 4, "###!"), (12, 4, 7, " Ȟǣđ A3"),
]
for i in range(0, len(formats), 2):
assert formats[i].foreground().color().getRgb() == colHeadMark
assert formats[i+1].foreground().color().getRgb() == colHeadText
# Unicode > 0xFFFF
doc.setPlainText(
"# 😇😎 1\n\n"
"## 😇😎 2\n\n"
"### 😇😎 3\n\n"
"#### 😇😎 4\n\n"
"#! 😇😎 A1\n\n"
"##! 😇😎 A2\n\n"
"###! 😇😎 A3\n\n"
)
syntax.rehighlightByType(BLOCK_TITLE)
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 1, "#"), (0, 1, 7, " 😇😎 1"),
(2, 0, 2, "##"), (2, 2, 7, " 😇😎 2"),
(4, 0, 3, "###"), (4, 3, 7, " 😇😎 3"),
(6, 0, 4, "####"), (6, 4, 7, " 😇😎 4"),
(8, 0, 2, "#!"), (8, 2, 8, " 😇😎 A1"),
(10, 0, 3, "##!"), (10, 3, 8, " 😇😎 A2"),
(12, 0, 4, "###!"), (12, 4, 8, " 😇😎 A3"),
]
for i in range(0, len(formats), 2):
assert formats[i].foreground().color().getRgb() == colHeadMark
assert formats[i+1].foreground().color().getRgb() == colHeadText
@pytest.mark.gui
def testGuiDocHighlighter_Comments(syntax):
"""Test highlighting of comments."""
theme = SHARED.theme
doc = syntax.document()
assert doc is not None
# Settings
syntax._tHandle = T_HANDLE
colHidden = theme.syntaxTheme.hidden.getRgb()
colMod = theme.syntaxTheme.mod.getRgb()
colValue = theme.syntaxTheme.val.getRgb()
colNote = theme.syntaxTheme.note.getRgb()
# Ascii
doc.setPlainText(
"% Plain\n"
"%~ Ignored\n"
"%Synopsis: Synopsis\n"
"%Note.Stuff: Note\n"
)
syntax.rehighlight()
assert maxOrd(doc.toPlainText()) <= 0x7f
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 7, "% Plain"),
(1, 0, 10, "%~ Ignored"),
(2, 0, 10, "%Synopsis:"), (2, 10, 9, " Synopsis"),
(3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 5, " Note"),
]
assert formats[0].foreground().color().getRgb() == colHidden
assert formats[1].foreground().color().getRgb() == colHidden
assert formats[1].fontStrikeOut() is True
assert formats[2].foreground().color().getRgb() == colMod
assert formats[3].foreground().color().getRgb() == colNote
assert formats[4].foreground().color().getRgb() == colMod
assert formats[5].foreground().color().getRgb() == colValue
assert formats[6].foreground().color().getRgb() == colNote
# Unicode <= 0xFFFF
doc.setPlainText(
"% Рівнина\n"
"%~ Ігноровано\n"
"%Synopsis: Синопсис\n"
"%Note.Stuff: Примітка\n"
)
syntax.rehighlight()
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 9, "% Рівнина"),
(1, 0, 13, "%~ Ігноровано"),
(2, 0, 10, "%Synopsis:"), (2, 10, 9, " Синопсис"),
(3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 9, " Примітка"),
]
assert formats[0].foreground().color().getRgb() == colHidden
assert formats[1].foreground().color().getRgb() == colHidden
assert formats[1].fontStrikeOut() is True
assert formats[2].foreground().color().getRgb() == colMod
assert formats[3].foreground().color().getRgb() == colNote
assert formats[4].foreground().color().getRgb() == colMod
assert formats[5].foreground().color().getRgb() == colValue
assert formats[6].foreground().color().getRgb() == colNote
# Unicode > 0xFFFF
doc.setPlainText(
"% 😎😎\n"
"%~ 🙈🙈🙈\n"
"%Synopsis: 😍😍😍😍\n"
"%Note.Stuff: 😡😡😡😡😡\n"
)
syntax.rehighlight()
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 6, "% 😎😎"),
(1, 0, 9, "%~ 🙈🙈🙈"),
(2, 0, 10, "%Synopsis:"), (2, 10, 9, " 😍😍😍😍"),
(3, 0, 6, "%Note."), (3, 6, 6, "Stuff:"), (3, 12, 11, " 😡😡😡😡😡"),
]
assert formats[0].foreground().color().getRgb() == colHidden
assert formats[1].foreground().color().getRgb() == colHidden
assert formats[1].fontStrikeOut() is True
assert formats[2].foreground().color().getRgb() == colMod
assert formats[3].foreground().color().getRgb() == colNote
assert formats[4].foreground().color().getRgb() == colMod
assert formats[5].foreground().color().getRgb() == colValue
assert formats[6].foreground().color().getRgb() == colNote
@pytest.mark.gui
def testGuiDocHighlighter_Special(syntax):
"""Test highlighting of special commands."""
theme = SHARED.theme
doc = syntax.document()
assert doc is not None
# Settings
syntax._tHandle = T_HANDLE
colErr = theme.syntaxTheme.error.getRgb()
colCode = theme.syntaxTheme.code.getRgb()
colValue = theme.syntaxTheme.val.getRgb()
# Ascii
doc.setPlainText(
"[NewPage]\n"
"[New Page]\n"
"[VSpace]\n"
"[VSpace:123]\n"
"[VSpace:Meh]\n"
)
syntax.rehighlight()
assert maxOrd(doc.toPlainText()) <= 0x7f
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 9, "[NewPage]"),
(1, 0, 10, "[New Page]"),
(2, 0, 8, "[VSpace]"),
(3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"),
(4, 0, 8, "[VSpace:"), (4, 8, 3, "Meh"), (4, 11, 1, "]"),
]
assert formats[0].foreground().color().getRgb() == colCode
assert formats[1].foreground().color().getRgb() == colCode
assert formats[2].foreground().color().getRgb() == colCode
assert formats[3].foreground().color().getRgb() == colCode
assert formats[4].foreground().color().getRgb() == colValue
assert formats[5].foreground().color().getRgb() == colCode
assert formats[6].foreground().color().getRgb() == colCode
assert formats[7].underlineColor().getRgb() == colErr
assert formats[8].foreground().color().getRgb() == colCode
# Unicode <= 0xFFFF
doc.setPlainText(
"[NewPage]\n"
"[New Page]\n"
"[VSpace]\n"
"[VSpace:123]\n"
"[VSpace:⅘]\n"
)
syntax.rehighlight()
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 9, "[NewPage]"),
(1, 0, 10, "[New Page]"),
(2, 0, 8, "[VSpace]"),
(3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"),
(4, 0, 8, "[VSpace:"), (4, 8, 1, ""), (4, 9, 1, "]"),
]
assert formats[0].foreground().color().getRgb() == colCode
assert formats[1].foreground().color().getRgb() == colCode
assert formats[2].foreground().color().getRgb() == colCode
assert formats[3].foreground().color().getRgb() == colCode
assert formats[4].foreground().color().getRgb() == colValue
assert formats[5].foreground().color().getRgb() == colCode
assert formats[6].foreground().color().getRgb() == colCode
assert formats[7].underlineColor().getRgb() == colErr
assert formats[8].foreground().color().getRgb() == colCode
# Unicode > 0xFFFF
doc.setPlainText(
"[NewPage]\n"
"[New Page]\n"
"[VSpace]\n"
"[VSpace:123]\n"
"[VSpace:🙈🙈]\n"
)
syntax.rehighlight()
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 9, "[NewPage]"),
(1, 0, 10, "[New Page]"),
(2, 0, 8, "[VSpace]"),
(3, 0, 8, "[VSpace:"), (3, 8, 3, "123"), (3, 11, 1, "]"),
(4, 0, 8, "[VSpace:"), (4, 8, 4, "🙈🙈"), (4, 12, 1, "]"),
]
assert formats[0].foreground().color().getRgb() == colCode
assert formats[1].foreground().color().getRgb() == colCode
assert formats[2].foreground().color().getRgb() == colCode
assert formats[3].foreground().color().getRgb() == colCode
assert formats[4].foreground().color().getRgb() == colValue
assert formats[5].foreground().color().getRgb() == colCode
assert formats[6].foreground().color().getRgb() == colCode
assert formats[7].underlineColor().getRgb() == colErr
assert formats[8].foreground().color().getRgb() == colCode
@pytest.mark.gui
def testGuiDocHighlighter_Text(monkeypatch, syntax):
"""Test highlighting of text."""
theme = SHARED.theme
doc = syntax.document()
assert doc is not None
# Settings
syntax._tHandle = T_HANDLE
syntax._isNovel = True
syntax.setSpellCheck(True)
monkeypatch.setattr(SHARED.spelling, "checkWord", lambda *a: False)
colHidden = theme.syntaxTheme.hidden.getRgb()
colEmph = theme.syntaxTheme.emph.getRgb()
colLink = theme.syntaxTheme.link.getRgb()
colSpell = theme.syntaxTheme.spell.getRgb()
colCode = theme.syntaxTheme.code.getRgb()
colDialogue = theme.syntaxTheme.dialN.getRgb()
colAltDialogue = theme.syntaxTheme.dialA.getRgb()
# Ascii
doc.setPlainText(
"Text **bold** text _italic_ text ~~strike~~ text [b]bold[/b], http://example.com\n\n"
)
syntax.rehighlight()
assert maxOrd(doc.toPlainText()) <= 0x7f
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 4, "Text"),
(0, 5, 2, "**"), (0, 7, 4, "bold"), (0, 11, 2, "**"),
(0, 14, 4, "text"),
(0, 19, 1, "_"), (0, 20, 6, "italic"), (0, 26, 1, "_"),
(0, 28, 4, "text"),
(0, 33, 2, "~~"), (0, 35, 6, "strike"), (0, 41, 2, "~~"),
(0, 44, 4, "text"),
(0, 49, 3, "[b]"), (0, 52, 4, "bold"), (0, 56, 4, "[/b]"),
(0, 62, 18, "http://example.com"),
]
assert formats[0].underlineColor().getRgb() == colSpell # Text
assert formats[1].foreground().color().getRgb() == colHidden # **
assert formats[2].foreground().color().getRgb() == colEmph # bold
assert formats[2].underlineColor().getRgb() == colSpell
assert formats[3].foreground().color().getRgb() == colHidden # **
assert formats[4].underlineColor().getRgb() == colSpell # text
assert formats[5].foreground().color().getRgb() == colHidden # _
assert formats[6].foreground().color().getRgb() == colEmph # italic
assert formats[6].underlineColor().getRgb() == colSpell
assert formats[7].foreground().color().getRgb() == colHidden # _
assert formats[8].underlineColor().getRgb() == colSpell # text
assert formats[9].foreground().color().getRgb() == colHidden # ~~
assert formats[10].foreground().color().getRgb() == colHidden # strike
assert formats[10].fontStrikeOut() is True
assert formats[10].underlineColor().getRgb() == colSpell
assert formats[11].foreground().color().getRgb() == colHidden # ~~
assert formats[12].underlineColor().getRgb() == colSpell # text
assert formats[13].foreground().color().getRgb() == colCode # [b]
assert formats[14].underlineColor().getRgb() == colSpell # bold
assert formats[15].foreground().color().getRgb() == colCode # [/b]
assert formats[16].foreground().color().getRgb() == colLink # http://example.com
# Spell Check
data = doc.findBlockByNumber(0).userData()
assert isinstance(data, TextBlockData)
assert data.metaData == [(62, 80, "http://example.com", "url")]
assert data.spellErrors == [
(0, 4, "Text"), (7, 11, "bold"),
(14, 18, "text"), (20, 26, "italic"),
(28, 32, "text"), (35, 41, "strike"),
(44, 48, "text"), (52, 56, "bold"),
]
# Unicode <= 0xFFFF
doc.setPlainText(
"\u201cDialogue,\u201d and then ::dialogue::, http://example.com\n\n"
)
syntax.rehighlight()
assert 0x7f < maxOrd(doc.toPlainText()) <= 0xffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 1, "\u201c"), (0, 1, 8, "Dialogue"), (0, 9, 2, ",\u201d"),
(0, 12, 3, "and"), (0, 16, 4, "then"),
(0, 21, 2, "::"), (0, 23, 8, "dialogue"), (0, 31, 2, "::"),
(0, 35, 18, "http://example.com"),
]
assert formats[0].foreground().color().getRgb() == colDialogue # Quote
assert formats[1].foreground().color().getRgb() == colDialogue # Dialogue
assert formats[1].underlineColor().getRgb() == colSpell
assert formats[2].foreground().color().getRgb() == colDialogue # Quote
assert formats[3].underlineColor().getRgb() == colSpell # and
assert formats[4].underlineColor().getRgb() == colSpell # then
assert formats[5].foreground().color().getRgb() == colAltDialogue # ::
assert formats[6].foreground().color().getRgb() == colAltDialogue # dialogue
assert formats[6].underlineColor().getRgb() == colSpell
assert formats[7].foreground().color().getRgb() == colAltDialogue # ::
assert formats[8].foreground().color().getRgb() == colLink # http://example.com
# Spell Check
data = doc.findBlockByNumber(0).userData()
assert isinstance(data, TextBlockData)
assert data.metaData == [(35, 53, "http://example.com", "url")]
assert data.spellErrors == [
(1, 9, "Dialogue"), (12, 15, "and"),
(16, 20, "then"), (23, 31, "dialogue"),
]
# Unicode > 0xFFFF
doc.setPlainText(
"\u201c😁 Grinning 😁,\u201d and then ::🙊 shush 🙊::, http://example.com\n\n"
)
syntax.rehighlight()
assert 0xffff < maxOrd(doc.toPlainText()) <= 0xffffffff
pieces, formats = getFragments(syntax)
assert pieces == [
(0, 0, 4, "\u201c😁 "), (0, 4, 8, "Grinning"), (0, 12, 5, " 😁,\u201d"),
(0, 18, 3, "and"), (0, 22, 4, "then"),
(0, 27, 5, "::🙊 "), (0, 32, 5, "shush"), (0, 37, 5, " 🙊::"),
(0, 44, 18, "http://example.com"),
]
assert formats[0].foreground().color().getRgb() == colDialogue # Quote😁
assert formats[1].foreground().color().getRgb() == colDialogue # Grinning
assert formats[1].underlineColor().getRgb() == colSpell
assert formats[2].foreground().color().getRgb() == colDialogue # 😁Quote
assert formats[3].underlineColor().getRgb() == colSpell # and
assert formats[4].underlineColor().getRgb() == colSpell # then
assert formats[5].foreground().color().getRgb() == colAltDialogue # ::🙊
assert formats[6].foreground().color().getRgb() == colAltDialogue # shush
assert formats[6].underlineColor().getRgb() == colSpell
assert formats[7].foreground().color().getRgb() == colAltDialogue # 🙊::
assert formats[8].foreground().color().getRgb() == colLink # http://example.com
# Spell Check
data = doc.findBlockByNumber(0).userData()
assert isinstance(data, TextBlockData)
assert data.metaData == [(40, 58, "http://example.com", "url")]
assert data.spellErrors == [
(4, 12, "Grinning"), (18, 21, "and"),
(22, 26, "then"), (32, 37, "shush"),
]
+8 -2
View File
@@ -223,17 +223,23 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
CONFIG.showFullPath = True
# Document footer show/hide synopsis
assert nwGUI.viewDocument("f96ec11c6a3da") is True
nwGUI.viewDocument("f96ec11c6a3da")
assert len(docViewer.toPlainText()) == 4314
docViewer.docFooter._doToggleSynopsis(False)
assert len(docViewer.toPlainText()) == 4098
# Document footer show/hide comments
assert nwGUI.viewDocument("846352075de7d") is True
nwGUI.viewDocument("846352075de7d")
assert len(docViewer.toPlainText()) == 683
docViewer.docFooter._doToggleComments(False)
assert len(docViewer.toPlainText()) == 634
# Document footer show/hide notes
nwGUI.viewDocument("88d59a277361b")
assert len(docViewer.toPlainText()) == 900
docViewer.docFooter._doToggleNotes(False)
assert len(docViewer.toPlainText()) == 871
# Crash the HTML rendering
with monkeypatch.context() as mp:
mp.setattr(ToQTextDocument, "doConvert", causeException)
+1 -1
View File
@@ -224,7 +224,7 @@ def testGuiViewerPanel_Tags(qtbot, monkeypatch, caplog, nwGUI, projPath, mockRnd
# Update Labels
assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "New"
SHARED.project.data.itemImport.add(C.iNew, "Stuff", (100, 100, 100), "SQUARE", 0)
SHARED.project.data.itemImport.add(C.iNew, "Stuff", "#646464", "SQUARE", 0)
viewPanel.updateStatusLabels("i")
assert charTab.topLevelItem(0).text(charTab.C_IMPORT) == "Stuff"
+24 -9
View File
@@ -24,6 +24,7 @@ import shutil
from pathlib import Path
from shutil import copyfile
from unittest.mock import Mock
import pytest
@@ -32,9 +33,10 @@ from PyQt6.QtGui import QPalette
from PyQt6.QtWidgets import QInputDialog, QMessageBox
from novelwriter import CONFIG, SHARED
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT
from novelwriter.constants import nwFiles
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwView
from novelwriter.enum import nwDocAction, nwDocMode, nwFocus, nwItemType, nwTheme, nwView
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.gui.noveltree import GuiNovelView
from novelwriter.gui.outline import GuiOutlineView
@@ -179,21 +181,35 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
@pytest.mark.gui
def testGuiMain_UpdateTheme(qtbot, nwGUI):
"""Test updating the theme in the GUI."""
mainTheme = SHARED.theme
CONFIG.guiTheme = "default_dark"
CONFIG.guiSyntax = "default_dark"
mainTheme.loadTheme()
mainTheme.loadSyntax()
theme = SHARED.theme
CONFIG.themeMode = nwTheme.DARK
CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.lightTheme = DEF_GUI_LIGHT
theme.loadTheme()
assert theme.isDarkTheme is True
nwGUI._processConfigChanges(False, True, False, False)
nwGUI._processConfigChanges(True, True, True, True)
# Check editor syntax
syntax = SHARED.theme.syntaxTheme
assert nwGUI.docEditor.palette().color(QPalette.ColorRole.Window) == syntax.back
assert nwGUI.docEditor.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back
assert nwGUI.docViewer.palette().color(QPalette.ColorRole.Window) == syntax.back
assert nwGUI.docViewer.docHeader.palette().color(QPalette.ColorRole.Window) == syntax.back
# Update by check
CONFIG.themeMode = nwTheme.LIGHT
nwGUI.checkThemeUpdate()
assert theme.isDarkTheme is False
# Through change event
event = Mock()
event.type.return_value = 210
CONFIG.themeMode = nwTheme.DARK
nwGUI.changeEvent(event)
assert theme.isDarkTheme is True
# qtbot.stop()
@@ -251,8 +267,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
# Change some settings
CONFIG.hideHScroll = True
CONFIG.hideVScroll = True
CONFIG.autoScrollPos = 80
CONFIG.autoScroll = True
CONFIG.autoScroll = False
# Add a Character File
nwGUI._changeView(nwView.PROJECT)
+40 -39
View File
@@ -68,154 +68,155 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
assert docEditor.docAction(nwDocAction.COPY) is False
assert nwGUI.openProject(prjLipsum) is True
x = 72
# Split By Chapter
assert nwGUI.openDocument("4c4f28287af27") is True
docEditor.setCursorPosition(57)
cleanText = docEditor.getText()[54:101]
docEditor.setCursorPosition(x+3)
cleanText = docEditor.getText()[x:x+47]
# Bold
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
fmtStr = "**Pellentesque** nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:105] == fmtStr
assert docEditor.getText()[x:x+51] == fmtStr
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Italic
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
fmtStr = "_Pellentesque_ nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:103] == fmtStr
assert docEditor.getText()[x:x+49] == fmtStr
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Strikethrough
mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
fmtStr = "~~Pellentesque~~ nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:105] == fmtStr
assert docEditor.getText()[x:x+51] == fmtStr
mainMenu.aFmtStrike.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Should get us back to plain
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtItalic.activate(QAction.ActionEvent.Trigger)
mainMenu.aFmtBold.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Double Quotes
mainMenu.aFmtDQuote.activate(QAction.ActionEvent.Trigger)
fmtStr = "“Pellentesque” nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:103] == fmtStr
assert docEditor.getText()[x:x+49] == fmtStr
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Single Quotes
mainMenu.aFmtSQuote.activate(QAction.ActionEvent.Trigger)
fmtStr = "Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:103] == fmtStr
assert docEditor.getText()[x:x+49] == fmtStr
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Block Formats
# =============
# cSpell:ignore Pellentesque erat nulla posuere commodo
docEditor.setCursorPosition(57)
docEditor.setCursorPosition(x+3)
# Header 1
mainMenu.aFmtHead1.activate(QAction.ActionEvent.Trigger)
fmtStr = "# Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:103] == fmtStr
assert docEditor.getText()[x:x+49] == fmtStr
# Header 2
mainMenu.aFmtHead2.activate(QAction.ActionEvent.Trigger)
fmtStr = "## Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:104] == fmtStr
assert docEditor.getText()[x:x+50] == fmtStr
# Header 3
mainMenu.aFmtHead3.activate(QAction.ActionEvent.Trigger)
fmtStr = "### Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:105] == fmtStr
assert docEditor.getText()[x:x+51] == fmtStr
# Header 4
mainMenu.aFmtHead4.activate(QAction.ActionEvent.Trigger)
fmtStr = "#### Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:106] == fmtStr
assert docEditor.getText()[x:x+52] == fmtStr
# Title Format
mainMenu.aFmtTitle.activate(QAction.ActionEvent.Trigger)
fmtStr = "#! Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:104] == fmtStr
assert docEditor.getText()[x:x+50] == fmtStr
# Unnumbered Chapter
mainMenu.aFmtUnNum.activate(QAction.ActionEvent.Trigger)
fmtStr = "##! Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:105] == fmtStr
assert docEditor.getText()[x:x+51] == fmtStr
# Hard Scene
mainMenu.aFmtHardSc.activate(QAction.ActionEvent.Trigger)
fmtStr = "###! Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:106] == fmtStr
assert docEditor.getText()[x:x+52] == fmtStr
# Clear Format
mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Comment On
mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
fmtStr = "% Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:103] == fmtStr
assert docEditor.getText()[x:x+49] == fmtStr
# Comment Off
mainMenu.aFmtComment.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Check comment with no space before text
docEditor.setCursorPosition(54)
docEditor.setCursorPosition(x)
docEditor.insertText("%")
fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:102] == fmtStr
assert docEditor.getText()[x:x+48] == fmtStr
mainMenu.aFmtNoFormat.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Undo/Redo
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
fmtStr = "%Pellentesque nec erat ut nulla posuere commodo."
assert docEditor.getText()[54:102] == fmtStr
assert docEditor.getText()[x:x+48] == fmtStr
mainMenu.aEditRedo.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:101] == cleanText
assert docEditor.getText()[x:x+47] == cleanText
# Cut, Copy and Paste
docEditor.setCursorPosition(54)
docEditor.setCursorPosition(x)
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
mainMenu.aEditCut.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
assert docEditor.getText()[x:x+50] == (
" nec erat ut nulla posuere commodo. Curabitur nisi"
)
mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
assert docEditor.getText()[x:x+50] == (
"Pellentesque nec erat ut nulla posuere commodo. Cu"
)
docEditor.setCursorPosition(54)
docEditor.setCursorPosition(x)
docEditor._makeSelection(QTextCursor.SelectionType.WordUnderCursor)
mainMenu.aEditCopy.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
assert docEditor.getText()[x:x+50] == (
"Pellentesque nec erat ut nulla posuere commodo. Cu"
)
docEditor.setCursorPosition(54)
docEditor.setCursorPosition(x)
mainMenu.aEditPaste.activate(QAction.ActionEvent.Trigger)
assert docEditor.getText()[54:104] == (
assert docEditor.getText()[x:x+50] == (
"PellentesquePellentesque nec erat ut nulla posuere"
)
mainMenu.aEditUndo.activate(QAction.ActionEvent.Trigger)
# Select Paragraph/All
docEditor.setCursorPosition(57)
docEditor.setCursorPosition(x+3)
mainMenu.aSelectPar.activate(QAction.ActionEvent.Trigger)
cursor = docEditor.textCursor()
assert cursor.selectedText() == (
@@ -228,10 +229,10 @@ def testGuiMainMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
"nunc lacus, imperdiet nec posuere ac, interdum non lectus."
)
docEditor.setCursorPosition(57)
docEditor.setCursorPosition(x+3)
mainMenu.aSelectAll.activate(QAction.ActionEvent.Trigger)
cursor = docEditor.textCursor()
assert len(cursor.selectedText()) == 1910
assert len(cursor.selectedText()) == 1928
# Clear the Text
docEditor.clear()
+53
View File
@@ -0,0 +1,53 @@
"""
novelWriter Side Bar Class Tester
===================================
This file is a part of novelWriter
Copyright (C) 2025 Veronica Berglyd Olsen and novelWriter contributors
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from novelwriter import CONFIG
from novelwriter.constants import nwLabels
from novelwriter.enum import nwTheme
@pytest.mark.gui
def testGuiSideBar_CycleColourTheme(nwGUI):
"""Test theme cycle feature on the side bar."""
CONFIG.themeMode = nwTheme.AUTO
sidebar = nwGUI.sideBar
sidebar.mainGui.checkThemeUpdate = lambda *a: None
# Run 3 Cycles
for _ in range(3):
# Cycle Light
sidebar._cycleColurTheme()
assert CONFIG.themeMode == nwTheme.LIGHT
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.LIGHT]
# Cycle Dark
sidebar._cycleColurTheme()
assert CONFIG.themeMode == nwTheme.DARK
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.DARK]
# Cycle Auto
sidebar._cycleColurTheme()
assert CONFIG.themeMode == nwTheme.AUTO
assert sidebar.tbTheme.toolTip() == nwLabels.THEME_MODE_LABEL[nwTheme.AUTO]
+1 -1
View File
@@ -31,7 +31,7 @@ from tests.tools import C, buildTestProject
@pytest.mark.gui
def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the the various features of the status bar."""
"""Test the various features of the status bar."""
buildTestProject(nwGUI, projPath)
cHandle = SHARED.project.newFile("A Note", C.hCharRoot)
newDoc = SHARED.project.storage.getDocument(cHandle)
+461 -294
View File
@@ -20,344 +20,402 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import sys
import json
from configparser import ConfigParser
from pathlib import Path
from unittest.mock import MagicMock, Mock
import pytest
from PyQt6.QtGui import QColor, QIcon, QPalette, QPixmap
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QColor, QFont, QFontDatabase, QIcon, QPalette, QPixmap, QStyleHints
from novelwriter import CONFIG, SHARED
from novelwriter.common import NWConfigParser
from novelwriter.config import DEF_GUI
from novelwriter import CONFIG
from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS
from novelwriter.constants import nwLabels
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.gui.theme import _listConf
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme
from novelwriter.gui.theme import (
STYLES_BIG_TOOLBUTTON, STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON, GuiTheme,
ThemeMeta, _listContent
)
from tests.mocked import causeOSError
from tests.tools import writeFile
@pytest.mark.gui
def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
"""Test the theme class init."""
mainTheme = SHARED.theme
def testGuiTheme_ParseColor():
"""Test the colour parsing."""
theme = GuiTheme()
# Methods
# =======
# Pre-Populate
theme._qColors["red"] = QColor(255, 0, 0)
theme._qColors["green"] = QColor(0, 255, 0)
theme._qColors["blue"] = QColor(0, 0, 255)
theme._qColors["grey"] = QColor(127, 127, 127)
mSize = mainTheme.getTextWidth("m")
assert mSize > 0
assert mainTheme.getTextWidth("m", mainTheme.guiFont) == mSize
# By Name
assert theme.parseColor("red").getRgb() == (255, 0, 0, 255)
assert theme.parseColor("green").getRgb() == (0, 255, 0, 255)
assert theme.parseColor("blue").getRgb() == (0, 0, 255, 255)
assert theme.parseColor("bob").getRgb() == (0, 0, 0, 255)
# Scan for Themes
# ===============
# CSS Format
assert theme.parseColor("#ff0000").getRgb() == (255, 0, 0, 255)
assert theme.parseColor("#ff00007f").getRgb() == (255, 0, 0, 127)
assert theme.parseColor("#ff00").getRgb() == (0, 0, 0, 255) # Too short -> ignored
assert theme.parseColor("#ff00007f15").getRgb() == (0, 0, 0, 255) # Too long -> ignored
result = {}
_listConf({}, Path("not_a_path"), ".conf")
assert result == {}
# Name + Alpha
assert theme.parseColor("red:255").getRgb() == (255, 0, 0, 255)
assert theme.parseColor("red:127").getRgb() == (255, 0, 0, 127)
assert theme.parseColor("red:512").getRgb() == (255, 0, 0, 255) # Value truncated
themeOne = tstPaths.cnfDir / "themes" / "themeone.conf"
themeTwo = tstPaths.cnfDir / "themes" / "themetwo.conf"
writeFile(themeOne, "# Stuff")
writeFile(themeTwo, "# Stuff")
# Name + Lighter
assert theme.parseColor("grey:L100").getRgb() == (127, 127, 127, 255)
assert theme.parseColor("grey:L150").getRgb() == (190, 190, 190, 255)
assert theme.parseColor("grey:L50").getRgb() == (63, 63, 63, 255)
_listConf(result, tstPaths.cnfDir / "themes", ".conf")
assert result["themeone"] == themeOne
assert result["themetwo"] == themeTwo
# Name + Darker
assert theme.parseColor("grey:D100").getRgb() == (127, 127, 127, 255)
assert theme.parseColor("grey:D150").getRgb() == (85, 85, 85, 255)
assert theme.parseColor("grey:D50").getRgb() == (254, 254, 254, 255)
# Parse Colours
# =============
parser = NWConfigParser()
parser["Palette"] = {
"colour1": "100, 150, 200", # Valid
"colour2": "100, 150, 200, 250", # With alpha
"colour3": "100, 150, 200, 250, 300", # Too many values
"colour4": "250, 250", # Missing blue
"colour5": "-10, 127, 300", # Invalid red and blue
"colour6": "bob, 127, 255", # Invalid red
}
# Test the parser for several valid and invalid values
assert mainTheme._parseColor(parser, "Palette", "colour1").getRgb() == (100, 150, 200, 255)
assert mainTheme._parseColor(parser, "Palette", "colour2").getRgb() == (100, 150, 200, 250)
assert mainTheme._parseColor(parser, "Palette", "colour3").getRgb() == (100, 150, 200, 250)
assert mainTheme._parseColor(parser, "Palette", "colour4").getRgb() == (250, 250, 0, 255)
assert mainTheme._parseColor(parser, "Palette", "colour5").getRgb() == (0, 0, 0, 0)
assert mainTheme._parseColor(parser, "Palette", "colour6").getRgb() == (0, 127, 255, 255)
# The palette should load with the parsed values
mainTheme._setPalette(parser, "Palette", "colour1", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 255)
mainTheme._setPalette(parser, "Palette", "colour2", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour3", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (100, 150, 200, 250)
mainTheme._setPalette(parser, "Palette", "colour4", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (250, 250, 0, 255)
mainTheme._setPalette(parser, "Palette", "colour5", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 0)
mainTheme._setPalette(parser, "Palette", "colour6", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 127, 255, 255)
# Non-existing value should return default colour
mainTheme._setPalette(parser, "Palette", "stuff", QPalette.ColorRole.Window)
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == (0, 0, 0, 255)
# qtbot.stop()
# Values
assert theme.parseColor("255, 0, 0").getRgb() == (255, 0, 0, 255)
assert theme.parseColor("255, 0, 0, 255").getRgb() == (255, 0, 0, 255)
assert theme.parseColor("255, 0, 0, 127").getRgb() == (255, 0, 0, 127)
assert theme.parseColor("255, 0, 0, 127, 42").getRgb() == (255, 0, 0, 127) # Truncated
@pytest.mark.gui
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
"""Test the theme part of the class."""
mainTheme = SHARED.theme
def testGuiTheme_ScanThemes(monkeypatch):
"""Test the themes scanning."""
theme = GuiTheme()
# List Themes
# Load built-in themes
files = []
_listContent(files, CONFIG.assetPath("themes"), ".conf")
assert len(files) > 0
# Block reading theme files
with monkeypatch.context() as mp:
mp.setattr(ConfigParser, "read", causeOSError)
theme._scanThemes(files)
assert theme.colourThemes == {}
# Read all themes correctly
theme._scanThemes(files)
assert len(theme.colourThemes) > 0
dark = theme.colourThemes[DEF_GUI_DARK]
light = theme.colourThemes[DEF_GUI_LIGHT]
assert dark.name == "Default Dark Theme"
assert dark.dark is True
assert light.name == "Default Light Theme"
assert light.dark is False
@pytest.mark.gui
def testGuiTheme_LoadThemes(monkeypatch):
"""Test loading themes."""
theme = GuiTheme()
theme.iconCache = MagicMock()
CONFIG.lightTheme = DEF_GUI_LIGHT
CONFIG.darkTheme = DEF_GUI_DARK
# Load built-in themes
files = []
_listContent(files, CONFIG.assetPath("themes"), ".conf")
theme._scanThemes(files)
assert DEF_GUI_LIGHT in theme._allThemes
assert DEF_GUI_DARK in theme._allThemes
assert theme._currentTheme == ""
# Load light theme
CONFIG.themeMode = nwTheme.LIGHT
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_LIGHT
# Load dark theme
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_DARK
# Let auto switch back to light, then dark
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060500)
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light)
CONFIG.themeMode = nwTheme.AUTO
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_LIGHT
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060500)
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark)
CONFIG.themeMode = nwTheme.AUTO
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_DARK
# Error Cases
# ===========
# Block the reading of the files
# Invalid light theme
CONFIG.lightTheme = "not_a_theme"
CONFIG.themeMode = nwTheme.LIGHT
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_LIGHT
# Invalid dark theme
CONFIG.darkTheme = "not_a_theme"
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme()
assert theme._currentTheme == DEF_GUI_DARK
# Clear meta and check exit early cases
theme._meta = ThemeMeta()
assert theme._meta.name == ""
# Reload dark should not load anything
CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme()
assert theme._meta.name == ""
# Force reload, but fail parsing
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert mainTheme.listThemes() == []
mp.setattr(ConfigParser, "read", causeOSError)
CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme(force=True)
assert theme._meta.name == ""
# Load the theme info, default themes first
themesList = mainTheme.listThemes()
assert themesList[0] == ("default_dark", "Default Dark Theme")
assert themesList[1] == ("default_light", "Default Light Theme")
assert themesList[2] == ("cyberpunk_night", "Cyberpunk Night")
assert themesList[3] == ("dracula", "Dracula")
# Invalid theme, and defaults are missing
del theme._allThemes[DEF_GUI_DARK]
del theme._allThemes[DEF_GUI_LIGHT]
# A second call should returned the cached list
assert mainTheme.listThemes() == mainTheme._themeList
CONFIG.lightTheme = "not_a_theme"
CONFIG.themeMode = nwTheme.LIGHT
theme.loadTheme(force=True)
assert theme._meta.name == ""
# Check handling of broken theme settings
CONFIG.guiTheme = "not_a_theme"
availThemes = mainTheme._availThemes
mainTheme._availThemes = {}
assert mainTheme.loadTheme() is False
mainTheme._availThemes = availThemes
CONFIG.darkTheme = "not_a_theme"
CONFIG.themeMode = nwTheme.DARK
theme.loadTheme(force=True)
assert theme._meta.name == ""
# Check handling of unreadable file
CONFIG.guiTheme = DEF_GUI
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert mainTheme.loadTheme() is False
# Load Default Theme
# ==================
@pytest.mark.gui
def testGuiTheme_SpecialColors(tstPaths):
"""Test handling special cases for colours."""
theme = GuiTheme()
theme.iconCache = MagicMock()
if sys.platform != "win32":
# Set a mock colour for the window background
mainTheme._guiPalette.color(QPalette.ColorRole.Window).setRgb(0, 0, 0, 0)
# Load the default theme
CONFIG.guiTheme = DEF_GUI
assert mainTheme.loadTheme() is True
# This should load a standard palette
wCol = QPalette().color(QPalette.ColorRole.Window).getRgb()
assert mainTheme._guiPalette.color(QPalette.ColorRole.Window).getRgb() == wCol
# Mock Dark Theme
# ===============
mockTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
mockTheme.write_text(
testTheme: Path = tstPaths.cnfDir / "themes" / "test.conf"
testTheme.write_text(
"[Main]\n"
"name = Test\n"
"mode = light\n"
"\n"
"[Base]\n"
"default = #cccccc\n"
"faded = #949494\n"
"red = #ff0000\n"
"orange = #ff7f00\n"
"yellow = #ffff00\n"
"green = #00ff00\n"
"cyan = #00ffff\n"
"blue = #0000ff\n"
"purple = #ff00ff\n"
"\n"
"[Project]\n"
"root = blue\n"
"folder = yellow\n"
"file = default\n"
"title = green\n"
"chapter = red\n"
"scene = blue\n"
"note = yellow\n"
"\n"
"[Palette]\n"
"window = 0, 0, 0\n"
"text = 255, 255, 255\n"
"window = #000000\n"
"text = #ffffff\n"
)
mainTheme._availThemes["test"] = mockTheme
theme._scanThemes([testTheme])
assert len(theme.colourThemes) == 1
CONFIG.themeMode = nwTheme.LIGHT
CONFIG.lightTheme = "test"
CONFIG.guiTheme = "test"
assert mainTheme.loadTheme() is True
assert mainTheme._guiPalette.window().color().getRgb() == (0, 0, 0, 255)
assert mainTheme._guiPalette.text().color().getRgb() == (255, 255, 255, 255)
assert mainTheme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
assert mainTheme.isDarkTheme is True
# Load theme
theme.loadTheme()
# Load Default Light Theme
# ========================
# Since window is black, a lighter version should be generated
assert theme._guiPalette.light().color().getRgb() == (57, 57, 57, 255)
CONFIG.guiTheme = "default_light"
assert mainTheme.loadTheme() is True
# Reload with project override to red
CONFIG.iconColTree = "red"
CONFIG.iconColDocs = True
theme.loadTheme(force=True)
# Check a few values
assert mainTheme._guiPalette.color(
QPalette.ColorRole.Window
).getRgb() == (239, 239, 239, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.WindowText
).getRgb() == (0, 0, 0, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.Base
).getRgb() == (255, 255, 255, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.AlternateBase
).getRgb() == (224, 224, 224, 255)
assert theme.getBaseColor("root").getRgb() == (255, 0, 0, 255)
assert theme.getBaseColor("folder").getRgb() == (255, 0, 0, 255)
assert theme.getBaseColor("file").getRgb() == (204, 204, 204, 255)
assert theme.getBaseColor("title").getRgb() == (0, 255, 0, 255)
assert theme.getBaseColor("chapter").getRgb() == (255, 0, 0, 255)
assert theme.getBaseColor("scene").getRgb() == (0, 0, 255, 255)
assert theme.getBaseColor("note").getRgb() == (255, 255, 0, 255)
# Load Default Dark Theme
# =======================
CONFIG.guiTheme = "default_dark"
assert mainTheme.loadTheme() is True
# Check a few values
assert mainTheme._guiPalette.color(
QPalette.ColorRole.Window).getRgb() == (54, 54, 54, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.WindowText).getRgb() == (204, 204, 204, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.Base).getRgb() == (62, 62, 62, 255)
assert mainTheme._guiPalette.color(
QPalette.ColorRole.AlternateBase).getRgb() == (78, 78, 78, 255)
# qtbot.stop()
@pytest.mark.gui
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
"""Test the syntax part of the class."""
mainTheme = SHARED.theme
# List Themes
# ===========
# Block the reading of the files
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert mainTheme.listThemes() == []
# Load the syntax info
syntaxList = mainTheme.listSyntax()
assert syntaxList[0] == ("default_dark", "Default Dark")
assert syntaxList[1] == ("default_light", "Default Light")
# A second call should returned the cached list
assert mainTheme.listSyntax() == mainTheme._syntaxList
# Check handling of broken theme settings
availSyntax = mainTheme._availSyntax
mainTheme._availSyntax = {}
CONFIG.guiSyntax = "not_a_syntax"
assert mainTheme.loadSyntax() is False
mainTheme._availSyntax = availSyntax
# Check handling of unreadable file
CONFIG.guiSyntax = "default_light"
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert mainTheme.loadSyntax() is False
# Load Default Light Syntax
# =========================
# Load the default syntax
CONFIG.guiSyntax = "default_light"
assert mainTheme.loadSyntax() is True
# Check some values
assert mainTheme.syntaxMeta.name == "Default Light"
assert mainTheme.syntaxTheme.back == QColor(255, 255, 255)
assert mainTheme.syntaxTheme.text == QColor(0, 0, 0)
assert mainTheme.syntaxTheme.link == QColor(0, 0, 200)
# Load Default Dark Theme
# =======================
# Load the default syntax
CONFIG.guiSyntax = "default_dark"
assert mainTheme.loadSyntax() is True
# Check some values
assert mainTheme.syntaxMeta.name == "Default Dark"
assert mainTheme.syntaxTheme.back == QColor(42, 42, 42)
assert mainTheme.syntaxTheme.text == QColor(204, 204, 204)
assert mainTheme.syntaxTheme.link == QColor(102, 153, 204)
# qtbot.stop()
@pytest.mark.gui
def testGuiTheme_IconThemes(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
"""Test the icon cache class."""
iconCache = SHARED.theme.iconCache
# Load Theme
# ==========
# Invalid theme name
availThemes = iconCache._availThemes
iconCache._availThemes = {}
assert iconCache.loadTheme("not_a_theme") is False
iconCache._availThemes = availThemes
# Check handling of unreadable file
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert iconCache.loadTheme("material_rounded_normal") is False
# Load working theme file
assert iconCache.loadTheme("material_rounded_normal") is True
assert iconCache.themeMeta.name == "Material Symbols - Rounded"
# Load with project colour override
purple = iconCache._svgColors["purple"]
assert iconCache._svgColors["root"] != purple
assert iconCache._svgColors["folder"] != purple
assert iconCache._svgColors["file"] != purple
assert iconCache._svgColors["title"] != purple
assert iconCache._svgColors["chapter"] != purple
assert iconCache._svgColors["scene"] != purple
assert iconCache._svgColors["note"] != purple
assert theme.getRawBaseColor("root") == b"#ff0000"
assert theme.getRawBaseColor("folder") == b"#ff0000"
assert theme.getRawBaseColor("file") == b"#cccccc"
assert theme.getRawBaseColor("title") == b"#00ff00"
assert theme.getRawBaseColor("chapter") == b"#ff0000"
assert theme.getRawBaseColor("scene") == b"#0000ff"
assert theme.getRawBaseColor("note") == b"#ffff00"
# Reload with project override to purple, also for docs
CONFIG.iconColTree = "purple"
assert iconCache.loadTheme("material_rounded_normal") is True
assert iconCache._svgColors["root"] == purple
assert iconCache._svgColors["folder"] == purple
assert iconCache._svgColors["file"] == purple
assert iconCache._svgColors["title"] == purple
assert iconCache._svgColors["chapter"] == purple
assert iconCache._svgColors["scene"] == purple
assert iconCache._svgColors["note"] == purple
CONFIG.iconColDocs = False
theme.loadTheme(force=True)
# Change some colours
iconCache.setIconColor("root", QColor(255, 255, 255))
assert iconCache._svgColors["root"] != purple
assert iconCache._svgColors["root"] == b"#ffffff"
assert theme.getBaseColor("root").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("folder").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("file").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("title").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("chapter").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("scene").getRgb() == (255, 0, 255, 255)
assert theme.getBaseColor("note").getRgb() == (255, 0, 255, 255)
# List Themes
# ===========
# Load error returns empty list
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
themes = iconCache.listThemes()
assert themes == []
# Successful read
themes = iconCache.listThemes()
assert len(themes) > 1
assert "material_rounded_normal" in dict(themes)
# Load error doesn't matter on second read since list is cached
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert iconCache.listThemes() == themes
# qtbot.stop()
assert theme.getRawBaseColor("root") == b"#ff00ff"
assert theme.getRawBaseColor("folder") == b"#ff00ff"
assert theme.getRawBaseColor("file") == b"#ff00ff"
assert theme.getRawBaseColor("title") == b"#ff00ff"
assert theme.getRawBaseColor("chapter") == b"#ff00ff"
assert theme.getRawBaseColor("scene") == b"#ff00ff"
assert theme.getRawBaseColor("note") == b"#ff00ff"
@pytest.mark.gui
def testGuiTheme_LoadIcons(qtbot, nwGUI):
def testGuiTheme_Methods(monkeypatch):
"""Test other themes methods."""
theme = GuiTheme()
theme.iconCache = MagicMock()
CONFIG.darkTheme = DEF_GUI_DARK
CONFIG.themeMode = nwTheme.DARK
# Init theme
assert theme.colourThemes == {}
theme.initThemes()
assert theme._meta.name == "Default Dark Theme"
# Text width
theme.guiFont = QFontDatabase.systemFont(QFontDatabase.SystemFont.GeneralFont)
assert theme.getTextWidth("MMMMM") > theme.getTextWidth("MMM")
font = QFont(theme.guiFont)
font.setPointSizeF(0.5*font.pointSizeF())
assert theme.getTextWidth("MMMMM", font) < theme.getTextWidth("MMMMM")
# Detect desktop mode Qt 6.5+
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060500)
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Light)
assert theme.isDesktopDarkMode() is False
mp.setattr(QStyleHints, "colorScheme", lambda *a: Qt.ColorScheme.Dark)
assert theme.isDesktopDarkMode() is True
# Detect desktop mode Qt 6.4
mockWhite = Mock()
mockWhite.color.return_value = QColor(255, 255, 255)
mockBlack = Mock()
mockBlack.color.return_value = QColor(0, 0, 0)
with monkeypatch.context() as mp:
mp.setattr(CONFIG, "verQtValue", 0x060400)
mp.setattr(QPalette, "window", lambda *a: mockWhite)
mp.setattr(QPalette, "windowText", lambda *a: mockBlack)
assert theme.isDesktopDarkMode() is False
mp.setattr(QPalette, "window", lambda *a: mockBlack)
mp.setattr(QPalette, "windowText", lambda *a: mockWhite)
assert theme.isDesktopDarkMode() is True
# Stylesheets
assert theme.getStyleSheet(STYLES_FLAT_TABS) != ""
assert theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) != ""
assert theme.getStyleSheet(STYLES_BIG_TOOLBUTTON) != ""
assert theme.getStyleSheet("stuff") == ""
@pytest.mark.gui
def testGuiTheme_ScanIcons(monkeypatch):
"""Test the icon theme scanning."""
theme = GuiTheme()
# Load built-in themes
files = []
_listContent(files, CONFIG.assetPath("icons"), ".icons")
assert len(files) > 0
# Block reading theme files
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
theme.iconCache._scanThemes(files)
assert theme.iconCache.iconThemes == {}
# Read all themes correctly
theme.iconCache._scanThemes(files)
assert len(theme.iconCache.iconThemes) > 0
@pytest.mark.gui
def testGuiTheme_IconThemes(monkeypatch):
"""Test loading icon theme."""
CONFIG.lightTheme = DEF_GUI_LIGHT
CONFIG.themeMode = nwTheme.LIGHT
CONFIG.iconTheme = DEF_ICONS
theme = GuiTheme()
# Init should load default theme
theme.initThemes()
assert theme.iconCache._meta.name == "Material Symbols - Rounded"
assert DEF_ICONS in theme.iconCache.iconThemes
# Load default theme directly
theme.iconCache._meta = ThemeMeta()
theme.iconCache.loadTheme("DEF_ICONS")
assert theme.iconCache._meta.name == "Material Symbols - Rounded"
# Failed loading should load nothing
theme.iconCache._meta = ThemeMeta()
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
theme.iconCache.loadTheme("DEF_ICONS")
assert theme.iconCache._meta.name == ""
# Reload with non-existent theme should reload default
theme.iconCache._meta = ThemeMeta()
theme.iconCache.loadTheme("not_a_theme")
assert theme.iconCache._meta.name == "Material Symbols - Rounded"
# If default theme is missing, load nothing
del theme.iconCache._allThemes[DEF_ICONS]
theme.iconCache._meta = ThemeMeta()
theme.iconCache.loadTheme("not_a_theme")
assert theme.iconCache._meta.name == ""
@pytest.mark.gui
def testGuiTheme_LoadIcons():
"""Test the icon cache class."""
iconCache = SHARED.theme.iconCache
assert iconCache.loadTheme("material_rounded_normal") is True
theme = GuiTheme()
theme.initThemes()
iconCache = theme.iconCache
# Load Icons
# ==========
@@ -455,14 +513,13 @@ def testGuiTheme_LoadIcons(qtbot, nwGUI):
nwItemType.NO_TYPE, nwItemClass.NOVEL, nwItemLayout.DOCUMENT, hLevel="H0"
) == iconCache._noIcon
# qtbot.stop()
@pytest.mark.gui
def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
def testGuiTheme_LoadDecorations(monkeypatch):
"""Test the icon cache class."""
iconCache = SHARED.theme.iconCache
assert iconCache.loadTheme("material_rounded_normal") is True
theme = GuiTheme()
theme.initThemes()
iconCache = theme.iconCache
# Load Decorations
# ================
@@ -520,4 +577,114 @@ def testGuiTheme_LoadDecorations(qtbot, monkeypatch, nwGUI):
assert iconCache.getHeaderDecorationNarrow(5) == iconCache._headerDecNarrow[5]
assert iconCache.getHeaderDecorationNarrow(6) == iconCache._headerDecNarrow[5]
# qtbot.stop()
THEMES = []
_listContent(THEMES, CONFIG.assetPath("themes"), ".conf")
@pytest.mark.gui
@pytest.mark.parametrize("theme", [a.stem for a in THEMES])
def testGuiTheme_CheckTheme(theme):
"""Test loading all themes."""
themes = GuiTheme()
themes.iconCache = MagicMock()
themes._scanThemes(THEMES)
assert theme in themes.colourThemes
current = themes.colourThemes[theme]
if current.dark:
CONFIG.darkTheme = theme
CONFIG.themeMode = nwTheme.DARK
else:
CONFIG.lightTheme = theme
CONFIG.themeMode = nwTheme.LIGHT
# Check loading
themes.loadTheme()
assert themes._meta.name == current.name
assert themes.isDarkTheme == current.dark
# Check completeness
parser = ConfigParser()
parser.read(current.path, encoding="utf-8")
sections = ["Main", "Base", "Project", "Palette", "GUI", "Syntax"]
assert sorted(parser.sections()) == sorted(sections)
structure = {
"Main": [
"name", "mode", "author", # The rest are not required
],
"Base": [
"base", "default", "faded", "red", "orange", "yellow", "green",
"cyan", "blue", "purple",
],
"Project": [
"root", "folder", "file", "title", "chapter", "scene", "note",
"active", "inactive", "disabled",
],
"Palette": [
"window", "windowtext", "base", "alternatebase", "text",
"tooltipbase", "tooltiptext", "button", "buttontext", "brighttext",
"highlight", "highlightedtext", "link", "linkvisited", "accent",
],
"GUI": [
"helptext", "fadedtext", "errortext",
],
"Syntax": [
"background", "text", "line", "link", "headertext", "headertag",
"emphasis", "dialog", "altdialog", "hidden", "note", "shortcode",
"keyword", "tag", "value", "optional", "spellcheckline",
"errorline", "replacetag", "modifier", "texthighlight",
],
}
optional = ["credit", "url"]
missing = []
for section, options in structure.items():
missing.extend(opt for opt in options if opt not in parser[section])
assert missing == [], "Missing options in theme file"
# Check deprecated
deprecated = []
for section in sections:
deprecated.extend(
opt for opt in parser[section]
if opt not in structure[section] and opt not in optional
)
assert deprecated == [], "Deprecated options in theme file"
ICONS = []
_listContent(ICONS, CONFIG.assetPath("icons"), ".icons")
@pytest.mark.gui
@pytest.mark.parametrize("icons", [a.stem for a in ICONS])
def testGuiTheme_CheckIcons(icons, tstPaths):
"""Test loading all icons."""
keysFile: Path = tstPaths.filesDir / "all_icons.json"
iconKeys = json.loads(keysFile.read_text(encoding="utf-8"))
assert isinstance(iconKeys, list)
CONFIG.lightTheme = DEF_GUI_LIGHT
CONFIG.themeMode = nwTheme.LIGHT
themes = GuiTheme()
themes.initThemes()
iconCache = themes.iconCache
assert icons in iconCache.iconThemes
current = iconCache.iconThemes[icons]
CONFIG.iconTheme = icons
# Check loading
iconCache.loadTheme(icons)
assert iconCache._meta.name == current.name
# Check completeness
missing = [key for key in iconKeys if key not in iconCache._svgData]
assert missing == [], "Missing keys in icons file"
# Check deprecated
deprecated = [key for key in iconCache._svgData if key not in iconKeys]
assert deprecated == [], "Deprecated keys in icons file"
+9
View File
@@ -157,6 +157,15 @@ def testTextPatterns_Markdown():
assert allMatches(regEx, "one ~two~~ three") == []
assert allMatches(regEx, "one~~two~~three") == []
# Mark
regEx = REGEX_PATTERNS.markdownMark
assert allMatches(regEx, "one ==two== three") == [
[("==two==", 4, 11), ("==", 4, 6), ("two", 6, 9), ("==", 9, 11)]
]
assert allMatches(regEx, "one ==two= three") == []
assert allMatches(regEx, "one =two== three") == []
assert allMatches(regEx, "one==two==three") == []
@pytest.mark.core
def testTextPatterns_ShortcodesPlain():