Improve test coverage of build tools (#1466)

This commit is contained in:
Veronica Berglyd Olsen
2023-07-18 18:14:07 +02:00
committed by GitHub
22 changed files with 2493 additions and 143 deletions
+27 -21
View File
@@ -39,7 +39,6 @@ from novelwriter.enum import nwBuildFmt
from novelwriter.error import logException
from novelwriter.common import checkUuid, isHandle, jsonEncode
from novelwriter.constants import nwFiles, nwHeadFmt
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__)
@@ -208,15 +207,15 @@ class BuildSettings:
def getInt(self, key: str) -> int:
"""Type safe value access for integers."""
value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1]))
if isinstance(value, int):
return value
if isinstance(value, (int, float)):
return int(value)
return 0
def getFloat(self, key: str) -> float:
"""Type safe value access for floats."""
value = self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1]))
if isinstance(value, float):
return value
if isinstance(value, (int, float)):
return float(value)
return 0.0
##
@@ -282,8 +281,8 @@ class BuildSettings:
self._changed = True
return
def setSkipRoot(self, tHandle: str, state: bool):
"""Set a specific root folder as skipped or not."""
def setAllowRoot(self, tHandle: str, state: bool):
"""Set a specific root folder as allowed or not."""
if state is True:
self._skipRoot.discard(tHandle)
self._changed = True
@@ -299,7 +298,7 @@ class BuildSettings:
definition = SETTINGS_TEMPLATE[key]
if not isinstance(value, definition[0]):
return False
if len(definition) == 4:
if len(definition) == 4 and isinstance(value, (int, float)):
value = min(max(value, definition[2]), definition[3])
self._changed = value != self._settings[key]
self._settings[key] = value
@@ -328,24 +327,30 @@ class BuildSettings:
incNotes = bool(self.getBool("filter.includeNotes"))
incInactive = bool(self.getBool("filter.includeInactive"))
postponed = []
def allowRoot(rHandle):
if rHandle in postponed and rHandle in result and rHandle is not None:
result[rHandle] = (True, FilterMode.ROOT)
postponed.remove(rHandle)
for item in project.tree:
tHandle = item.itemHandle
if not tHandle:
continue
if not isinstance(item, NWItem):
result[tHandle] = (False, FilterMode.UNKNOWN)
if tHandle is None:
continue
if item.isInactiveClass() or (item.itemRoot in self._skipRoot):
result[tHandle] = (False, FilterMode.SKIPPED)
continue
if withRoots and item.isRootType():
result[tHandle] = (True, FilterMode.ROOT)
result[tHandle] = (False, FilterMode.SKIPPED)
postponed.append(tHandle)
continue
if not item.isFileType():
result[tHandle] = (False, FilterMode.SKIPPED)
continue
if tHandle in self._included:
result[tHandle] = (True, FilterMode.INCLUDED)
allowRoot(item.itemRoot)
continue
if tHandle in self._excluded:
result[tHandle] = (False, FilterMode.EXCLUDED)
@@ -361,6 +366,8 @@ class BuildSettings:
isAllowed = byActive and byLayout
result[tHandle] = (isAllowed, FilterMode.FILTERED)
if isAllowed:
allowRoot(item.itemRoot)
return result
@@ -401,7 +408,7 @@ class BuildSettings:
self.setLastPath(data.get("path", None))
self.setLastBuildName(data.get("build", ""))
buildFmt = str(data.get("build", ""))
buildFmt = str(data.get("format", ""))
if buildFmt in nwBuildFmt.__members__:
self.setLastFormat(nwBuildFmt[buildFmt])
@@ -454,14 +461,13 @@ class BuildCollection:
build.unpack(self._builds[buildID])
return build
def setBuild(self, build: BuildSettings) -> bool:
def setBuild(self, build: BuildSettings):
"""Set build settings data in the collection."""
if not isinstance(build, BuildSettings):
return False
buildID = build.buildID
self._builds[buildID] = build.pack()
self._saveCollection()
return True
if isinstance(build, BuildSettings):
buildID = build.buildID
self._builds[buildID] = build.pack()
self._saveCollection()
return
def removeBuild(self, buildID: str):
"""Remove the a build from the collection."""
+27 -31
View File
@@ -33,6 +33,7 @@ from PyQt5.QtGui import QFont, QFontInfo
from novelwriter import CONFIG
from novelwriter.enum import nwBuildFmt
from novelwriter.error import formatException, logException
from novelwriter.core.item import NWItem
from novelwriter.core.tomd import ToMarkdown
from novelwriter.core.toodt import ToOdt
from novelwriter.core.tohtml import ToHtml
@@ -96,15 +97,13 @@ class NWBuildDocument:
def queueAll(self):
"""Queue all document as defined by the build settings."""
self._queue = []
filtered = self._build.buildItemFilter(self._project)
noteTitles = self._build.getBool("text.addNoteHeadings")
for item in self._project.tree:
if not item.itemHandle:
continue
if filtered.get(item.itemHandle, False):
self._queue.append(item.itemHandle)
elif item.isRootType() and noteTitles:
self._queue.append(item.itemHandle)
return
def iterBuild(self, path: Path, bFormat: nwBuildFmt) -> Iterable[tuple[int, bool]]:
@@ -155,9 +154,6 @@ class NWBuildDocument:
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs()
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
@@ -165,6 +161,9 @@ class NWBuildDocument:
else:
yield i, False
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs()
self._error = None
self._cache = makeObj
@@ -217,8 +216,6 @@ class NWBuildDocument:
filtered = self._setupBuild(makeObj)
makeObj.setKeepMarkdown(True)
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
for i, tHandle in enumerate(self._queue):
self._error = None
@@ -227,6 +224,9 @@ class NWBuildDocument:
else:
yield i, False
if self._build.getBool("format.replaceTabs"):
makeObj.replaceTabs(nSpaces=4, spaceChar=" ")
self._error = None
self._cache = makeObj
@@ -299,30 +299,26 @@ class NWBuildDocument:
def _doBuild(self, bldObj: Tokenizer, tHandle: str, convert: bool = True) -> bool:
"""Build a single document and add it to the build object."""
tItem = self._project.tree[tHandle]
if tItem is None:
self._error = f"Build: Unknown item '{tHandle}'"
logger.error(self._error)
return False
if isinstance(tItem, NWItem):
try:
if tItem.isRootType() and not tItem.isNovelLike():
bldObj.addRootHeading(tHandle)
if convert:
bldObj.doConvert()
elif tItem.isFileType():
bldObj.setText(tHandle)
bldObj.doPreProcessing()
bldObj.tokenizeText()
bldObj.doHeaders()
if convert:
bldObj.doConvert()
else:
logger.info(f"Build: Skipping '{tHandle}'")
try:
if tItem.isRootType() and not tItem.isNovelLike():
bldObj.addRootHeading(tItem.itemHandle)
if convert:
bldObj.doConvert()
elif tItem.isFileType():
bldObj.setText(tHandle)
bldObj.doPreProcessing()
bldObj.tokenizeText()
bldObj.doHeaders()
if convert:
bldObj.doConvert()
else:
logger.info(f"Build: Skipping '{tHandle}'")
except Exception:
self._error = f"Build: Failed to build '{tHandle}'"
logger.error(self._error)
return False
except Exception:
self._error = f"Build: Failed to build '{tHandle}'"
logger.error(self._error)
return False
return True
+1 -1
View File
@@ -339,7 +339,7 @@ class ToHtml(Tokenizer):
},
"text": {
"css": self.getStyleSheet(),
"html": [page.rstrip("\n").split("\n") for page in self.fullHTML],
"html": [t.replace("\t", "	").rstrip().split("\n") for t in self.fullHTML],
}
}
with open(path, mode="w", encoding="utf-8") as fObj:
+2
View File
@@ -186,6 +186,8 @@ class ToMarkdown(Tokenizer):
"""Replace tabs with spaces."""
spaces = spaceChar*nSpaces
self._fullMD = [p.replace("\t", spaces) for p in self._fullMD]
if self._keepMarkdown:
self._allMarkdown = [p.replace("\t", spaces) for p in self._allMarkdown]
return
##
+7 -13
View File
@@ -59,8 +59,7 @@ class NPagedSideBar(QToolBar):
return
def setLabelColor(self, color):
"""Set the text color for the labels.
"""
"""Set the text color for the labels."""
if isinstance(color, list):
self._labelCol = QColor(*color)
elif isinstance(color, QColor):
@@ -68,8 +67,7 @@ class NPagedSideBar(QToolBar):
return
def addSeparator(self):
"""Add a spacer widget.
"""
"""Add a spacer widget."""
spacer = QWidget(self)
spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
spacer.setFixedHeight(self._spacerHeight)
@@ -77,16 +75,14 @@ class NPagedSideBar(QToolBar):
return
def addLabel(self, text):
"""Add a new label to the toolbar.
"""
"""Add a new label to the toolbar."""
label = NPagedToolLabel(self, self._labelCol)
label.setText(text)
self.insertWidget(self._stretchAction, label)
return
def addButton(self, text, buttonId=-1):
"""Add a new button to the toolbar.
"""
"""Add a new button to the toolbar."""
button = NPagedToolButton(self)
button.setText(text)
@@ -99,8 +95,7 @@ class NPagedSideBar(QToolBar):
return action
def setSelected(self, buttonId):
"""Set the selected button.
"""
"""Set the selected button."""
self._group.button(buttonId).setChecked(True)
return
@@ -110,8 +105,7 @@ class NPagedSideBar(QToolBar):
@pyqtSlot("QAbstractButton*")
def _buttonClicked(self, button):
"""A button was clicked in the group, emit its id.
"""
"""A button was clicked in the group, emit its id."""
buttonId = self._group.id(button)
if buttonId != -1:
self.buttonClicked.emit(buttonId)
@@ -155,7 +149,7 @@ class NPagedToolButton(QToolButton):
height = self.height()
palette = self.palette()
if opt.state & QStyle.State_MouseOver:
if opt.state & QStyle.State_MouseOver == QStyle.State_MouseOver:
backCol = palette.alternateBase()
paint.setBrush(backCol)
paint.setOpacity(0.5)
+8 -2
View File
@@ -45,11 +45,15 @@ class NSwitchBox(QScrollArea):
self._hSwitch = baseSize
self._wSwitch = 2*self._hSwitch
self._sIcon = baseSize
self._widgets = []
self.clear()
return
def clear(self):
"""Rebuild the content of the core widget."""
self._index = 0
self._widgets = []
self._content = QGridLayout()
self._content.setColumnStretch(1, 1)
@@ -69,6 +73,7 @@ class NSwitchBox(QScrollArea):
font.setBold(True)
label.setFont(font)
self._content.addWidget(label, self._index, 0, 1, 3, Qt.AlignLeft)
self._widgets.append(label)
self._bumpIndex()
return
@@ -87,16 +92,17 @@ class NSwitchBox(QScrollArea):
switch.toggled.connect(lambda state: self._emitSwitchSignal(identifier, state))
self._content.addWidget(switch, self._index, 2, Qt.AlignRight)
self._widgets.append(switch)
self._bumpIndex()
return
def addSeparator(self):
"""Add a blank entry in the content box.
"""
"""Add a blank entry in the content box."""
spacer = QWidget()
spacer.setFixedHeight(int(0.5*self._sIcon))
self._content.addWidget(spacer, self._index, 0, 1, 3, Qt.AlignLeft)
self._widgets.append(spacer)
self._bumpIndex()
return
+1 -15
View File
@@ -229,7 +229,7 @@ class GuiManuscriptBuild(QDialog):
self.btnReset.clicked.connect(self._doResetBuildName)
self.btnBrowse.clicked.connect(self._doSelectPath)
self.dlgButtons.clicked.connect(self._dialogButtonClicked)
self.listFormats.itemSelectionChanged.connect(self._formatSelectionChanged)
self.listFormats.itemSelectionChanged.connect(self._resetProgress)
logger.debug("Ready: GuiManuscriptBuild")
@@ -256,12 +256,6 @@ class GuiManuscriptBuild(QDialog):
# Private Slots
##
@pyqtSlot(str, bool)
def _applyBuildOptions(self, key: str, state: bool):
"""Set the build options for the build."""
self._build.setValue(key, state)
return
@pyqtSlot("QAbstractButton*")
def _dialogButtonClicked(self, button: QAbstractButton):
"""Handle button clicks from the dialog button box."""
@@ -292,14 +286,6 @@ class GuiManuscriptBuild(QDialog):
self._build.setLastBuildName(bName)
return
@pyqtSlot()
def _formatSelectionChanged(self):
"""The user selected a different format, so we reset the
progress bar.
"""
self.buildProgress.setValue(0)
return
@pyqtSlot()
def _resetProgress(self):
"""Set the progress bar back to 0."""
+7 -6
View File
@@ -32,11 +32,11 @@ from datetime import datetime
from PyQt5.QtGui import QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from PyQt5.QtWidgets import (
QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton,
QSplitter, QTextBrowser, QToolButton, QVBoxLayout, QWidget, qApp
)
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from novelwriter import CONFIG
from novelwriter.error import logException
@@ -300,7 +300,6 @@ class GuiManuscript(QDialog):
for step, _ in docBuild.iterBuildHTML(None):
self.docPreview.buildStep(step + 1)
qApp.processEvents()
qApp.thread().msleep(5)
buildObj = docBuild.lastBuild
assert isinstance(buildObj, ToHtml)
@@ -528,10 +527,11 @@ class _PreviewWidget(QTextBrowser):
def setTextFont(self, family: str, size: int):
"""Set the text font properties."""
font = QFont()
font.setFamily(family)
font.setPointSize(size)
self.setFont(font)
if family:
font = QFont()
font.setFamily(family)
font.setPointSize(size)
self.setFont(font)
return
##
@@ -617,6 +617,7 @@ class _PreviewWidget(QTextBrowser):
self.document().print(printer)
qApp.restoreOverrideCursor()
return
##
# Private Slots
##
+21 -24
View File
@@ -118,8 +118,8 @@ class GuiBuildSettings(QDialog):
# Create Tabs
self.optTabSelect = _FilterTab(self, self._build)
self.optTabHeadings = _HeadingsTab(self, self._build)
self.optTabFormat = _FormatTab(self, self._build)
self.optTabContent = _ContentTab(self, self._build)
self.optTabFormat = _FormatTab(self, self._build)
self.optTabOutput = _OutputTab(self, self._build)
# Add Tabs
@@ -408,9 +408,8 @@ class _FilterTab(QWidget):
def mainSplitSizes(self) -> tuple[int, int]:
"""Extract the sizes of the main splitter."""
sizes = self.mainSplit.sizes()
if len(sizes) < 2:
return 0, 0
return CONFIG.rpxInt(sizes[0]), CONFIG.rpxInt(sizes[1])
m, n = (sizes[0], sizes[1]) if len(sizes) >= 2 else (0, 0)
return CONFIG.rpxInt(m), CONFIG.rpxInt(n)
##
# Slots
@@ -423,7 +422,7 @@ class _FilterTab(QWidget):
self._build.setValue(key[4:], state)
self._setTreeItemMode()
elif key.startswith("root:"):
self._build.setSkipRoot(key[5:], state)
self._build.setAllowRoot(key[5:], state)
self._populateTree()
return
@@ -436,7 +435,7 @@ class _FilterTab(QWidget):
logger.debug("Building project tree")
self._treeMap = {}
self.optTree.clear()
for nwItem in self.theProject.tree:
for nwItem in self.theProject.getProjectItems():
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
@@ -527,18 +526,16 @@ class _FilterTab(QWidget):
def _setSelectedMode(self, mode: int):
"""Set the mode for the selected items."""
for item in self.optTree.selectedItems():
if not isinstance(item, QTreeWidgetItem):
continue
tHandle = item.data(self.C_DATA, self.D_HANDLE)
isFile = item.data(self.C_DATA, self.D_FILE)
if isFile:
if mode == self.F_FILTERED:
self._build.setFiltered(tHandle)
elif mode == self.F_INCLUDED:
self._build.setIncluded(tHandle)
elif mode == self.F_EXCLUDED:
self._build.setExcluded(tHandle)
if isinstance(item, QTreeWidgetItem):
tHandle = item.data(self.C_DATA, self.D_HANDLE)
isFile = item.data(self.C_DATA, self.D_FILE)
if isFile:
if mode == self.F_FILTERED:
self._build.setFiltered(tHandle)
elif mode == self.F_INCLUDED:
self._build.setIncluded(tHandle)
elif mode == self.F_EXCLUDED:
self._build.setExcluded(tHandle)
self._setTreeItemMode()
@@ -592,7 +589,7 @@ class _HeadingsTab(QWidget):
# Title Heading
self.lblTitle = QLabel(self._build.getLabel("headings.fmtTitle"))
self.fmtTitle = QLineEdit("")
self.fmtTitle.setEnabled(False)
self.fmtTitle.setReadOnly(True)
self.btnTitle = QToolButton()
self.btnTitle.setIcon(self.mainTheme.getIcon("edit"))
self.btnTitle.clicked.connect(lambda: self._editHeading(self.EDIT_TITLE))
@@ -608,7 +605,7 @@ class _HeadingsTab(QWidget):
# Chapter Heading
self.lblChapter = QLabel(self._build.getLabel("headings.fmtChapter"))
self.fmtChapter = QLineEdit("")
self.fmtChapter.setEnabled(False)
self.fmtChapter.setReadOnly(True)
self.btnChapter = QToolButton()
self.btnChapter.setIcon(self.mainTheme.getIcon("edit"))
self.btnChapter.clicked.connect(lambda: self._editHeading(self.EDIT_CHAPTER))
@@ -624,7 +621,7 @@ class _HeadingsTab(QWidget):
# Unnumbered Chapter Heading
self.lblUnnumbered = QLabel(self._build.getLabel("headings.fmtUnnumbered"))
self.fmtUnnumbered = QLineEdit("")
self.fmtUnnumbered.setEnabled(False)
self.fmtUnnumbered.setReadOnly(True)
self.btnUnnumbered = QToolButton()
self.btnUnnumbered.setIcon(self.mainTheme.getIcon("edit"))
self.btnUnnumbered.clicked.connect(lambda: self._editHeading(self.EDIT_UNNUM))
@@ -641,7 +638,7 @@ class _HeadingsTab(QWidget):
sceneHideTip = self._build.getLabel("headings.hideScene")
self.lblScene = QLabel(self._build.getLabel("headings.fmtScene"))
self.fmtScene = QLineEdit("")
self.fmtScene.setEnabled(False)
self.fmtScene.setReadOnly(True)
self.btnScene = QToolButton()
self.btnScene.setIcon(self.mainTheme.getIcon("edit"))
self.btnScene.clicked.connect(lambda: self._editHeading(self.EDIT_SCENE))
@@ -668,7 +665,7 @@ class _HeadingsTab(QWidget):
sectionHideTip = self._build.getLabel("headings.hideSection")
self.lblSection = QLabel(self._build.getLabel("headings.fmtSection"))
self.fmtSection = QLineEdit("")
self.fmtSection.setEnabled(False)
self.fmtSection.setReadOnly(True)
self.btnSection = QToolButton()
self.btnSection.setIcon(self.mainTheme.getIcon("edit"))
self.btnSection.clicked.connect(lambda: self._editHeading(self.EDIT_SECTION))
@@ -814,7 +811,7 @@ class _HeadingsTab(QWidget):
def _saveFormat(self):
"""Save the format from the edit text box."""
heading = self._editing
text = self.editTextBox.toPlainText().replace("\n", "//")
text = self.editTextBox.toPlainText().strip().replace("\n", "//")
if heading == self.EDIT_TITLE:
self.fmtTitle.setText(text)
self._build.setValue("headings.fmtTitle", text)
+2 -2
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-03 18:18:20">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1509" autoCount="237" editTime="75095">
<novelWriterXML appVersion="2.0.7" hexVersion="0x020007f0" fileVersion="1.5" fileRevision="1" timeStamp="2023-06-19 08:41:53">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1510" autoCount="237" editTime="75111">
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
+1
View File
@@ -153,6 +153,7 @@ def nwGUI(qtbot, monkeypatch, functionFixture):
nwGUI = main(["--testmode", f"--config={_TMP_CONF}", f"--data={_TMP_CONF}"])
qtbot.addWidget(nwGUI)
resetConfigVars()
nwGUI.docEditor.initEditor()
nwGUI.show()
qtbot.wait(20)
@@ -63,9 +63,9 @@ a {color: rgb(66, 113, 174);}
<p>Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus.</p>
<h1 style='page-break-before: always;'>Why do we use it?</h1>
<p class='comment'><strong>Comment:</strong> Exctracted from the lipsum.com website.</p>
<p>&#09;It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</p>
<p>&#09;The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.</p>
<p>&#09;Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.</p>
<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>
<h1 style='page-break-before: always;'>Chapter: Chapter Two</h1>
<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>
<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>
@@ -0,0 +1,163 @@
{
"meta": {
"projectName": "Lorem Ipsum",
"novelTitle": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1687620769,
"buildTimeStr": "2023-06-24 17:32:49"
},
"text": {
"css": [
"body {font-family: 'Arial'; font-size: 12pt;}",
"p {text-align: justify; line-height: 150%; margin-top: 0.00em; margin-bottom: 0.76em;}",
"h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.30em; margin-bottom: 0.65em;}",
"h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.09em; margin-bottom: 0.65em;}",
"h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}",
"h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.76em; margin-bottom: 0.65em;}",
".title {font-size: 2.5em; margin-top: 1.30em; margin-bottom: 0.65em;}",
".sep, .skip {text-align: center; margin-top: 1.30em; margin-bottom: 1.30em;}",
"a {color: rgb(66, 113, 174);}",
".tags {color: rgb(245, 135, 31); font-weight: bold;}",
".break {text-align: left;}",
".synopsis {font-style: italic;}",
".comment {font-style: italic; color: rgb(100, 100, 100);}"
],
"html": [
[
"<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;'>\u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d</p>",
"<p style='text-align: center;'>\u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d</p>"
],
[
"<p class='comment'><strong>Comment:</strong> Exctracted from the lipsum.com website.</p>",
"<p>Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of \u201cde Finibus Bonorum et Malorum\u201d (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, \u201cLorem ipsum dolor sit amet..\u201d, comes from a line in section 1.10.32.</p>",
"<p>The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from \u201cde Finibus Bonorum et Malorum\u201d by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.</p>"
],
[
"<h1 style='page-break-before: always;'>Prologue</h1>",
"<p class='synopsis'><strong>Synopsis:</strong> Explanation from the lipsum.com website.</p>",
"<p><em>Lorem Ipsum</em> is simply dummy text 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.</p>"
],
[
"<h1 class='title' style='text-align: center; page-break-before: always;'>Title: Act One</h1>",
"<p style='text-align: center;'>\u201cFusce maximus felis libero\u201d</p>"
],
[
"<h1 style='page-break-before: always;'>Chapter: Chapter One</h1>",
"<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>",
"<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>",
"<p class='synopsis'><strong>Synopsis:</strong> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque at aliquam quam.</p>",
"<p>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.</p>"
],
[
"<h2>Scene: Scene One</h2>",
"<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>",
"<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>",
"<p class='synopsis'><strong>Synopsis:</strong> 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.</p>",
"<p>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.</p>",
"<p>Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex.</p>",
"<h3>Section: Scene One, Section Two</h3>",
"<p>Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst.</p>",
"<p>Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend.</p>"
],
[
"<h2>Scene: Scene Two</h2>",
"<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>",
"<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>",
"<p class='synopsis'><strong>Synopsis:</strong> 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.</p>",
"<p>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.</p>",
"<p>Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis.</p>",
"<p>Nulla accumsan ante in pulvinar efficitur. Nulla non velit quis urna hendrerit bibendum. Suspendisse ultrices ante eu justo malesuada, sed fermentum enim rutrum. Nunc fermentum pharetra felis, vitae sollicitudin quam rutrum porta. Aliquam fringilla velit a mi laoreet, et luctus est rutrum. In gravida non ipsum sit amet tempus. Curabitur et eleifend purus. Nulla facilisi.</p>",
"<h3>Section: Scene Two, Section Two</h3>",
"<p>Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu.</p>",
"<p>Vivamus ut venenatis lectus. Phasellus nec elit id sem dictum ornare. Quisque feugiat, diam eget sagittis ultricies, orci turpis efficitur nisi, et fringilla justo odio nec nibh. In hac habitasse platea dictumst. Sed tempus bibendum feugiat. Etiam luctus mauris arcu, non interdum ipsum ultrices id. Vivamus blandit urna sit amet scelerisque vulputate. Quisque in metus eget massa rutrum dictum sit amet sed nulla. Vivamus vel efficitur dolor.</p>",
"<p>Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus.</p>"
],
[
"<h1 style='page-break-before: always;'>Why do we use it?</h1>",
"<p class='comment'><strong>Comment:</strong> Exctracted from the lipsum.com website.</p>",
"<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.</p>",
"<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.</p>",
"<p>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).</p>"
],
[
"<h1 style='page-break-before: always;'>Chapter: Chapter Two</h1>",
"<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>",
"<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>",
"<p class='synopsis'><strong>Synopsis:</strong> Curabitur a elit posuere, varius ex et, convallis neque. Phasellus sagittis pharetra sem vitae dapibus. Curabitur varius lorem non pulvinar congue.</p>",
"<p>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.</p>"
],
[
"<h2>Scene: Scene Three</h2>",
"<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>",
"<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>",
"<p class='synopsis'><strong>Synopsis:</strong> 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.</p>",
"<p>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.</p>",
"<p>Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis.</p>",
"<p>Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus.</p>",
"<p>Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac.</p>"
],
[
"<h2>Scene: Scene Four</h2>",
"<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>",
"<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>",
"<p class='synopsis'><strong>Synopsis:</strong> 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.</p>",
"<p>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.</p>",
"<p>Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum.</p>",
"<p>Etiam sagittis, erat vitae accumsan tempor, neque augue scelerisque nulla, ut ultrices justo urna sit amet augue. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean at pulvinar tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel porta quam. Nullam eu mauris mollis, vehicula justo vel, placerat sapien. Phasellus viverra elit et vestibulum pharetra. Vestibulum commodo fermentum leo, eu porta nisi aliquam eget. Nulla tempus porttitor nisi nec mollis. Nam non mollis turpis. Nam finibus leo a bibendum tincidunt. Donec commodo velit magna, ac semper sapien mattis id. Proin sem velit, lobortis quis ultricies id, pharetra et lectus. Vestibulum condimentum neque vitae mi dapibus mollis. Mauris luctus vel sapien vitae hendrerit.</p>",
"<p>Aenean vestibulum magna placerat fermentum tempus. Nam auctor condimentum nunc, in elementum quam ornare a. Etiam in ipsum elit. Proin pharetra, dolor sollicitudin pellentesque congue, lorem dolor ultricies magna, non iaculis risus nisl dictum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus vel euismod nibh, et lobortis dolor. Maecenas dui odio, gravida nec molestie ut, feugiat ut arcu. Pellentesque risus sapien, gravida a convallis quis, ullamcorper porttitor sapien.</p>",
"<p>Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui.</p>",
"<p>Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero.</p>"
],
[
"<h2>Scene: Scene Five</h2>",
"<p style='margin-bottom: 0;'><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></p>",
"<p style='margin-bottom: 0; margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></p>",
"<p class='synopsis'><strong>Synopsis:</strong> 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.</p>",
"<p>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.</p>",
"<p>In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere.</p>",
"<p>Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis.</p>",
"<p>Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue.</p>",
"<p>Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.</p>"
],
[
"<h1 class='title' style='text-align: center; page-break-before: always;'>Notes: Characters</h1>"
],
[
"<h1>Nobody Owens</h1>",
"<p style='margin-bottom: 0;'><span class='tags'>Tag:</span> <a name='tag_Bod'>Bod</a></p>",
"<p style='margin-top: 0;'><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></p>",
"<p>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>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>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><span class='tags'>Tag:</span> <a name='tag_Main'>Main</a></p>",
"<p>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>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><span class='tags'>Tag:</span> <a name='tag_Europe'>Europe</a></p>",
"<p>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>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>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>"
]
]
}
}
@@ -0,0 +1,215 @@
{
"meta": {
"projectName": "Lorem Ipsum",
"novelTitle": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
"buildTime": 1687621222,
"buildTimeStr": "2023-06-24 17:40:22"
},
"text": {
"nwd": [
[
"#! Lorem Ipsum",
"",
"**By lipsum.com**",
"",
"\u201cNeque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit\u2026\u201d",
"",
"\u201cThere is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain\u2026\u201d"
],
[
"",
"% Exctracted from the lipsum.com website.",
"",
"Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of \u201cde Finibus Bonorum et Malorum\u201d (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, \u201cLorem ipsum dolor sit amet..\u201d, comes from a line in section 1.10.32.",
"",
"The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from \u201cde Finibus Bonorum et Malorum\u201d by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham."
],
[
"##! Prologue",
"",
"% Synopsis:Explanation from the lipsum.com website.",
"",
"_Lorem Ipsum_ is simply dummy text 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."
],
[
"# Act One",
"",
"\u201cFusce maximus felis libero\u201d"
],
[
"## Chapter One",
"",
"@pov: Bod",
"@plot: Main",
"@location: Europe",
"",
"% 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."
],
[
"### 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.",
"",
"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.",
"",
"Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex.",
"",
"#### Scene One, Section Two",
"",
"Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst.",
"",
"Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend."
],
[
"### 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. ",
"",
"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.",
"",
"Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis.",
"",
"Nulla accumsan ante in pulvinar efficitur. Nulla non velit quis urna hendrerit bibendum. Suspendisse ultrices ante eu justo malesuada, sed fermentum enim rutrum. Nunc fermentum pharetra felis, vitae sollicitudin quam rutrum porta. Aliquam fringilla velit a mi laoreet, et luctus est rutrum. In gravida non ipsum sit amet tempus. Curabitur et eleifend purus. Nulla facilisi.",
"",
"#### Scene Two, Section Two",
"",
"Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu.",
"",
"Vivamus ut venenatis lectus. Phasellus nec elit id sem dictum ornare. Quisque feugiat, diam eget sagittis ultricies, orci turpis efficitur nisi, et fringilla justo odio nec nibh. In hac habitasse platea dictumst. Sed tempus bibendum feugiat. Etiam luctus mauris arcu, non interdum ipsum ultrices id. Vivamus blandit urna sit amet scelerisque vulputate. Quisque in metus eget massa rutrum dictum sit amet sed nulla. Vivamus vel efficitur dolor.",
"",
"Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus."
],
[
"##! Why do we use it?",
"",
"% Exctracted from the lipsum.com website.",
"",
" It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.",
"",
" The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.",
"",
" Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like)."
],
[
"## 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.",
"",
"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."
],
[
"### 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.",
"",
"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.",
"",
"Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis.",
"",
"Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus.",
"",
"Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac."
],
[
"### 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.",
"",
"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.",
"",
"Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum.",
"",
"Etiam sagittis, erat vitae accumsan tempor, neque augue scelerisque nulla, ut ultrices justo urna sit amet augue. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean at pulvinar tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel porta quam. Nullam eu mauris mollis, vehicula justo vel, placerat sapien. Phasellus viverra elit et vestibulum pharetra. Vestibulum commodo fermentum leo, eu porta nisi aliquam eget. Nulla tempus porttitor nisi nec mollis. Nam non mollis turpis. Nam finibus leo a bibendum tincidunt. Donec commodo velit magna, ac semper sapien mattis id. Proin sem velit, lobortis quis ultricies id, pharetra et lectus. Vestibulum condimentum neque vitae mi dapibus mollis. Mauris luctus vel sapien vitae hendrerit.",
"",
"Aenean vestibulum magna placerat fermentum tempus. Nam auctor condimentum nunc, in elementum quam ornare a. Etiam in ipsum elit. Proin pharetra, dolor sollicitudin pellentesque congue, lorem dolor ultricies magna, non iaculis risus nisl dictum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus vel euismod nibh, et lobortis dolor. Maecenas dui odio, gravida nec molestie ut, feugiat ut arcu. Pellentesque risus sapien, gravida a convallis quis, ullamcorper porttitor sapien.",
"",
"Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui.",
"",
"Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero."
],
[
"### 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.",
"",
"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.",
"",
"In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere.",
"",
"Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis.",
"",
"Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue.",
"",
"Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo."
],
[
"# Notes: Characters"
],
[
"# Nobody Owens",
"",
"@tag: Bod",
"@plot: Main",
"",
"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.",
"",
"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."
],
[
"# Notes: Plot"
],
[
"# Main Plot",
"",
"@tag: Main",
"",
"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."
],
[
"# Notes: World"
],
[
"# Ancient Europe",
"",
"@tag: Europe",
"",
"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.",
"",
"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."
]
]
}
}
@@ -0,0 +1,184 @@
#! Lorem Ipsum
**By lipsum.com**
“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit…”
“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain…”
% Exctracted from the lipsum.com website.
Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. Lorem Ipsum comes from sections 1.10.32 and 1.10.33 of “de Finibus Bonorum et Malorum” (The Extremes of Good and Evil) by Cicero, written in 45 BC. This book is a treatise on the theory of ethics, very popular during the Renaissance. The first line of Lorem Ipsum, “Lorem ipsum dolor sit amet..”, comes from a line in section 1.10.32.
The standard chunk of Lorem Ipsum used since the 1500s is reproduced below for those interested. Sections 1.10.32 and 1.10.33 from “de Finibus Bonorum et Malorum” by Cicero are also reproduced in their exact original form, accompanied by English versions from the 1914 translation by H. Rackham.
##! Prologue
% Synopsis:Explanation from the lipsum.com website.
_Lorem Ipsum_ is simply dummy text 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.
# Act One
“Fusce maximus felis libero”
## Chapter One
@pov: Bod
@plot: Main
@location: Europe
% 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.
### 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.
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.
Aliquam ut nisl arcu. Ut ultricies, lorem dignissim rutrum convallis, risus orci tempus lectus, congue feugiat sem lectus vitae odio. Duis sit amet justo finibus, hendrerit nulla at, ullamcorper enim. Praesent vel tellus sit amet tellus vulputate bibendum. Morbi eleifend sagittis sem, ac volutpat ante congue non. In hac habitasse platea dictumst. Morbi lobortis fermentum elit, dignissim sagittis ligula volutpat lacinia. Vestibulum eu interdum odio. Integer ac purus commodo metus congue tempor non at urna. Sed eget tortor vel quam viverra egestas. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec non convallis mauris, ac feugiat ex.
#### Scene One, Section Two
Integer vel libero ipsum. Donec varius aliquam libero, sit amet commodo urna hendrerit non. Nullam quis erat mollis nunc viverra volutpat tincidunt in odio. Nam vitae quam sem. Aliquam suscipit nulla non lorem pharetra semper. Ut suscipit erat eu ligula accumsan ultrices. Phasellus nisl tellus, placerat sed laoreet id, consectetur nec dolor. Sed fringilla ipsum id dapibus posuere. Aenean finibus pharetra tincidunt. Ut molestie malesuada nulla, id posuere lorem tincidunt eu. Aliquam tempor eros a est vulputate, scelerisque pulvinar ipsum fermentum. In hac habitasse platea dictumst.
Curabitur congue, justo quis interdum fermentum, tellus nulla imperdiet sapien, eu interdum enim tellus condimentum metus. Vivamus nunc velit, dignissim ut ultrices sit amet, ultricies quis enim. Donec ut vestibulum neque. Vivamus semper neque id ex ullamcorper varius. Fusce mattis nibh viverra lorem sagittis, et tempor arcu congue. Suspendisse sit amet felis sed urna facilisis mattis eget vitae arcu. Proin eu magna hendrerit, tristique sem maximus, placerat diam. Nulla tristique sed velit sit amet varius. Etiam vel ornare magna, in vulputate arcu. Cras velit orci, tincidunt sed volutpat cursus, bibendum vel sem. Nunc vulputate pharetra tortor, ac consectetur neque tincidunt sit amet. Nulla ornare mi sed mi dignissim ultricies. Ut tincidunt bibendum mauris, sed elementum ex vulputate vel. Mauris fermentum, felis nec vehicula congue, felis lorem facilisis erat, a dictum dolor augue vitae quam. Maecenas rutrum tortor nec consequat eleifend.
### 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.
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.
Proin vitae gravida nisl. Integer viverra orci turpis, sit amet pretium ligula facilisis consequat. Nulla interdum commodo metus, mollis consequat dui tincidunt et. Proin consequat bibendum justo id commodo. Fusce fermentum nunc turpis, eu vestibulum risus feugiat ut. Sed scelerisque vel ligula ut interdum. Suspendisse ac blandit ligula, sagittis fringilla dolor. In tincidunt convallis diam et ornare. Aenean id dignissim est, ut rhoncus quam. Donec vitae nisl velit. In convallis nibh ut augue dignissim, eu elementum quam cursus. Phasellus in lectus lorem. Curabitur in pellentesque nisi, at gravida sapien. Sed cursus justo volutpat lacus placerat, sit amet dignissim turpis commodo. Aliquam vitae orci eget nulla posuere condimentum in ut felis.
Nulla accumsan ante in pulvinar efficitur. Nulla non velit quis urna hendrerit bibendum. Suspendisse ultrices ante eu justo malesuada, sed fermentum enim rutrum. Nunc fermentum pharetra felis, vitae sollicitudin quam rutrum porta. Aliquam fringilla velit a mi laoreet, et luctus est rutrum. In gravida non ipsum sit amet tempus. Curabitur et eleifend purus. Nulla facilisi.
#### Scene Two, Section Two
Suspendisse potenti. Fusce tempus lorem nec laoreet suscipit. Fusce vulputate nisl ac diam tincidunt, nec malesuada quam pellentesque. Maecenas congue, tellus quis commodo rutrum, magna leo egestas arcu, quis suscipit ex risus id ligula. Suspendisse potenti. Morbi blandit lacus vitae laoreet vulputate. Donec vitae tellus eleifend, lobortis eros eu, tincidunt enim. Nullam et ullamcorper nisi. Vivamus tellus ex, lobortis quis rutrum ut, dapibus sit amet turpis. Phasellus pellentesque metus diam, commodo tristique ante commodo ac. Ut mollis ipsum nec diam blandit sollicitudin. Duis bibendum lacus nec commodo dapibus. Sed condimentum luctus ante, id ultricies urna varius nec. Nam convallis magna nec bibendum ultrices. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Sed auctor pharetra quam, vitae porta ex bibendum eu.
Vivamus ut venenatis lectus. Phasellus nec elit id sem dictum ornare. Quisque feugiat, diam eget sagittis ultricies, orci turpis efficitur nisi, et fringilla justo odio nec nibh. In hac habitasse platea dictumst. Sed tempus bibendum feugiat. Etiam luctus mauris arcu, non interdum ipsum ultrices id. Vivamus blandit urna sit amet scelerisque vulputate. Quisque in metus eget massa rutrum dictum sit amet sed nulla. Vivamus vel efficitur dolor.
Ut et consequat enim, quis ornare nibh. In lectus neque, mollis et suscipit et, vestibulum vitae augue. Praesent id ante sit amet odio venenatis placerat a at erat. Sed sed metus sed nisi dictum varius. Integer tincidunt fermentum purus ac porta. Fusce porttitor non risus eget tristique. Donec augue nunc, maximus at fermentum vel, varius et neque. Ut sed consectetur mauris. Quisque ipsum enim, porttitor vitae imperdiet sit amet, tempor et mauris. Aliquam malesuada tincidunt lectus quis blandit. Sed commodo orci felis, quis ultrices tellus facilisis sed. Nunc vel varius est. Duis ullamcorper eu metus in pulvinar. Morbi at sapien dictum, rutrum mauris eget, interdum tellus.
##! Why do we use it?
% Exctracted from the lipsum.com website.
It is a long established fact that a reader will be distracted by the readable content of a page when looking at its layout.
The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using 'Content here, content here', making it look like readable English.
Many desktop publishing packages and web page editors now use Lorem Ipsum as their default model text, and a search for 'lorem ipsum' will uncover many web sites still in their infancy. Various versions have evolved over the years, sometimes by accident, sometimes on purpose (injected humour and the like).
## 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.
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.
### 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.
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.
Pellentesque id sagittis dui. Praesent ut nisi sit amet libero euismod ornare. Vestibulum vehicula, lorem eget aliquet imperdiet, eros nulla iaculis mi, vel bibendum est dui sed orci. Nullam vitae lorem rutrum, euismod lacus id, ullamcorper lectus. Duis nec commodo mi, a fringilla diam. Vestibulum molestie nibh tristique, viverra augue non, aliquet metus. Phasellus a tellus ac nisl tempor aliquet. Nulla vitae sapien rutrum augue ornare ultrices a quis nisi. Sed pulvinar tincidunt ex. Fusce vel sem vitae ante pellentesque lobortis.
Maecenas ullamcorper lacus nec turpis finibus aliquet eget rutrum augue. Integer lorem erat, faucibus non lacus lacinia, pulvinar egestas felis. Proin rutrum nunc eget nulla varius, id blandit mauris tincidunt. Donec sit amet ullamcorper nisi, ut efficitur mi. Aliquam aliquet, nulla eget rhoncus tristique, justo lorem consectetur dui, id ornare leo odio sed tellus. Curabitur interdum velit a turpis condimentum venenatis. Nunc rhoncus sem ac augue auctor, nec malesuada ex fringilla. Vestibulum egestas diam sed leo consectetur vulputate quis eget enim. Nam tincidunt metus sit amet maximus ullamcorper. Sed placerat velit vitae massa efficitur viverra. Etiam eleifend dignissim ante, sed luctus nisl tristique a. In vestibulum pharetra dolor in molestie. Vivamus auctor massa ac magna imperdiet, sit amet iaculis turpis finibus.
Aenean dapibus vulputate purus, sit amet tempor nunc suscipit consequat. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Mauris auctor congue eros, non pellentesque neque dapibus ac. Vestibulum non leo nec urna lacinia eleifend quis et diam. Praesent eu nisi magna. Nulla at magna massa. Suspendisse porta varius scelerisque. Duis at auctor dolor, non dapibus urna. Nunc venenatis feugiat magna non molestie. Aliquam non ornare ex. Quisque eu ultrices velit, quis pellentesque eros. Phasellus eleifend, elit id imperdiet aliquam, nulla quam molestie turpis, at egestas odio ante et tortor. Suspendisse fringilla condimentum justo, at aliquet odio aliquam ac.
### 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.
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.
Duis efficitur metus pulvinar, molestie magna eget, feugiat dui. Fusce convallis vehicula ipsum convallis blandit. Duis eros risus, malesuada eu imperdiet in, hendrerit ac metus. Vestibulum id justo gravida, dignissim nibh non, iaculis diam. Fusce accumsan est ut massa porta ultricies. Nulla vitae justo in tortor laoreet mollis. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin eu libero justo. Vivamus aliquet placerat est, et auctor eros posuere venenatis. Nunc quam diam, tincidunt ac aliquet in, fermentum sit amet lectus. Proin commodo tincidunt blandit. Quisque erat arcu, semper nec dui non, consectetur gravida ipsum. Nullam pretium consectetur elit at condimentum.
Etiam sagittis, erat vitae accumsan tempor, neque augue scelerisque nulla, ut ultrices justo urna sit amet augue. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean at pulvinar tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Cras vel porta quam. Nullam eu mauris mollis, vehicula justo vel, placerat sapien. Phasellus viverra elit et vestibulum pharetra. Vestibulum commodo fermentum leo, eu porta nisi aliquam eget. Nulla tempus porttitor nisi nec mollis. Nam non mollis turpis. Nam finibus leo a bibendum tincidunt. Donec commodo velit magna, ac semper sapien mattis id. Proin sem velit, lobortis quis ultricies id, pharetra et lectus. Vestibulum condimentum neque vitae mi dapibus mollis. Mauris luctus vel sapien vitae hendrerit.
Aenean vestibulum magna placerat fermentum tempus. Nam auctor condimentum nunc, in elementum quam ornare a. Etiam in ipsum elit. Proin pharetra, dolor sollicitudin pellentesque congue, lorem dolor ultricies magna, non iaculis risus nisl dictum diam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Vivamus vel euismod nibh, et lobortis dolor. Maecenas dui odio, gravida nec molestie ut, feugiat ut arcu. Pellentesque risus sapien, gravida a convallis quis, ullamcorper porttitor sapien.
Donec ipsum eros, vestibulum sit amet cursus eget, iaculis quis dolor. Pellentesque magna augue, tristique dapibus mi vitae, molestie venenatis enim. Nam malesuada, turpis volutpat rhoncus ullamcorper, justo est eleifend orci, ut luctus risus ex rutrum arcu. Sed mi elit, feugiat rhoncus ornare sed, porta id leo. Pellentesque feugiat nulla tincidunt erat suscipit, eu congue lacus hendrerit. Morbi pulvinar enim sed consequat auctor. Ut eleifend enim sem, vitae euismod ex ultricies sit amet. Curabitur eu efficitur nisi, suscipit finibus sapien. In sodales blandit erat, vestibulum pulvinar ante volutpat nec. Vivamus dictum non libero at molestie. Donec sit amet neque in ante convallis pretium. Nunc vel iaculis dui.
Phasellus eu nunc ut nunc faucibus laoreet. Aliquam at magna risus. Praesent lobortis, risus finibus semper varius, magna purus vestibulum eros, at pulvinar sapien enim a ex. In scelerisque malesuada ex, sit amet egestas neque condimentum sed. Praesent vulputate efficitur massa. Cras at accumsan ligula. In elementum lectus eget blandit dictum. Nam vitae libero ut justo eleifend rutrum ac nec arcu. Aliquam sodales in quam congue vestibulum. Aliquam in accumsan sapien. Quisque lobortis nisl nisi, vitae bibendum turpis efficitur sed. Vestibulum tempor nulla eget nisi convallis, blandit sagittis ipsum convallis. Donec odio nibh, ultrices quis odio in, mollis euismod libero.
### 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.
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.
In ut lobortis lacus, nec luctus arcu. Vivamus condimentum sapien a ipsum malesuada sodales. Donec et vestibulum risus. Integer dictum euismod eros id tincidunt. Aliquam sagittis leo vitae consequat fermentum. Donec maximus ex eu ex iaculis porta. Praesent pharetra lacinia risus, et eleifend diam commodo non. Sed feugiat ipsum ut orci sagittis, quis faucibus lectus blandit. Sed tellus quam, gravida vitae laoreet quis, tempus lobortis dui. Vivamus semper accumsan ullamcorper. Praesent tempus pretium eros, non elementum risus. Pellentesque odio quam, auctor quis ex non, vulputate egestas dolor. Nunc luctus enim ut justo sodales consectetur. Sed aliquet a mauris vel posuere.
Donec luctus lectus efficitur, blandit nisi vitae, dignissim tellus. Pellentesque euismod pharetra augue gravida hendrerit. Quisque nisi mi, mattis ac nisi non, maximus malesuada ante. Nulla lobortis, diam eu ornare ornare, tellus enim feugiat arcu, non vestibulum tortor nunc eu justo. Integer blandit felis justo, eu semper est scelerisque vel. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nam ultricies, nisi vel elementum commodo, nisl dolor tincidunt magna, sed varius est nunc at lectus. Aliquam dolor tortor, sodales placerat ultricies quis, sodales quis sapien. Duis ullamcorper sollicitudin risus at mattis. Integer consequat et nunc at condimentum. Pellentesque cursus congue augue, non suscipit lectus sodales ut. Nam a mi bibendum, blandit nisl eu, accumsan nunc. Aliquam a ex mauris. Sed nec sem quis arcu dignissim tempus eget et turpis. Ut sed ex nec ipsum ultrices lobortis.
Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan quis odio quis euismod. Maecenas ultrices, augue et aliquam tincidunt, erat tellus ornare ligula, quis ultrices turpis nibh vel justo. Fusce gravida odio tellus. In a congue diam. Mauris consequat ex id leo lacinia dictum. Fusce id sem sodales, ultrices sapien ac, convallis orci. Donec gravida nunc sit amet nisi hendrerit, sed porta enim aliquam. In hac habitasse platea dictumst. Cras a orci felis. Curabitur non felis nec urna maximus auctor ut ut nisi. Curabitur at turpis eleifend, blandit eros at, molestie odio. Phasellus euismod neque augue.
Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.
# Notes: Characters
# Nobody Owens
@tag: Bod
@plot: Main
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.
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.
# Notes: Plot
# Main Plot
@tag: Main
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.
# Notes: World
# Ancient Europe
@tag: Europe
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.
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.
+472
View File
@@ -0,0 +1,472 @@
"""
novelWriter Manuscript Build Settings Tester
==============================================
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
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/>.
"""
import json
import uuid
import pytest
import shutil
from pathlib import Path
from tools import C, buildTestProject
from mocked import causeOSError
from novelwriter.enum import nwBuildFmt, nwItemClass
from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
from novelwriter.core.buildsettings import BuildCollection, BuildSettings, FilterMode
def isUUID(value):
"""Checks if a value is a valid UUID object."""
try:
uuid.UUID(value)
return True
except Exception:
return False
@pytest.mark.core
def testCoreBuildSettings_ClassAttributes(fncPath: Path):
"""Test the BuildSettings class attributes."""
build = BuildSettings()
assert build.changed is False
# Name is always converted to string
build.setName(None) # type: ignore
assert build.name == "None"
build.setName("Test Build")
assert build.name == "Test Build"
# Only valid UUIDs are accpeted, anything else generates a new UUID
build.setBuildID("5cf45d24-f496-42c9-8733-529a9e52a62b")
assert build.buildID == "5cf45d24-f496-42c9-8733-529a9e52a62b"
build.setBuildID(None) # type: ignore
assert build.buildID != "5cf45d24-f496-42c9-8733-529a9e52a62b"
assert isUUID(build.buildID)
build.setBuildID("qcf45d24-f496-42c9-8733-529a9e52a62b")
assert build.buildID != "qcf45d24-f496-42c9-8733-529a9e52a62b"
assert isUUID(build.buildID)
# Last path must be valid, if not it defaults to $HOME
build.setLastPath("/path/to/nowhere")
assert build.lastPath == Path.home()
build.setLastPath(None)
assert build.lastPath == Path.home()
(fncPath / "test.txt").write_text("foobar")
build.setLastPath(fncPath / "test.txt") # Can't be a file
assert build.lastPath == Path.home()
build.setLastPath(fncPath)
assert build.lastPath == fncPath
build.setLastPath(str(fncPath)) # String paths are also ok
assert build.lastPath == fncPath
# Last path no longer exists -> fallback to $HOME
testDir = fncPath / "test_dir"
testDir.mkdir()
build.setLastPath(testDir)
assert build.lastPath == testDir
testDir.rmdir()
assert build.lastPath == Path.home()
# Last build name
build.setLastBuildName(None) # type: ignore
assert build.lastBuildName == "None"
build.setLastBuildName("\tBuild Name ")
assert build.lastBuildName == "Build Name"
# Last build format
build.setLastFormat(None) # type: ignore
assert build.lastFormat == nwBuildFmt.ODT # Default value
build.setLastFormat(nwBuildFmt.FODT)
assert build.lastFormat == nwBuildFmt.FODT
# Changes are recorded
assert build.changed is True
build.resetChangedState()
assert build.changed is False
# Set some sensible values
build.setName("Test Build")
build.setBuildID("5cf45d24-f496-42c9-8733-529a9e52a62b")
build.setLastPath(fncPath)
build.setLastBuildName("Build Name")
build.setLastFormat(nwBuildFmt.HTML)
# Pack the values
data = build.pack()
assert data["name"] == "Test Build"
assert data["uuid"] == "5cf45d24-f496-42c9-8733-529a9e52a62b"
assert data["path"] == str(fncPath)
assert data["build"] == "Build Name"
assert data["format"] == nwBuildFmt.HTML.name
# Unpack into new object
another = BuildSettings()
another.unpack(data)
more = another.pack()
assert more["name"] == "Test Build"
assert more["uuid"] == "5cf45d24-f496-42c9-8733-529a9e52a62b"
assert more["path"] == str(fncPath)
assert more["build"] == "Build Name"
assert more["format"] == nwBuildFmt.HTML.name
# END Test testCoreBuildSettings_ClassAttributes
@pytest.mark.core
def testCoreBuildSettings_BuildValues():
"""Test BuildSettings get/set of build values."""
build = BuildSettings()
strSetting = "headings.fmtTitle"
intSetting = "format.textSize"
boolSetting = "filter.includeNovel"
floatSetting = "format.lineHeight"
# Invalid setting
assert build.setValue("foo", "bar") is False
# Value must be correct type
assert build.setValue(strSetting, 15) is False
assert build.setValue(intSetting, 15.0) is False
assert build.setValue(boolSetting, "string") is False
assert build.setValue(floatSetting, 15) is False
# Check min/max range
assert build.setValue(floatSetting, 200.0) is True
assert build.getFloat(floatSetting) == 3.0
assert build.setValue(floatSetting, 0.0) is True
assert build.getFloat(floatSetting) == 0.75
# Check string values
assert build.setValue(strSetting, "foobar") is True
assert build.getStr(strSetting) == "foobar"
assert build.getInt(strSetting) == 0
assert build.getBool(strSetting) is True
assert build.getFloat(strSetting) == 0.0
# Check int values
assert build.setValue(intSetting, 42) is True
assert build.getStr(intSetting) == "42"
assert build.getInt(intSetting) == 42
assert build.getBool(intSetting) is True
assert build.getFloat(intSetting) == 42.0
# Check bool values
assert build.setValue(boolSetting, True) is True
assert build.getStr(boolSetting) == "True"
assert build.getInt(boolSetting) == 1
assert build.getBool(boolSetting) is True
assert build.getFloat(boolSetting) == 1.0
# Check float values
assert build.setValue(floatSetting, 2.5) is True
assert build.getStr(floatSetting) == "2.5"
assert build.getInt(floatSetting) == 2
assert build.getBool(floatSetting) is True
assert build.getFloat(floatSetting) == 2.5
# Check labels
assert build.getLabel(strSetting) == "Title Headings"
assert BuildSettings.getLabel(strSetting) == "Title Headings"
# Pack the values
data = build.pack()
assert data["settings"][strSetting] == "foobar"
assert data["settings"][intSetting] == 42
assert data["settings"][boolSetting] is True
assert data["settings"][floatSetting] == 2.5
# Unpack into new object
another = BuildSettings()
another.unpack(data)
more = another.pack()
assert more["settings"][strSetting] == "foobar"
assert more["settings"][intSetting] == 42
assert more["settings"][boolSetting] is True
assert more["settings"][floatSetting] == 2.5
# END Test testCoreBuildSettings_BuildValues
@pytest.mark.core
def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd):
"""Test filters for project items."""
project = NWProject(mockGUI)
buildTestProject(project, fncPath)
build = BuildSettings()
# Add some more items
hArchRoot = project.newRoot(nwItemClass.ARCHIVE, "Archive")
hPlotDoc = project.newFile("Main Plot", C.hPlotRoot)
hCharDoc = project.newFile("Jane Doe", C.hCharRoot)
initLen = len(project.tree)
# With no changes
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is True
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=False) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (False, FilterMode.SKIPPED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.FILTERED),
hCharDoc: (False, FilterMode.FILTERED),
}
# Enable notes and roots
build.setValue("filter.includeNotes", True)
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (True, FilterMode.ROOT),
C.hCharRoot: (True, FilterMode.ROOT),
C.hWorldRoot: (False, FilterMode.SKIPPED), # World folder is empty
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (True, FilterMode.FILTERED),
hCharDoc: (True, FilterMode.FILTERED),
}
# Skip plot and char roots
build.setValue("filter.includeNotes", True)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, False)
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is False
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (False, FilterMode.SKIPPED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.SKIPPED), # Now also skipped since in plot
hCharDoc: (False, FilterMode.SKIPPED), # Now also skipped since in char
}
# Enable char root again, but exclude char doc and scene doc
build.setValue("filter.includeNotes", True)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, True)
build.setExcluded(C.hSceneDoc)
build.setExcluded(hCharDoc) # type: ignore
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (False, FilterMode.SKIPPED), # Is skipped anyway since doc is skipped
C.hWorldRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (False, FilterMode.EXCLUDED),
hArchRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.EXCLUDED),
}
# Disable notes, but include char doc
build.setValue("filter.includeNotes", False)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, True)
build.setExcluded(C.hSceneDoc)
build.setIncluded(hCharDoc) # type: ignore
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (True, FilterMode.ROOT),
C.hWorldRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (False, FilterMode.EXCLUDED),
hArchRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.SKIPPED),
hCharDoc: (True, FilterMode.INCLUDED),
}
# Set everything back to filered
build.setValue("filter.includeNotes", False)
build.setAllowRoot(C.hPlotRoot, False)
build.setAllowRoot(C.hCharRoot, True)
build.setFiltered(C.hSceneDoc)
build.setFiltered(hCharDoc) # type: ignore
assert build.isRootAllowed(C.hNovelRoot) is True
assert build.isRootAllowed(C.hPlotRoot) is False
assert build.isRootAllowed(C.hCharRoot) is True
assert build.isRootAllowed(C.hWorldRoot) is True
assert build.buildItemFilter(project, withRoots=True) == {
C.hNovelRoot: (True, FilterMode.ROOT),
C.hPlotRoot: (False, FilterMode.SKIPPED),
C.hCharRoot: (False, FilterMode.SKIPPED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
hArchRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.FILTERED),
}
# Check error handling
project.tree._treeOrder.append("00000000000ff")
project.tree._projTree["00000000000ff"] = NWItem(project)
assert project.tree["00000000000ff"].itemHandle is None # type: ignore
filtered = build.buildItemFilter(project, withRoots=False)
assert len(filtered) == initLen
# No valid project provided
assert build.buildItemFilter(None) == {} # type: ignore
# END Test testCoreBuildSettings_Filters
@pytest.mark.core
def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRnd):
"""Test the collections class for builds."""
project = NWProject(mockGUI)
buildTestProject(project, fncPath)
buildsFile = project.storage.getMetaFile(nwFiles.BUILDS_FILE)
assert isinstance(buildsFile, Path)
# No initial builds in a fresh project
builds = BuildCollection(project)
assert len(builds) == 0
assert not buildsFile.exists()
# Greate a default build
buildOne = BuildSettings()
buildOne.setName("Build One")
buildIDOne = buildOne.buildID
# Check that invalid type is ahndled
builds.setBuild(None) # type: ignore
assert len(builds) == 0
assert not buildsFile.exists()
assert builds.getBuild(buildIDOne) is None
# Add the build
builds.setBuild(buildOne)
assert len(builds) == 1
assert buildsFile.exists()
assert builds.getBuild(buildIDOne).buildID == buildIDOne # type: ignore
# Create another build
buildTwo = BuildSettings()
buildTwo.setName("Build Two")
buildIDTwo = buildTwo.buildID
# Check that we can extract infor about the builds
builds.setBuild(buildTwo)
assert len(builds) == 2
assert buildsFile.exists()
assert builds.getBuild(buildIDTwo).buildID == buildIDTwo # type: ignore
assert list(builds.builds()) == [
(buildIDOne, "Build One"),
(buildIDTwo, "Build Two"),
]
# Check the file content
data = json.loads(buildsFile.read_text(encoding="utf-8"))
assert list(data["novelWriter.builds"].keys()) == [buildIDOne, buildIDTwo]
# Remove a build
builds.removeBuild(buildIDOne)
assert builds.getBuild(buildIDOne) is None
assert list(builds.builds()) == [
(buildIDTwo, "Build Two"),
]
# Check the file content
data = json.loads(buildsFile.read_text(encoding="utf-8"))
assert list(data["novelWriter.builds"].keys()) == [buildIDTwo]
builds.setBuild(buildOne)
assert list(builds.builds()) == [
(buildIDTwo, "Build Two"),
(buildIDOne, "Build One"),
]
# Check errors: No valid path
with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
assert builds._loadCollection() is False
assert builds._saveCollection() is False
# Check errors: I/O error
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
assert builds._loadCollection() is False
assert builds._saveCollection() is False
# Check errors: Can't parse json file
shutil.copy(buildsFile, buildsFile.with_suffix(".bak"))
buildsFile.write_text("foobar")
assert builds._loadCollection() is False
# Check errors: Valid jason file, but list instead of object
buildsFile.write_text("[]")
assert builds._loadCollection() is False
buildsFile.unlink()
shutil.copy(buildsFile.with_suffix(".bak"), buildsFile)
# Load builds file into new object
another = BuildCollection(project)
assert list(another.builds()) == [
(buildIDTwo, "Build Two"),
(buildIDOne, "Build One"),
]
# END Test testCoreBuildSettings_Collection
+316 -9
View File
@@ -19,16 +19,23 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import json
import pytest
from shutil import copyfile
from pathlib import Path
from tools import C, ODT_IGNORE, buildTestProject, cmpFiles
from mocked import causeException, causeOSError
from novelwriter.core.buildsettings import BuildSettings
from tools import ODT_IGNORE, cmpFiles
from novelwriter.enum import nwBuildFmt
from novelwriter.core.item import NWItem
from novelwriter.core.tomd import ToMarkdown
from novelwriter.core.toodt import ToOdt
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.project import NWProject
from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.buildsettings import BuildSettings
BUILD_CONF = {
"name": "Test Build",
@@ -55,7 +62,7 @@ BUILD_CONF = {
"format.lineHeight": 1.5,
"format.justifyText": True,
"format.stripUnicode": False,
"format.replaceTabs": False,
"format.replaceTabs": True,
"odt.addColours": True,
"html.addStyles": True,
},
@@ -69,8 +76,7 @@ BUILD_CONF = {
@pytest.mark.core
def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test builing an open document manuscript.
"""
"""Test builing an open document manuscript."""
project = NWProject(mockGUI)
project.openProject(prjLipsum)
@@ -174,8 +180,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
@pytest.mark.core
def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test builing an HTML manuscript.
"""
"""Test builing an HTML manuscript."""
project = NWProject(mockGUI)
project.openProject(prjLipsum)
@@ -207,6 +212,26 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile)
# Check HTML5 JSON Build
# ======================
docFile = fncPath / "Lorem Ipsum.json"
tstFile = tstPaths.outDir / "mBuildDocBuild_HTML5_Lorem_Ipsum.json"
cmpFile = tstPaths.refDir / "mBuildDocBuild_HTML5_Lorem_Ipsum.json"
count = 0
error = []
for _, success in docBuild.iterBuildHTML(docFile, asJson=True):
count += 1 if success else 0
if docBuild.error:
error.append(docBuild.error)
assert count == 19
assert error == []
copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7])
# Check Error Handling
# ====================
@@ -225,8 +250,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
@pytest.mark.core
def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test builing an Markdown manuscript.
"""
"""Test builing an Markdown manuscript."""
project = NWProject(mockGUI)
project.openProject(prjLipsum)
@@ -292,3 +316,286 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
assert not docFile.is_file()
# END Test testCoreDocBuild_Markdown
@pytest.mark.core
def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test builing a NWD manuscript."""
project = NWProject(mockGUI)
project.openProject(prjLipsum)
build = BuildSettings()
build.unpack(BUILD_CONF)
docBuild = NWBuildDocument(project, build)
docBuild.queueAll()
assert len(docBuild) == 21
# Check NWD Build
# ===============
docFile = fncPath / "Lorem Ipsum.txt"
tstFile = tstPaths.outDir / "mBuildDocBuild_NWD_Lorem_Ipsum.txt"
cmpFile = tstPaths.refDir / "mBuildDocBuild_NWD_Lorem_Ipsum.txt"
count = 0
error = []
for _, success in docBuild.iterBuildNWD(docFile, asJson=False):
count += 1 if success else 0
if docBuild.error:
error.append(docBuild.error)
assert count == 19
assert error == []
copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile)
# Check NWD JSON Build
# ====================
docFile = fncPath / "Lorem Ipsum.json"
tstFile = tstPaths.outDir / "mBuildDocBuild_NWD_Lorem_Ipsum.json"
cmpFile = tstPaths.refDir / "mBuildDocBuild_NWD_Lorem_Ipsum.json"
count = 0
error = []
for _, success in docBuild.iterBuildNWD(docFile, asJson=True):
count += 1 if success else 0
if docBuild.error:
error.append(docBuild.error)
assert count == 19
assert error == []
copyfile(docFile, tstFile)
assert cmpFiles(tstFile, cmpFile, ignoreLines=[6, 7])
# Check Error Handling
# ====================
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
docFile = fncPath / "Lorem Ipsum Err.md"
for _ in docBuild.iterBuildNWD(docFile):
pass
assert docBuild.error == "OSError: Mock OSError"
assert not docFile.is_file()
# END Test testCoreDocBuild_NWD
@pytest.mark.core
def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
"""Test custom builds and some error handling."""
project = NWProject(mockGUI)
buildTestProject(project, fncPath)
build = BuildSettings()
build.unpack(BUILD_CONF)
docBuild = NWBuildDocument(project, build)
docBuild.queueAll()
assert len(docBuild) == 8
# Build a simple text doc
count = 0
error = []
docFile = fncPath / "Minimal.txt"
for _, success in docBuild.iterBuildNWD(docFile, asJson=False):
count += 1 if success else 0
if docBuild.error:
error.append(docBuild.error)
assert count == 4
assert error == []
assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n"
"By Jane Doe\n\n"
"## New Chapter\n\n\n"
"### New Scene\n\n\n"
)
docFile.unlink()
# Add an invalid item to the project
bHandle = "0123456789abc"
nHandle = "0123456789def"
project.tree._treeOrder.append(bHandle)
project.tree._projTree[bHandle] = NWItem(project) # Handle should be None
project.tree._treeOrder.append(nHandle)
project.tree._projTree[nHandle] = None
docBuild.queueAll()
assert len(docBuild) == 8
docBuild.addDocument(bHandle)
docBuild.addDocument(nHandle)
assert len(docBuild) == 10
# Build the doc again with broken items
count = 0
error = []
docFile = fncPath / "Minimal.txt"
for _, success in docBuild.iterBuildNWD(docFile, asJson=False):
count += 1 if success else 0
if docBuild.error:
error.append(docBuild.error)
assert count == 4
assert error == []
assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n"
"By Jane Doe\n\n"
"## New Chapter\n\n\n"
"### New Scene\n\n\n"
)
docFile.unlink()
# END Test testCoreDocBuild_Custom
@pytest.mark.core
def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd):
"""Test iter build wrapper."""
project = NWProject(mockGUI)
buildTestProject(project, fncPath)
build = BuildSettings()
build.unpack(BUILD_CONF)
# Add some more items
hPlotDoc = project.newFile("Main Plot", C.hPlotRoot)
hCharDoc = project.newFile("Jane Doe", C.hCharRoot)
project.storage.getDocument(hPlotDoc).writeDocument("# Main Plot\n**Text**")
project.storage.getDocument(hCharDoc).writeDocument("# Jane Doe\n~~Text~~")
# Fix project order as this has never been opened in a GUI
project.tree.setOrder([
C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc, C.hWorldRoot
])
docBuild = NWBuildDocument(project, build)
docBuild.queueAll()
assert len(docBuild) == 10
# ODT Format
docFile = fncPath / "Minimal.odt"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.ODT)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToOdt)
assert docFile.is_file()
docFile.unlink()
# FODT Format
docFile = fncPath / "Minimal.fodt"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.FODT)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToOdt)
assert docFile.read_text(encoding="utf-8").startswith("<?xml")
docFile.unlink()
# HTML Format
docFile = fncPath / "Minimal.html"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.HTML)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToHtml)
assert docFile.read_text(encoding="utf-8").startswith("<!DOCTYPE html>")
docFile.unlink()
# JSON HTML Format
docFile = fncPath / "Minimal.json"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.J_HTML)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToHtml)
data = json.loads(docFile.read_text(encoding="utf-8"))
assert "meta" in data
assert "text" in data
docFile.unlink()
# Standard Markdown Format
docFile = fncPath / "Minimal.md"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.STD_MD)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToMarkdown)
assert docFile.read_text(encoding="utf-8") == (
"# New Novel\n\n"
"By Jane Doe\n\n"
"## Chapter: New Chapter\n\n"
"### Scene: New Scene\n\n"
"# Notes: Plot\n\n"
"# Main Plot\n\n"
"**Text**\n\n"
"# Notes: Characters\n\n"
"# Jane Doe\n\n"
"Text\n\n" # Standard converts strikethrough to ordinary text
)
docFile.unlink()
# Extended Markdown Format
docFile = fncPath / "Minimal.md"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.EXT_MD)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToMarkdown)
assert docFile.read_text(encoding="utf-8") == (
"# New Novel\n\n"
"By Jane Doe\n\n"
"## Chapter: New Chapter\n\n"
"### Scene: New Scene\n\n"
"# Notes: Plot\n\n"
"# Main Plot\n\n"
"**Text**\n\n"
"# Notes: Characters\n\n"
"# Jane Doe\n\n"
"~~Text~~\n\n" # Extended allows this syntax
)
docFile.unlink()
# NWD Format
docFile = fncPath / "Minimal.txt"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.NWD)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToMarkdown)
assert docFile.read_text(encoding="utf-8") == (
"#! New Novel\n\n"
"By Jane Doe\n\n"
"## New Chapter\n\n\n"
"### New Scene\n\n\n"
"# Notes: Plot\n\n"
"# Main Plot\n"
"**Text**\n\n"
"# Notes: Characters\n\n"
"# Jane Doe\n"
"~~Text~~\n\n"
)
docFile.unlink()
# JSON NWD Format
docFile = fncPath / "Minimal.json"
assert list(docBuild.iterBuild(docFile, nwBuildFmt.J_NWD)) == [
(0, True), (1, True), (2, False), (3, True), (4, True),
(5, True), (6, True), (7, True), (8, True), (9, False),
]
assert isinstance(docBuild.lastBuild, ToMarkdown)
data = json.loads(docFile.read_text(encoding="utf-8"))
assert "meta" in data
assert "text" in data
docFile.unlink()
# END Test testCoreDocBuild_IterBuild
+2 -2
View File
@@ -704,9 +704,9 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tstPaths):
assert theProject.backupProject(doNotify=True) is True
theFiles = list((tstPaths.tmpDir / "Test Minimal").iterdir())
assert len(theFiles) == 1
assert len(theFiles) in (1, 2) # Sometimes 2 due to clock tick
theZip = theFiles[0].name
theZip = theFiles[-1].name
assert theZip[:12] == "Backup from "
assert theZip[-4:] == ".zip"
+139
View File
@@ -0,0 +1,139 @@
"""
novelWriter Manuscript Build Dialog Tester
============================================
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
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/>.
"""
import pytest
from pathlib import Path
from pytestqt.qtbot import QtBot
from tools import buildTestProject
from PyQt5.QtWidgets import QDialogButtonBox, QFileDialog, QListWidgetItem, QMessageBox
from novelwriter.enum import nwBuildFmt
from novelwriter.guimain import GuiMain
from novelwriter.constants import nwLabels
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.core.buildsettings import BuildSettings
@pytest.mark.gui
def testManuscriptBuild_Main(
monkeypatch, qtbot: QtBot, nwGUI: GuiMain, fncPath: Path, projPath: Path, mockRnd
):
"""Test the GuiManuscriptBuild dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
build = BuildSettings()
build.setLastPath(fncPath)
manus = GuiManuscriptBuild(nwGUI, nwGUI, build)
manus.show()
# Check initial values
assert manus.buildPath.text() == str(fncPath)
assert manus.buildName.text() == "New Project - "
assert manus.listContent.count() == 3 # All novel docs
# Reset build name to default
build.setName("Test Build")
manus.btnReset.click()
assert manus.buildName.text() == "New Project - Test Build"
# Change path
with monkeypatch.context() as mp:
mp.setattr(QFileDialog, "getExistingDirectory", lambda *a: str(projPath))
manus.btnBrowse.click()
assert manus.buildPath.text() == str(projPath)
# Builds
# ======
# Reset values
manus.buildPath.setText(str(fncPath))
manus.buildName.setText("TestBuild")
# Helper function
def selectFormat(fmt):
for i in range(manus.listFormats.count()):
item = manus.listFormats.item(i)
assert isinstance(item, QListWidgetItem)
if item.data(manus.D_KEY) == fmt:
manus.listFormats.setCurrentItem(item)
return
else:
raise ValueError("No such key in format list")
# Build documents
lastFmt = None
for fmt in nwBuildFmt:
selectFormat(fmt)
assert manus._getSelectedFormat() == fmt
manus.btnBuild.click()
assert (fncPath / "TestBuild").with_suffix(nwLabels.BUILD_EXT[fmt]).exists()
lastFmt = fmt
manus._dialogButtonClicked(manus.dlgButtons.button(QDialogButtonBox.Close))
manus.deleteLater()
assert build.lastBuildName == "TestBuild"
assert build.lastFormat == lastFmt
assert build.lastPath == fncPath
# Error Handling
# ==============
manus = GuiManuscriptBuild(nwGUI, nwGUI, build)
manus.show()
# Name, path and format should be remembered
assert manus.buildPath.text() == str(fncPath)
assert manus.buildName.text() == "TestBuild"
assert manus._getSelectedFormat() == lastFmt
# Check handling of no selected build
manus.listFormats.clearSelection()
assert manus._getSelectedFormat() is None
assert manus._runBuild() is False
# Check ODT build with no name set, which should just reset it
selectFormat(nwBuildFmt.ODT)
manus.buildName.clear()
assert manus._runBuild() is True
assert manus.buildName.text() == "New Project - Test Build"
assert (fncPath / "New Project - Test Build.odt").exists()
# But invalid path should fail
manus.buildPath.setText(str(fncPath / "blabla"))
assert manus._runBuild() is False
# Blocking overwrite should also fail
manus.buildPath.setText(str(fncPath))
manus.buildName.setText("TestBuild")
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert manus._runBuild() is False
# Finish
manus._dialogButtonClicked(manus.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()
# END Test testManuscriptBuild_Main
+263
View File
@@ -0,0 +1,263 @@
"""
novelWriter Manuscript Tool Tester
====================================
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
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/>.
"""
import sys
import pytest
from pathlib import Path
from pytestqt.qtbot import QtBot
from tools import C, buildTestProject, getGuiItem
from mocked import causeOSError
from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import QDialogButtonBox
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from novelwriter import CONFIG
from novelwriter.guimain import GuiMain
from novelwriter.core.buildsettings import BuildSettings
from novelwriter.tools.manuscript import GuiManuscript
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
@pytest.mark.gui
def testManuscript_Init(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd):
"""Test the init/main functionality of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
nwGUI.theProject.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi")
allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi\n* * *"
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == ""
# Run the default build
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() == allText
manus.close()
# A new dialog should load the old build
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == allText
manus.close()
# But blocking the reload should leave it empty
with monkeypatch.context() as mp:
mp.setattr("builtins.open", lambda *a, **k: causeOSError)
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
assert manus.docPreview.toPlainText().strip() == ""
manus.close()
# Finish
# qtbot.stop()
# END Test testManuscript_Init
@pytest.mark.gui
def testManuscript_Builds(qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
"""Test the handling of builds in the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
# Delete the default build
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
manus.tbDel.click()
assert manus.buildList.count() == 0
# Create a new build
manus.tbAdd.click()
bSettings = getGuiItem("GuiBuildSettings")
assert isinstance(bSettings, GuiBuildSettings)
bSettings.editBuildName.setText("Test Build")
build = None
@pyqtSlot(BuildSettings)
def _testNewSettingsReady(new: BuildSettings):
nonlocal build
build = new
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings.dlgButtons.button(QDialogButtonBox.Save).click()
assert isinstance(build, BuildSettings)
assert build.name == "Test Build"
assert manus.buildList.count() == 1
# Edit the new build
manus.buildList.clearSelection()
manus.buildList.setCurrentRow(0)
manus.tbEdit.click()
bSettings = getGuiItem("GuiBuildSettings")
assert isinstance(bSettings, GuiBuildSettings)
build = None
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings.dlgButtons.button(QDialogButtonBox.Apply).click() # Should leave the dialog open
assert isinstance(build, BuildSettings)
assert build.name == "Test Build"
assert manus.buildList.count() == 1
# Close the dialog should also close the child dialogs
manus.btnClose.click()
if isinstance(bSettings, GuiBuildSettings):
# May have been garbage collected, but if it isn't, it should be hidden
assert bSettings.isVisible() is False
# Finish
# qtbot.stop()
# END Test testManuscript_Builds
@pytest.mark.gui
def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
"""Test other featurs of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
cacheFile = CONFIG.dataPath("cache") / f"build_{nwGUI.theProject.data.uuid}.json"
manus.buildList.setCurrentRow(0)
build = manus._getSelectedBuild()
assert isinstance(build, BuildSettings)
# Previews
# ========
# No build selected
manus.buildList.clearSelection()
manus.btnPreview.click()
qtbot.wait(200) # Should be enough to run the build
assert manus.docPreview.toPlainText().strip() == ""
assert cacheFile.exists() is False
# Preview the first, but fail to save cache
manus.buildList.setCurrentRow(0)
with monkeypatch.context() as mp:
mp.setattr("builtins.open", lambda *a, **k: causeOSError)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert cacheFile.exists() is False
# Preview again, and allow cache file to be created
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() != ""
assert cacheFile.exists() is True
# Toggle justify
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignAbsolute
manus.docPreview.setJustify(True)
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignJustify
manus.docPreview.setJustify(False)
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignAbsolute
# Tests are too fast to trigger this one, so we trigger it manually to ensure it isn't failing
manus.docPreview._hideProgress()
# Builds
# ======
build._changed = True
manus.buildList.clearSelection()
with monkeypatch.context() as mp:
mp.setattr("novelwriter.tools.manusbuild.GuiManuscriptBuild.exec_", lambda *a: None)
# With no selection, no dialog should be created
manus.btnBuild.click()
for obj in manus.children():
assert not isinstance(obj, GuiManuscriptBuild)
# With a selection, there should be one
manus.buildList.setCurrentRow(0)
manus.btnBuild.click()
for obj in manus.children():
if isinstance(obj, GuiManuscriptBuild):
obj.close()
break
else:
assert False
# Finish
manus.close()
# qtbot.stop()
# END Test testManuscript_Features
@pytest.mark.gui
@pytest.mark.skipif(sys.platform.startswith("darwin"), reason="Not running on Darwin")
def testManuscript_Print(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
"""Test the print feature of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
manus.btnPreview.click()
assert manus.docPreview.toPlainText().strip() != ""
with monkeypatch.context() as mp:
mp.setattr(QPrintPreviewDialog, "exec_", lambda *a: None)
manus.btnPrint.click()
for obj in manus.children():
if isinstance(obj, QPrintPreviewDialog):
obj.show()
obj.close()
break
else:
assert False
# Finish
manus.close()
# qtbot.stop()
# END Test testManuscript_Print
@@ -0,0 +1,617 @@
"""
novelWriter Manuscript Build Settings Dialog Tester
=====================================================
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
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/>.
"""
import pytest
from tools import C, buildTestProject
from pathlib import Path
from pytestqt.qtbot import QtBot
from PyQt5.QtGui import QFont
from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QDialogButtonBox, QFontDialog
from novelwriter import CONFIG
from novelwriter.guimain import GuiMain
from novelwriter.constants import nwHeadFmt
from novelwriter.core.buildsettings import BuildSettings, FilterMode
from novelwriter.tools.manussettings import (
GuiBuildSettings, _OutputTab, _FormatTab, _ContentTab, _HeadingsTab, _FilterTab
)
@pytest.mark.gui
def testBuildSettings_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd):
"""Test the initialisation of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
build = BuildSettings()
# Create the dialog and populate it
bSettings = GuiBuildSettings(nwGUI, nwGUI, build)
bSettings.show()
bSettings.loadContent()
# Flip through pages
bSettings.optSideBar._group.button(bSettings.OPT_OUTPUT).click()
assert isinstance(bSettings.toolStack.currentWidget(), _OutputTab)
bSettings.optSideBar._group.button(bSettings.OPT_FORMAT).click()
assert isinstance(bSettings.toolStack.currentWidget(), _FormatTab)
bSettings.optSideBar._group.button(bSettings.OPT_CONTENT).click()
assert isinstance(bSettings.toolStack.currentWidget(), _ContentTab)
bSettings.optSideBar._group.button(bSettings.OPT_HEADINGS).click()
assert isinstance(bSettings.toolStack.currentWidget(), _HeadingsTab)
bSettings.optSideBar._group.button(bSettings.OPT_FILTERS).click()
assert isinstance(bSettings.toolStack.currentWidget(), _FilterTab)
# Check dialog buttons
triggered = False
@pyqtSlot(BuildSettings)
def _testNewSettingsReady(new: BuildSettings):
nonlocal build, triggered
assert new is build
triggered = True
# Capture Apply button
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Apply))
assert triggered
# Capture Save button
triggered = False
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Save))
assert triggered
# Close manually
# This pops the ask to save dialog, which is automatically handled
triggered = False
build._changed = True
bSettings.show()
with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000):
bSettings.newSettingsReady.connect(_testNewSettingsReady)
bSettings.close()
assert triggered
# Finish
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()
# END Test testBuildSettings_Init
@pytest.mark.gui
def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd):
"""Test the Filter Tab of the GuiBuildSettings dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
build = BuildSettings()
switchMap = {
"incNovel": 1,
"incNotes": 2,
"incInactive": 3,
"novelRoot": 6,
"plotRoot": 7,
"charRoot": 8,
"worldRoot": 9,
}
hPlotDoc = nwGUI.theProject.newFile("Main Plot", C.hPlotRoot)
hCharDoc = nwGUI.theProject.newFile("Jane Doe", C.hCharRoot)
nwGUI.projView.projTree.revealNewTreeItem(hPlotDoc)
nwGUI.projView.projTree.revealNewTreeItem(hCharDoc)
nwGUI.theProject.tree[hPlotDoc].setActive(False)
# Create the dialog and populate it
bSettings = GuiBuildSettings(nwGUI, nwGUI, build)
bSettings.show()
bSettings.loadContent()
filterTab = bSettings.optTabSelect
bSettings.optSideBar._group.button(bSettings.OPT_FILTERS).click()
assert bSettings.toolStack.currentWidget() is filterTab
# Check content
assert filterTab.optTree.topLevelItemCount() == 4 # The 4 root folders
assert filterTab.filterOpt._index == 10 # 2 headers, 1 sep, 3 opt and 4 roots
# Untoggle note folders
filterTab.filterOpt._widgets[switchMap["worldRoot"]].setChecked(False) # World Root
assert filterTab.optTree.topLevelItemCount() == 3
assert C.hWorldRoot in build._skipRoot
filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(False) # Char Root
assert filterTab.optTree.topLevelItemCount() == 2
assert C.hCharRoot in build._skipRoot
filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(False) # Plot Root
assert filterTab.optTree.topLevelItemCount() == 1
assert C.hPlotRoot in build._skipRoot
# Reset Plot and Char
filterTab.filterOpt._widgets[switchMap["plotRoot"]].setChecked(True)
filterTab.filterOpt._widgets[switchMap["charRoot"]].setChecked(True)
assert filterTab.optTree.topLevelItemCount() == 3
# Switch off novel docs
filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False)
assert build.buildItemFilter(nwGUI.theProject) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (False, FilterMode.FILTERED),
C.hSceneDoc: (False, FilterMode.FILTERED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.FILTERED),
C.hCharRoot: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.FILTERED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
}
# Switch on note docs
filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True)
assert build.buildItemFilter(nwGUI.theProject) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (False, FilterMode.FILTERED),
C.hSceneDoc: (False, FilterMode.FILTERED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.FILTERED), # Set to inactive
C.hCharRoot: (False, FilterMode.SKIPPED),
hCharDoc: (True, FilterMode.FILTERED), # Now enabled
C.hWorldRoot: (False, FilterMode.SKIPPED),
}
# Switch on inactive docs
filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True)
assert build.buildItemFilter(nwGUI.theProject) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (False, FilterMode.FILTERED),
C.hSceneDoc: (False, FilterMode.FILTERED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (True, FilterMode.FILTERED), # Now enabled
C.hCharRoot: (False, FilterMode.SKIPPED),
hCharDoc: (True, FilterMode.FILTERED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
}
# Set chapter and scene docs to included
filterTab._treeMap[C.hChapterDoc].setSelected(True)
filterTab._treeMap[C.hSceneDoc].setSelected(True)
filterTab.includedButton.click()
assert build.buildItemFilter(nwGUI.theProject) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.INCLUDED), # Now included
C.hSceneDoc: (True, FilterMode.INCLUDED), # Now included
C.hPlotRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (True, FilterMode.FILTERED),
C.hCharRoot: (False, FilterMode.SKIPPED),
hCharDoc: (True, FilterMode.FILTERED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
}
# Set char and plot docs to excluded
filterTab.optTree.clearSelection()
filterTab._treeMap[hPlotDoc].setSelected(True)
filterTab._treeMap[hCharDoc].setSelected(True)
filterTab.excludedButton.click()
assert build.buildItemFilter(nwGUI.theProject) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.INCLUDED),
C.hSceneDoc: (True, FilterMode.INCLUDED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.EXCLUDED), # Now excluded
C.hCharRoot: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.EXCLUDED), # Now excluded
C.hWorldRoot: (False, FilterMode.SKIPPED),
}
# Switch on novel docs
filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True)
assert build.buildItemFilter(nwGUI.theProject) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.INCLUDED),
C.hSceneDoc: (True, FilterMode.INCLUDED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (False, FilterMode.EXCLUDED),
C.hCharRoot: (False, FilterMode.SKIPPED),
hCharDoc: (False, FilterMode.EXCLUDED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
}
# Set everything back to filtered
filterTab.optTree.clearSelection()
filterTab._treeMap[C.hChapterDoc].setSelected(True)
filterTab._treeMap[C.hSceneDoc].setSelected(True)
filterTab._treeMap[hPlotDoc].setSelected(True)
filterTab._treeMap[hCharDoc].setSelected(True)
filterTab.filteredButton.click()
assert build.buildItemFilter(nwGUI.theProject) == {
C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED),
C.hChapterDoc: (True, FilterMode.FILTERED),
C.hSceneDoc: (True, FilterMode.FILTERED),
C.hPlotRoot: (False, FilterMode.SKIPPED),
hPlotDoc: (True, FilterMode.FILTERED),
C.hCharRoot: (False, FilterMode.SKIPPED),
hCharDoc: (True, FilterMode.FILTERED),
C.hWorldRoot: (False, FilterMode.SKIPPED),
}
# Check handling of invalid project items
assert list(filterTab._treeMap.keys()) == [
C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc,
]
nwGUI.theProject.tree[hCharDoc].setRoot(None) # Char doc has no root handle
nwGUI.theProject.tree[hPlotDoc].setParent(None) # Plot doc has no parent handle
filterTab._populateTree()
assert list(filterTab._treeMap.keys()) == [
C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
C.hPlotRoot, C.hCharRoot
]
# Finish
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()
# END Test testBuildSettings_Filter
@pytest.mark.gui
def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain):
"""Test the Headings Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
ttTitle = f"Title: {nwHeadFmt.TITLE}"
chTitle = f"Chapter: {nwHeadFmt.TITLE}"
unTitle = f"Interlude: {nwHeadFmt.TITLE}"
scTitle = f"Scene: {nwHeadFmt.TITLE}"
sxTitle = f"Section: {nwHeadFmt.TITLE}"
build.setValue("headings.fmtTitle", ttTitle)
build.setValue("headings.fmtChapter", chTitle)
build.setValue("headings.fmtUnnumbered", unTitle)
build.setValue("headings.fmtScene", scTitle)
build.setValue("headings.fmtSection", sxTitle)
build.setValue("headings.hideScene", False)
build.setValue("headings.hideSection", False)
# Create the dialog and populate it
bSettings = GuiBuildSettings(nwGUI, nwGUI, build)
bSettings.show()
bSettings.loadContent()
headTab = bSettings.optTabHeadings
bSettings.optSideBar._group.button(bSettings.OPT_HEADINGS).click()
assert bSettings.toolStack.currentWidget() is headTab
# Check initial values
assert headTab.fmtTitle.text() == ttTitle
assert headTab.fmtChapter.text() == chTitle
assert headTab.fmtUnnumbered.text() == unTitle
assert headTab.fmtScene.text() == scTitle
assert headTab.fmtSection.text() == sxTitle
assert headTab.swtScene.isChecked() is False
assert headTab.swtSection.isChecked() is False
# Edit in Turn
# ============
# Nothing to apply
assert headTab._editing == 0
assert headTab.editTextBox.isEnabled() is False
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Title
headTab.btnTitle.click()
assert headTab._editing == headTab.EDIT_TITLE
assert headTab.editTextBox.isEnabled() is True
assert headTab.editTextBox.toPlainText() == ttTitle
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Chapter
headTab.btnChapter.click()
assert headTab._editing == headTab.EDIT_CHAPTER
assert headTab.editTextBox.isEnabled() is True
assert headTab.editTextBox.toPlainText() == chTitle
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Unnumbered
headTab.btnUnnumbered.click()
assert headTab._editing == headTab.EDIT_UNNUM
assert headTab.editTextBox.isEnabled() is True
assert headTab.editTextBox.toPlainText() == unTitle
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Scene
headTab.btnScene.click()
assert headTab._editing == headTab.EDIT_SCENE
assert headTab.editTextBox.isEnabled() is True
assert headTab.editTextBox.toPlainText() == scTitle
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Section
headTab.btnSection.click()
assert headTab._editing == headTab.EDIT_SECTION
assert headTab.editTextBox.isEnabled() is True
assert headTab.editTextBox.toPlainText() == sxTitle
headTab.btnApply.click()
assert headTab.editTextBox.isEnabled() is False
# Edit a Heading
# ==============
# Create new format of all bits
headTab.btnChapter.click()
allFmt = (
f"{nwHeadFmt.TITLE}{nwHeadFmt.CH_NUM}{nwHeadFmt.CH_WORD}{nwHeadFmt.CH_ROMU}"
f"{nwHeadFmt.CH_ROML}{nwHeadFmt.SC_NUM}{nwHeadFmt.SC_ABS}"
)
headTab.editTextBox.clear()
assert headTab.editTextBox.toPlainText() == ""
headTab.aInsTitle.trigger()
headTab.aInsChNum.trigger()
headTab.aInsChWord.trigger()
headTab.aInsChRomU.trigger()
headTab.aInsChRomL.trigger()
headTab.aInsScNum.trigger()
headTab.aInsScAbs.trigger()
assert headTab.editTextBox.toPlainText() == allFmt
headTab.btnApply.click()
assert build.getStr("headings.fmtChapter") == allFmt
# Check complex format
headTab.btnChapter.click()
headTab.editTextBox.setPlainText(f"Chapter {nwHeadFmt.CH_NUM}\n{nwHeadFmt.TITLE}\n")
headTab.btnApply.click()
assert build.getStr("headings.fmtChapter") == f"Chapter {nwHeadFmt.CH_NUM}//{nwHeadFmt.TITLE}"
# Set all to plain title
headTab.btnTitle.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtTitle") == nwHeadFmt.TITLE
headTab.btnChapter.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtChapter") == nwHeadFmt.TITLE
headTab.btnUnnumbered.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtUnnumbered") == nwHeadFmt.TITLE
headTab.btnScene.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtScene") == nwHeadFmt.TITLE
headTab.btnSection.click()
headTab.editTextBox.setPlainText(nwHeadFmt.TITLE)
headTab.btnApply.click()
assert build.getStr("headings.fmtSection") == nwHeadFmt.TITLE
# Check hide switches
headTab.swtScene.setChecked(True)
headTab.swtSection.setChecked(True)
headTab.saveContent()
assert build.getBool("headings.hideScene") is True
assert build.getBool("headings.hideSection") is True
# Finish
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()
# END Test testBuildSettings_Headings
@pytest.mark.gui
def testBuildSettings_Content(qtbot: QtBot, nwGUI: GuiMain):
"""Test the Content Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
build.setValue("text.includeSynopsis", False)
build.setValue("text.includeComments", False)
build.setValue("text.includeKeywords", False)
build.setValue("text.includeBodyText", False)
build.setValue("text.addNoteHeadings", False)
# Create the dialog and populate it
bSettings = GuiBuildSettings(nwGUI, nwGUI, build)
bSettings.show()
bSettings.loadContent()
contTab = bSettings.optTabContent
bSettings.optSideBar._group.button(bSettings.OPT_CONTENT).click()
assert bSettings.toolStack.currentWidget() is contTab
# Check initial values
assert contTab.incSynopsis.isChecked() is False
assert contTab.incComments.isChecked() is False
assert contTab.incKeywords.isChecked() is False
assert contTab.incBodyText.isChecked() is False
assert contTab.addNoteHead.isChecked() is False
# Toggle all
contTab.incSynopsis.setChecked(True)
contTab.incComments.setChecked(True)
contTab.incKeywords.setChecked(True)
contTab.incBodyText.setChecked(True)
contTab.addNoteHead.setChecked(True)
# Save values
contTab.saveContent()
assert build.getBool("text.includeSynopsis") is True
assert build.getBool("text.includeComments") is True
assert build.getBool("text.includeKeywords") is True
assert build.getBool("text.includeBodyText") is True
assert build.getBool("text.addNoteHeadings") is True
# Finish
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()
# END Test testBuildSettings_Content
@pytest.mark.gui
def testBuildSettings_Format(monkeypatch, qtbot: QtBot, nwGUI: GuiMain):
"""Test the Format Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
textFont = str(CONFIG.textFont)
build.setValue("format.buildLang", "en_US")
build.setValue("format.textFont", "") # Will fall back to config value
build.setValue("format.textSize", 12)
build.setValue("format.lineHeight", 1.2)
build.setValue("format.justifyText", False)
build.setValue("format.stripUnicode", False)
build.setValue("format.replaceTabs", False)
# Create the dialog and populate it
bSettings = GuiBuildSettings(nwGUI, nwGUI, build)
bSettings.show()
bSettings.loadContent()
fmtTab = bSettings.optTabFormat
bSettings.optSideBar._group.button(bSettings.OPT_FORMAT).click()
assert bSettings.toolStack.currentWidget() is fmtTab
# Check initial values
assert fmtTab.buildLang.currentData() == "en_US"
assert fmtTab.textFont.text() == textFont
assert fmtTab.textSize.value() == 12
assert fmtTab.lineHeight.value() == 1.2
assert fmtTab.justifyText.isChecked() is False
assert fmtTab.stripUnicode.isChecked() is False
assert fmtTab.replaceTabs.isChecked() is False
# Change values
fmtTab.buildLang.setCurrentIndex(fmtTab.buildLang.findData("en_GB"))
fmtTab.textFont.setText("Arial")
fmtTab.textSize.setValue(11)
fmtTab.lineHeight.setValue(1.15)
fmtTab.justifyText.setChecked(True)
fmtTab.stripUnicode.setChecked(True)
fmtTab.replaceTabs.setChecked(True)
# Save values
fmtTab.saveContent()
assert build.getStr("format.buildLang") == "en_GB"
assert build.getStr("format.textFont") == "Arial"
assert build.getInt("format.textSize") == 11
assert build.getFloat("format.lineHeight") == 1.15
assert build.getBool("format.justifyText") is True
assert build.getBool("format.stripUnicode") is True
assert build.getBool("format.replaceTabs") is True
# Check that the font dialog doesn't fail
with monkeypatch.context() as mp:
font = QFont()
font.setFamily("Times")
font.setPointSize(10)
mp.setattr(QFontDialog, "getFont", lambda *a: (font, True))
fmtTab.btnTextFont.click()
assert fmtTab.textFont.text() == "Times"
assert fmtTab.textSize.value() == 10
# Finish
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()
# END Test testBuildSettings_Format
@pytest.mark.gui
def testBuildSettings_Output(qtbot: QtBot, nwGUI: GuiMain):
"""Test the Output Tab of the GuiBuildSettings dialog."""
build = BuildSettings()
build.setValue("odt.addColours", False)
build.setValue("html.addStyles", False)
# Create the dialog and populate it
bSettings = GuiBuildSettings(nwGUI, nwGUI, build)
bSettings.show()
bSettings.loadContent()
outTab = bSettings.optTabOutput
bSettings.optSideBar._group.button(bSettings.OPT_OUTPUT).click()
assert bSettings.toolStack.currentWidget() is outTab
# Check initial values
assert outTab.odtAddColours.isChecked() is False
assert outTab.htmlAddStyles.isChecked() is False
# Toggle all
outTab.odtAddColours.setChecked(True)
outTab.htmlAddStyles.setChecked(True)
# Save values
outTab.saveContent()
assert build.getBool("odt.addColours") is True
assert build.getBool("html.addStyles") is True
# Finish
bSettings._dialogButtonClicked(bSettings.dlgButtons.button(QDialogButtonBox.Close))
# qtbot.stop()
# END Test testBuildSettings_Output
+15 -14
View File
@@ -18,6 +18,7 @@ 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 shutil
@@ -56,7 +57,11 @@ class C:
# END Class C
def cmpFiles(fileOne, fileTwo, ignoreLines=None, ignoreStart=None):
def cmpFiles(
fileOne: str | Path, fileTwo: str | Path,
ignoreLines: list | None = None,
ignoreStart: tuple | None = None
) -> bool:
"""Compare two files, but optionally ignore lines given by a list.
"""
if ignoreLines is None:
@@ -107,32 +112,28 @@ def cmpFiles(fileOne, fileTwo, ignoreLines=None, ignoreStart=None):
return not diffFound
def getGuiItem(theName):
"""Returns a QtWidget based on its objectName.
"""
def getGuiItem(name: str):
"""Returns a QtWidget based on its objectName."""
for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == theName:
if qWidget.objectName() == name:
return qWidget
return None
def readFile(fileName):
"""Returns the content of a file as a string.
"""
def readFile(fileName: str | Path):
"""Returns the content of a file as a string."""
with open(fileName, mode="r", encoding="utf-8") as inFile:
return inFile.read()
def writeFile(fileName, fileData):
"""Write the contents of a string to a file.
"""
def writeFile(fileName: str | Path, fileData: str):
"""Write the contents of a string to a file."""
with open(fileName, mode="w", encoding="utf-8") as outFile:
outFile.write(fileData)
def cleanProject(path):
"""Delete all generated files in a project.
"""
def cleanProject(path: str | Path):
"""Delete all generated files in a project."""
path = Path(path)
cacheDir = path / "cache"
if cacheDir.is_dir():