Merge pull request #696 from vkbo/testing

Testing to Main
This commit is contained in:
Veronica Berglyd Olsen
2021-03-02 11:36:30 +01:00
committed by GitHub
83 changed files with 5936 additions and 1487 deletions
+1
View File
@@ -23,6 +23,7 @@ __pycache__
/sample/meta
*.bak
*.lock
ToC.txt
# PyTest
/prof/
+174 -1
View File
@@ -1,5 +1,112 @@
# novelWriter Changelog
## Version 1.2 RC 1 [2021-03-02]
### Release Notes
This is the release candidate of 1.2. The release is intended for testing of new features and to
sort out potential bugs before the full release. Please take this into account when working on your
live projects.
#### The Build Novel Project Tool
The main changes for version 1.2 are to the Build Novel Project tool. The Open Document export, as
well as the Markdown export, is now handled entirely by code written for novelWriter. Previously,
these export features depended on the underlying Qt library's save routines connected to the
preview document shown in the build dialog. Using this method of export both meant that the content
of the document was dependent on the preview being generated first, and it also meant that the
exported document had limited support for novelWriter-specific features and custom formatting. The
new export class should generate a much better result, especially for the Open Document formats.
The Open Document standard is supported by Open Office, Libre Office, Google Docs, Microsoft Word,
and probably a number of other applications too. The Markdown export hasn't changed a lot, but
should be a slight improvement on the previous export feature.
These changes to the build tool also imply that the saving process is now independent of the
content of the preview window, meaning you don't have to rebuild the preview before saving, which
was previously the case. To make this more consistent, the PDF export option has been moved to the
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
export format. It is exactly the same as printing to file from the print preview dialog.
In addition to the changes to the export features, the Build Novel Project tool now also has
controls for line height, which applies to all rich text export formats, and the option to replace
unicode characters with appropriate html entities for html export.
#### Document Layout Automation
Among other changes in this release are a few improvements to the process of creating and changing
documents. When a new document is first created, the header is generated from the assigned label
and layout.
In addition, for some document layouts, when the user changes the header level of the first header
of the document, the document layout setting is updated accordingly. This should reduce the need
for the user to maintain two ways of assigning the role of a given document. This automation only
applies to combinations of header level and current document layouts where there is no ambiguity.
For instance, changing the header level in a "Scene" document from level 3 to 2 changes the
document layout automatically to "Chapter". But changing the first header of a "Book" layout
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
document layout and it's perfectly reasonable for its first header to be a chapter header.
Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
the same during exports. The distinction is only meant as a way to indicate the purpose of a
document in the project tree. This new automation is meant to assist in keeping this information up
to date. The other layouts do have an effect on formatting during export, and are generally left
alone.
#### The Session Timer and Idle Time
Another change that has been requested by a couple of users is to have the session timer in the
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
controlled from Preferences. The definition of "idle" in this context is either that the user is
active in a different application than novelWriter (loss of focus) or that the user has not made
any changes to the current document for a given amount of time. The time threshold is by default
five minutes, but can be altered in Preferences.
In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
writing stats log entries.
#### Other Changes
The user dictionary of the project, where words added to the dictionary from the document editor
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.
A small additional feature added is also the ability to undo the last move of an item in the
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
by up/down keypress or menu entries, and drag and drop moves.
Lastly, a new keyword has been added to mark characters in the story. The new keyword is intended
to tag a character as the focus character for a chapter or scene. This is useful for stories where
the point-of-view character and the focus character are different.
_These Release Notes also include the changes from 1.2 Beta 1._
### Detailed Changelog
**Bugfixes**
* If a tag or reference keyword was mistyped, it would still be indexed and put into the index.
This caused the index to be deemed invalid on the next loading of the project, triggering a
rebuild. A check has been added to the code parsing the lines starting with `@` to ensure only
valid keywords are written into the index. Issue #688. PR #690.
**New Features**
* Added a tool to edit the project's user dictionary. This is the dictionary where the "Add Word to
Dictionary" actions from the document editor go. The dialog tool allows for listing, removing and
adding words to this dictionary. Issue #665. PR #669.
**User Interface**
* Pressing the `F2` key when in the document editor will open the Item Editor for the open document
instead of the one selected in the project tree. PR #664.
* The shortcut for deleting an item in the project tree has been changed from `Ctrl+Del` to
`Ctrl+Shift+Del`. This change was also backported to version 1.1.1. Resolves #629. PR #664.
----
## Version 1.1.1 [2021-02-21]
### Release Notes
@@ -53,6 +160,72 @@ but is still lacking in functionality compared to Enchant.
----
## Version 1.2 Beta 1 [2021-02-11]
### Detailed Changelog
**New Features**
* A full Open Document exporter has been written and added for the Build Novel Project tool. This
replaces the previously used Qt Save to ODT which just saved the content of the preview window to
an ODT file. The Qt pathway was very limited and didn't generate proper formatting classes. The
new export class can generate both full `.odt` and flat XML `.fodt` files. The formatting support
of this exporter is equivalent or better than the HTML5 exporter, which was previously the best
supported option. Solves issue #611. PRs #607, #652, #660, and #654.
* A full Markdown exporter has been written and added for the Build Novel Project tool. This too
replaces a Qt feature that was used to save the content of the preview window into a markdown
file. The exporter allows for both standard markdown and GitHub flavour markdown. The only
relevant difference being that the latter allows strikethrough text. Issue #617. PR #650.
* The Build Novel Project tool now has a "Line height" property that is applied to the preview, and
to the HTML5 and Open Document export formats. Discussion #653. Issue #654. PR #660.
* The Build Novel Project tool now has an option to convert Unicode characters to HTML entities on
export to HTML5. Previously, some symbols were converted while others were not. This option
provides a more consistent "all or nothing" option. PR #660.
* The last file or folder move in the project tree can now be undone from the Project menu or by
pressing `Ctrl+Shift+Z`. PR #632.
* When a document header level is altered in a novel file of layout type Scene, Chapter,
Unnumbered, or Partition, and that document is saved, the document's layout setting as seen in
the project tree is updated to reflect the new level of that heading. This helps reduce the
duplication of effort by the user to keep this information in sync. See discussion #613. This is
an acceptable solution to #614. Issue #618. PR #620.
* The session timer now records the amount of time the user is idle. Idle is defined to be when the
application window does not have focus, and when the user hasn't made any changes to the document
open in the editor for a specified amount of time. The default is 5 minutes. The idle time is
recorded to the session log and can be shown in Writing Statistics. Optionally, the status bar
session timer can also be set to pause when the user is considered idle. Issues #606 and #651.
PRs #656 and #661.
* It is now possible to tag a character as the focus character for a give section of text. This is
useful for cases where the point-of-view character differs from the main character of the story,
or the part of the story. Issue #605. PR #662.
**User Interface**
* A document's class and layout is now displayed next to its status or importance in the document
editor footer bar. PR #628.
* When a new document is created, the header of the file is automatically generated based on the
document's tree label, and the level determined by the selected layout. Issue #530. PR #628.
* On the Build Novel Project GUI, the PDF option has been moved from the "Save As" button to the
"Print" button. This more accurately reflects what it actually does: print the content of the
preview to a PDF using the printer pathway. This also means that all remaining items on the "Save
As" dropdown list now can be executed regardless of the content of the preview window. They all
run their own separate build process. This resolves #611. PR #650.
* Export to plain text has been dropped. PR #617.
**Code Improvements**
* The index class now scans every element of the loaded index cache before accepting it. This means
that any unrecognised content will trigger a full re-indexing. This is particularly useful when
opening an index that was saved by a later release. Every entry that requires a lookup is checked
to avoid potential key errors for instance. All values are also checked for correct data type.
The new check is extensive, but still fast enough that it only adds a few milliseconds to the
startup time. PR #619.
**Code Maintenance**
* Cleaned up some redundant code after PR #637. PR #638.
----
## Version 1.1 [2021-02-07]
### Release Notes
@@ -87,7 +260,7 @@ _These Release Notes also include the changes from 1.1 RC 1._
**Bugfixes**
* A `None` check in the details panel below the project tree was missing, resulting in an
occasional error message being printed to the logging output. The error was otherwise handles, so
occasional error message being printed to the logging output. The error was otherwise handled, so
this is mainly a fix to prevent the error message. PR #639.
**User Interface**
+2 -2
View File
@@ -25,9 +25,9 @@ copyright = "20182021, Veronica Berglyd Olsen"
author = "Veronica Berglyd Olsen"
# The short X.Y version
version = "1.1.1"
version = "1.2"
# The full version, including alpha/beta/rc tags
release = "1.1.1"
release = "1.2-rc1"
# -- General configuration ---------------------------------------------------
+4 -6
View File
@@ -93,13 +93,11 @@ Project Export
==============
The project can at any time be exported to a range of different formats through the
:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to plain text file,
HTML document, novelWriter flavoured markdown, standard markdown (requires Qt 5.14), and to a basic
Open Document format.
:guilabel:`Build Novel Project` tool. Natively, novelWriter supports export to Open Document,
HTML5, and various flavours of Markdown.
In addition, printing and printing to PDF is also possible. The best supported export format is
HTML, which can be imported or converted by a number of other tools like Pandoc, or simply imported
into Libre Office Writer and similar word processors.
The HTML5 export format is suitable for convertion by a number of other tools like Pandoc, or for
importing into word processors. In addition, printing and printing to PDF is also possible.
It is also possible to export the content of the project to a JSON file. This is useful if you want
to write your own processing script in for instance Python as the entire novel can be read into a
+3 -4
View File
@@ -44,10 +44,9 @@ The following Python packages are needed to run novelWriter:
* ``pyenchant`` needed for efficient spell checking (optional).
PyQt/Qt should be at least 5.2.1, but ideally 5.10 or higher for nearly all features to work.
Exporting to standard Markdown, for instance, requires PyQt/Qt 5.14. Searching using regular
expressions requires 5.3, and for full Unicode support, 5.13. There is no known minimum version
requirement for package ``lxml``, but the code was originally written with 4.2, which is therefore
set as the minimum. It may work on lower versions. You have to test it.
Searching using regular expressions requires 5.3, and for full Unicode support, 5.13. There is no
known minimum version requirement for package ``lxml``, but the code was originally written with
4.2, which is therefore set as the minimum. It may work on lower versions. You have to test it.
Optionally, a package can be installed to interface with the Enchant spell checking libaries, but
this isn't strictly required. If no external spell checking library is available, novelWriter falls
+17 -17
View File
@@ -102,6 +102,16 @@ document has a checkmark after the status icon in the :guilabel:`Flags` column.
override these settings.
.. _a_export_print:
Printing
========
The print button allows you to print the content in the preview window. You can either print to one
of your system's printers, or print directly to file. You can also print to file from the regular
print dialog. The direct to file option is just a shortcut.
.. _a_export_formats:
Export Formats
@@ -110,21 +120,13 @@ Export Formats
Currently, six formats are supported for exporting.
OpenDocument Format
This produces an open document ``.odt`` file. The document produced has very little formatting,
and may require further editing afterwards. For a better formatted office document, you may get
a better result by exporting to HTML and then importing that HTML document into your office word
processor. They are generally good at importing HTML documents.
PDF Format
The PDF export is just a shortcut for print-to-file. For a better PDF result, you may instead
want to export to HTML and use a word processor to convert the HTML document to PDF.
The Build tool can produce either an ``.odt`` file, of a ``.fodt`` file. The latter is just a
flat version of the document format as a single XML file.
novelWriter HTML
The HTML export format writes a single ``.htm`` file with minimal style formatting. The exported
HTML document is suitable for further processing by document conversion tools like Pandoc, for
importing in word processors, or for printing from browser. It is generally the best formatted
export option and supports all features of novelWriter since it is entirely generated by the
application and doesn't depend on Qt library features.
importing in word processors, or for printing from browser.
novelWriter Markdown
This is simply a concatenation of the project documents selected by the filters. The documents
@@ -132,12 +134,10 @@ novelWriter Markdown
included if they are selected. This is a useful format for exporting the project for later
import back into novelWriter.
Standard Markdown
If you have Qt 5.14 or higher, the option to export to plain markdown is available. This feature
uses Qt's own markdown export feature.
Plain Text
The plain text export format writes a simple ``.txt`` file without any formatting at all.
Standard/GitHub Markdown
The Markdown export format comes in both Standard and GitHub flavour. The *only* difference in
terms of novelWriter functionality is the support of strikethrough text, which is not supported
by the Standard flavour, but *is* supported by the GitHub flavour.
.. _a_export_options:
+6 -3
View File
@@ -377,6 +377,10 @@ Most features are available as keyboard shortcuts. These are as follows:
":kbd:`Ctrl`:kbd:`Z`", "Undo latest changes."
":kbd:`Ctrl`:kbd:`F7`", "Toggle spell checking."
":kbd:`Ctrl`:kbd:`F10`", "Toggle automatic updating of project outline."
":kbd:`Ctrl`:kbd:`Up`", "Move item one step up in the project tree."
":kbd:`Ctrl`:kbd:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Del`", "Delete next word in editor."
":kbd:`Ctrl`:kbd:`Backspace`", "Delete previous word in editor."
":kbd:`Ctrl`:kbd:`'`", "Wrap selected text, or word under cursor, in single quotes."
":kbd:`Ctrl`:kbd:`""`", "Wrap selected text, or word under cursor, in double quotes."
":kbd:`Ctrl`:kbd:`Enter`", "Open the tag or reference under the cursor in the Viewer."
@@ -391,9 +395,7 @@ Most features are available as keyboard shortcuts. These are as follows:
":kbd:`Ctrl`:kbd:`Shift`:kbd:`R`", "Close the document viewer."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`S`", "Save the current project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`W`", "Close the current project."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Alternative sequence for redo last undo."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Up`", "Move item one step up in the project tree."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Down`", "Move item one step down in the project tree."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Z`", "Undo move of project tree item."
":kbd:`Ctrl`:kbd:`Shift`:kbd:`Del`", "If in the project tree, move a document to trash, or delete a folder."
":kbd:`F1`", "Open the documentation. This will either open the Qt Assistant, if available, or send you to the documentation website."
":kbd:`F2`", "If in the project tree, edit a document or folder settings. (Same as :kbd:`Ctrl`:kbd:`E`)"
@@ -453,6 +455,7 @@ a key or key combination for the inserted content.
":kbd:`Ctrl`:kbd:`K`, :kbd:`Ctrl`:kbd:`D`", "Insert a division sign."
":kbd:`Ctrl`:kbd:`K`, :kbd:`G`", "Insert a ``@tag`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`V`", "Insert a ``@pov`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`F`", "Insert a ``@focus`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`C`", "Insert a ``@char`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`P`", "Insert a ``@plot`` keyword."
":kbd:`Ctrl`:kbd:`K`, :kbd:`T`", "Insert a ``@time`` keyword."
+3 -2
View File
@@ -51,8 +51,9 @@ no restrictions are enforced by the application. You can use them however you wa
:guilabel:`Characters`
Character notes go in this root folder. These are especially important if one wants to use the
Outline view to see which character appears where, and which part of the story is told from a
specific character's point-of-view. Tags in this folder can be references using the ``@pov``
keyword for point-of-view characters, or the ``@char`` keyword for other characters.
specific character's point-of-view or focusing on a particular character's storyline. Tags in
this folder can be referenced using the ``@pov`` keyword for point-of-view characters,
``@focus`` for a focus character, or the ``@char`` keyword for any other characters.
:guilabel:`Locations`
The locations folder is for various scene locations that you want to track. Tags in this folder
+11 -6
View File
@@ -103,28 +103,33 @@ allow multiple values.
The point-of-view character for the current section. The target must be a note tag in the
:guilabel:`Character` type root folder.
``@focus``
The character that has the focus for the current section. This can be used in cases where the
focus is not a point-of-view character. The target must be a note tag in the
:guilabel:`Character` type root folder.
``@char``
Other characters in the current section. The target must be a note tag in a
Other characters in the current section. The target must be a note tag in the
:guilabel:`Character` type root folder. This should not include the point-of-view character.
``@plot``
The plot or subplot advanced in the current section. The target must be a note tag in a
The plot or subplot advanced in the current section. The target must be a note tag in the
:guilabel:`Plot` type root folder.
``@time``
The timelines touched by the current section. The target must be a note tag in a
The timelines touched by the current section. The target must be a note tag in the
:guilabel:`Timeline` type root folder.
``@location``
The location the current section takes place in. The target must be a note tag in a
The location the current section takes place in. The target must be a note tag in the
:guilabel:`Locations` type root folder.
``@object``
Objects present in the current section. The target must be a note tag in an :guilabel:`Object`
Objects present in the current section. The target must be a note tag in the :guilabel:`Object`
type root folder.
``@entity``
Entities present in the current section. The target must be a note tag in an
Entities present in the current section. The target must be a note tag in the
:guilabel:`Entities` type root folder.
``@custom``
+6 -6
View File
@@ -31,7 +31,7 @@ import logging
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import QApplication, QErrorMessage
from nw.error import exceptionHandler
from nw.error import exceptionHandler, logException
from nw.config import Config
##
@@ -62,9 +62,9 @@ __license__ = "GPLv3"
__author__ = "Veronica Berglyd Olsen"
__maintainer__ = "Veronica Berglyd Olsen"
__email__ = "code@vkbo.net"
__version__ = "1.1.1"
__hexversion__ = "0x010101f0"
__date__ = "2021-02-21"
__version__ = "1.2rc1"
__hexversion__ = "0x010200c1"
__date__ = "2021-03-02"
__status__ = "Stable"
__domain__ = "novelwriter.io"
__url__ = "https://novelwriter.io"
@@ -264,9 +264,9 @@ def main(sysArgs=None):
bundle = NSBundle.mainBundle()
info = bundle.localizedInfoDictionary() or bundle.infoDictionary()
info["CFBundleName"] = "novelWriter"
except ImportError as e:
except ImportError:
logger.error("Failed to set application name")
logger.error(str(e))
logException()
# Import GUI (after dependency checks), and launch
from nw.guimain import GuiMain
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5582">
<metadata
id="metadata5588">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5586" />
<path
d="m 6.7499997,3 c -1.656,0 -3,1.3440001 -3,3.0000001 V 17.999999 c 0,1.656 1.344,3.000001 3,3.000001 1.6560002,0 2.9999998,-1.344001 2.9999998,-3.000001 V 6.0000001 C 9.7499995,4.3440001 8.4059999,3 6.7499997,3 Z M 17.249999,3 c -1.656,0 -3.000001,1.3440001 -3.000001,3.0000001 V 17.999999 c 0,1.656 1.344001,3.000001 3.000001,3.000001 1.656,0 3.000001,-1.344001 3.000001,-3.000001 V 6.0000001 C 20.25,4.3440001 18.905999,3 17.249999,3 Z"
id="path5580"
style="fill:#aeaeae;fill-opacity:1;stroke-width:1.49999" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+31
View File
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
version="1.2"
width="24"
height="24"
viewBox="0 0 24 24"
id="svg5582">
<metadata
id="metadata5588">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs5586" />
<path
d="m 6.7499997,3 c -1.656,0 -3,1.3440001 -3,3.0000001 V 17.999999 c 0,1.656 1.344,3.000001 3,3.000001 1.6560002,0 2.9999998,-1.344001 2.9999998,-3.000001 V 6.0000001 C 9.7499995,4.3440001 8.4059999,3 6.7499997,3 Z M 17.249999,3 c -1.656,0 -3.000001,1.3440001 -3.000001,3.0000001 V 17.999999 c 0,1.656 1.344001,3.000001 3.000001,3.000001 1.656,0 3.000001,-1.344001 3.000001,-3.000001 V 6.0000001 C 20.25,4.3440001 18.905999,3 17.249999,3 Z"
id="path5580"
style="fill:#000000;fill-opacity:0.780392;stroke-width:1.49999" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+67 -40
View File
@@ -2,48 +2,75 @@
<html>
<body>
<h2>Release Notes for 1.1.1</h2>
<p><i>Released on 21 February 2021</i></p>
<p>This patch makes a couple of minor improvements to the GUI. The keyboard shortcut for deleting
entries in the project tree has been changed from "Ctrl+Del" to "Ctrl+Shift+Del" to free up that
shortcut for the document editor. It is now possible to use the shortcut for deleting the word in
front of the cursor, a common and useful feature of many text editors.</p>
<p>The way the negative word count filter option works on the Writing Statistics tool has been
changed to be more intuitive. Enabling this filter now appears to remove all negative entries
without altering the other entries. Previously, the removed negative counts would be included in
the following entries to make it consistent with the total word count. In addition, writing
sessions shorter than five minutes, and with no change in the word count, are no longer recorded in
the session log file.</p>
<p>Other changes include improving the speed of the internal spell checker, used when the Enchant
spell check library isn't available. The internal spell checker is no longer significantly slower,
but is still lacking in functionality compared to Enchant.</p>
<h2>Release Notes for 1.2 RC 1</h2>
<p><i>Released on 2 March 2021</i></p>
<p>This is the release candidate of 1.2. The release is intended for testing of new features and to
sort out potential bugs before the full release. Please take this into account when working on your
live projects.</p>
<h3>The Build Novel Project Tool</h3>
<p>The main changes for version 1.2 are to the Build Novel Project tool. The Open Document export,
as well as the Markdown export, is now handled entirely by code written for novelWriter.
Previously, these export features depended on the underlying Qt library's save routines connected
to the preview document shown in the build dialog. Using this method of export both meant that the
content of the document was dependent on the preview being generated first, and it also meant that
the exported document had limited support for novelWriter-specific features and custom formatting.
The new export class should generate a much better result, especially for the Open Document
formats. The Open Document standard is supported by Open Office, Libre Office, Google Docs,
Microsoft Word, and probably a number of other applications too. The Markdown export hasn't changed
a lot, but should be a slight improvement on the previous export feature.</p>
<p>These changes to the build tool also imply that the saving process is now independent of the
content of the preview window, meaning you don't have to rebuild the preview before saving, which
was previously the case. To make this more consistent, the PDF export option has been moved to the
print button as it is actually a print-to-file feature under the hood, not technically a proper PDF
export format. It is exactly the same as printing to file from the print preview dialog.</p>
<p>In addition to the changes to the export features, the Build Novel Project tool now also has
controls for line height, which applies to all rich text export formats, and the option to replace
unicode characters with appropriate html entities for html export.</p>
<h3>Document Layout Automation</h3>
<p>Among other changes in this release are a few improvements to the process of creating and
changing documents. When a new document is first created, the header is generated from the assigned
label and layout.</p>
<p>In addition, for some document layouts, when the user changes the header level of the first
header of the document, the document layout setting is updated accordingly. This should reduce the
need for the user to maintain two ways of assigning the role of a given document. This automation
only applies to combinations of header level and current document layouts where there is no
ambiguity. For instance, changing the header level in a "Scene" document from level 3 to 2 changes
the document layout automatically to "Chapter". But changing the first header of a "Book" layout
document from 1 to 2 does not change the document's layout as the "Book" layout is a generic
document layout and it's perfectly reasonable for its first header to be a chapter header.</p>
<p>Keep in mind that novelWriter treats documents with layout "Book", "Chapter" and "Scene" exactly
the same during exports. The distinction is only meant as a way to indicate the purpose of a
document in the project tree. This new automation is meant to assist in keeping this information up
to date. The other layouts do have an effect on formatting during export, and are generally left
alone.</p>
<h3>The Session Timer and Idle Time</h3>
<p>Another change that has been requested by a couple of users is to have the session timer in the
status bar stop counting when the user is inactive (idle). This feature is optional, and can be
controlled from Preferences. The definition of "idle" in this context is either that the user is
active in a different application than novelWriter (loss of focus) or that the user has not made
any changes to the current document for a given amount of time. The time threshold is by default
five minutes, but can be altered in Preferences.</p>
<p>In addition, the idle time is also recorded in the session log, and can be viewed in the Writing
Statistics dialog and exported with the rest of the information. The idle time is recorder in the
logs regardless of whether the status bar clock takes idle time into consideration or not. So even
if you turn off the idle time switch in Preferences, the other idle time setting still affects the
writing stats log entries.</p>
<h3>Other Changes</h3>
<p>The user dictionary of the project, where words added to the dictionary from the document editor
go, can now be viewed and edited with a new "Project Word List" tool in the "Tools" menu.</p>
<p>A small additional feature added is also the ability to undo the last move of an item in the
project tree. The keyboard shortcut for this is `Ctrl+Shift+Z`, or it can be accessed from the
menu. The feature can only undo the last move, but it includes both documents moved to trash, moves
by up/down keypress or menu entries, and drag and drop moves.</p>
<p>Lastly, a new keyword has been added to mark characters in the story. The new keyword is
intended to tag a character as the focus character for a chapter or scene. This is useful for
stories where the point-of-view character and the focus character are different.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
<hr/>
<h2>Release Notes for 1.1</h2>
<p><i>Released on 7 February 2021</i></p>
<p>The main change in this release is the addition of a new tab to the project tree on the left
side of the main window. The regular project tree is now on a tab named "Project", while a new tab
named "Novel" displays a simpler version of the information on the main "Outline" page. It lists
all the headers of the novel part of the project, as well as the word count and point-of-view
character of each section. This is an alternative way to navigate the novel part of the project.
The various tree views are now also kept better in sync when the user selects various documents and
headers.</p>
<p>In addition, a new information dialog named "Project Details" has been added. It replaces the
"Details" tab in "Project Settings", and adds more information about the novel part of the project.
In particular, a "Table of Contents" in the "Contents" tab displays a summary of the main parts
and chapters of the project, their total word counts, and an estimated page count. This was made in
response to users asking for ways to estimate the total page count of the project. The page count
is estimated based on a words per page setting, which can be changed on the dialog window.</p>
<p>Since the tabs below the project tree now add some extra room on the GUI, some convenient
buttons have been added in the same area, with direct access to "Project Details", "Writing
Statistics" and "Project Setting".</p>
<p>A few other minor changes have been made as well. The Preferences dialog has been improved with
clearer categories and hopefully better help text. Some new options have been added too. They allow
syntax highlighting of multi-paragraph quotes. The highlighter can now optionally accept quotes to
be left "hanging", that is, no closing quote in the same paragraph.</p>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
<!DOCTYPE html public "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<body>
<h2>Release Notes for 1.1.1</h2>
<p><i>Released on 21 February 2021</i></p>
<p>This patch makes a couple of minor improvements to the GUI. The keyboard shortcut for deleting
entries in the project tree has been changed from "Ctrl+Del" to "Ctrl+Shift+Del" to free up that
shortcut for the document editor. It is now possible to use the shortcut for deleting the word in
front of the cursor, a common and useful feature of many text editors.</p>
<p>The way the negative word count filter option works on the Writing Statistics tool has been
changed to be more intuitive. Enabling this filter now appears to remove all negative entries
without altering the other entries. Previously, the removed negative counts would be included in
the following entries to make it consistent with the total word count. In addition, writing
sessions shorter than five minutes, and with no change in the word count, are no longer recorded in
the session log file.</p>
<p>Other changes include improving the speed of the internal spell checker, used when the Enchant
spell check library isn't available. The internal spell checker is no longer significantly slower,
but is still lacking in functionality compared to Enchant.</p>
<p><i>See also the <a href="https://github.com/vkbo/novelWriter/releases">Releases</a> page.</i></p>
<hr/>
<h2>Release Notes for 1.1</h2>
<p><i>Released on 7 February 2021</i></p>
<p>The main change in this release is the addition of a new tab to the project tree on the left
side of the main window. The regular project tree is now on a tab named "Project", while a new tab
named "Novel" displays a simpler version of the information on the main "Outline" page. It lists
all the headers of the novel part of the project, as well as the word count and point-of-view
character of each section. This is an alternative way to navigate the novel part of the project.
The various tree views are now also kept better in sync when the user selects various documents and
headers.</p>
<p>In addition, a new information dialog named "Project Details" has been added. It replaces the
"Details" tab in "Project Settings", and adds more information about the novel part of the project.
In particular, a "Table of Contents" in the "Contents" tab displays a summary of the main parts
and chapters of the project, their total word counts, and an estimated page count. This was made in
response to users asking for ways to estimate the total page count of the project. The page count
is estimated based on a words per page setting, which can be changed on the dialog window.</p>
<p>Since the tabs below the project tree now add some extra room on the GUI, some convenient
buttons have been added in the same area, with direct access to "Project Details", "Writing
Statistics" and "Project Setting".</p>
<p>A few other minor changes have been made as well. The Preferences dialog has been improved with
clearer categories and hopefully better help text. Some new options have been added too. They allow
syntax highlighting of multi-paragraph quotes. The highlighter can now optionally accept quotes to
be left "hanging", that is, no closing quote in the same paragraph.</p>
</body>
</html>
+34 -4
View File
@@ -30,7 +30,9 @@ from datetime import datetime
from PyQt5.QtWidgets import qApp
from nw.constants import nwConst, nwUnicode
from nw.constants import (
nwConst, nwUnicode, nwItemClass, nwItemType, nwItemLayout
)
logger = logging.getLogger(__name__)
@@ -103,11 +105,39 @@ def isHandle(theString):
return False
if len(theString) != 13:
return False
invalidChar = False
for c in theString:
if c not in "0123456789abcdef":
invalidChar = True
return not invalidChar
return False
return True
def isTitleTag(theString):
"""Check if a string is a valid title string.
"""
if not isinstance(theString, str):
return False
if len(theString) != 7:
return False
if not theString.startswith("T"):
return False
for c in theString[1:]:
if c not in "0123456789":
return False
return True
def isItemClass(theString):
"""Check if an item is a calid nwItemClass identifier.
"""
return theString in nwItemClass.__members__
def isItemType(theString):
"""Check if an item is a calid nwItemType identifier.
"""
return theString in nwItemType.__members__
def isItemLayout(theString):
"""Check if an item is a calid nwItemLayout identifier.
"""
return theString in nwItemLayout.__members__
def hexToInt(value, default=0):
"""Convert a hex string to an integer.
+18 -6
View File
@@ -38,6 +38,7 @@ from PyQt5.QtCore import QT_VERSION_STR, QStandardPaths, QSysInfo
from nw.constants import nwConst, nwFiles, nwUnicode
from nw.common import splitVersionNumber, formatTimeStamp
from nw.error import logException
logger = logging.getLogger(__name__)
@@ -143,6 +144,9 @@ class Config:
self.allowOpenDQuote = True # Allow open-ended double quotes
self.highlightEmph = True # Add colour to text emphasis
self.stopWhenIdle = True # Stop the status bar clock when the user is idle
self.userIdleTime = 300 # Time of inactivity to consider user idle
## User-Selected Symbols
self.fmtApostrophe = nwUnicode.U_RSQUO
self.fmtSingleQuotes = [nwUnicode.U_LSQUO, nwUnicode.U_RSQUO]
@@ -296,7 +300,7 @@ class Config:
os.mkdir(self.confPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.confPath)
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.confPath)
self.errData.append(str(e))
@@ -319,7 +323,7 @@ class Config:
os.mkdir(self.dataPath)
except Exception as e:
logger.error("Could not create folder: %s" % self.dataPath)
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not create folder: %s" % self.dataPath)
self.errData.append(str(e))
@@ -364,7 +368,7 @@ class Config:
cnfParse.read_file(inFile)
except Exception as e:
logger.error("Could not load config file")
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not load config file")
self.errData.append(str(e))
@@ -531,6 +535,12 @@ class Config:
self.highlightEmph = self._parseLine(
cnfParse, cnfSec, "highlightemph", self.CNF_BOOL, self.highlightEmph
)
self.stopWhenIdle = self._parseLine(
cnfParse, cnfSec, "stopwhenidle", self.CNF_BOOL, self.stopWhenIdle
)
self.userIdleTime = self._parseLine(
cnfParse, cnfSec, "useridletime", self.CNF_INT, self.userIdleTime
)
## Backup
cnfSec = "Backup"
@@ -671,6 +681,8 @@ class Config:
cnfParse.set(cnfSec, "allowopensquote", str(self.allowOpenSQuote))
cnfParse.set(cnfSec, "allowopendquote", str(self.allowOpenDQuote))
cnfParse.set(cnfSec, "highlightemph", str(self.highlightEmph))
cnfParse.set(cnfSec, "stopwhenidle", str(self.stopWhenIdle))
cnfParse.set(cnfSec, "useridletime", str(self.userIdleTime))
## Backup
cnfSec = "Backup"
@@ -705,7 +717,7 @@ class Config:
self.confChanged = False
except Exception as e:
logger.error("Could not save config file")
logger.error(str(e))
logException()
self.hasError = True
self.errData.append("Could not save config file")
self.errData.append(str(e))
@@ -981,9 +993,9 @@ class Config:
return self._unpackList(
cnfParse.get(cnfSec, cnfName), cnfDefault, self.CNF_S_LST
)
except ValueError as e:
except ValueError:
logger.error("Failed to load value from config file.")
logger.error(str(e))
logException()
return cnfDefault
return cnfDefault
+3 -1
View File
@@ -1,7 +1,8 @@
# -*- coding: utf-8 -*-
from nw.constants.iso import isoLanguage, isoCountry
from nw.constants.constants import (
nwConst, nwLists, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes, nwUnicode
nwConst, nwLists, nwRegEx, nwFiles, nwKeyWords, nwLabels, nwQuotes,
nwUnicode, nwHtmlUnicode
)
from nw.constants.enum import (
nwAlert, nwDocAction, nwItemClass, nwItemLayout, nwItemType, nwOutline,
@@ -19,6 +20,7 @@ __all__ = [
"nwLabels",
"nwQuotes",
"nwUnicode",
"nwHtmlUnicode",
"nwAlert",
"nwDocAction",
"nwItemClass",
+68 -4
View File
@@ -55,6 +55,9 @@ class nwLists():
# Item classes where the full list of novel layouts are allowed
CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
# Item classes which do not require items to have same class
FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
# END Class nwLists
class nwRegEx():
@@ -83,6 +86,7 @@ class nwKeyWords:
TAG_KEY = "@tag"
POV_KEY = "@pov"
FOCUS_KEY = "@focus"
CHAR_KEY = "@char"
PLOT_KEY = "@plot"
TIME_KEY = "@time"
@@ -93,14 +97,15 @@ class nwKeyWords:
# Set of Valid Keys
VALID_KEYS = {
TAG_KEY, POV_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
TAG_KEY, POV_KEY, FOCUS_KEY, CHAR_KEY, PLOT_KEY, TIME_KEY,
WORLD_KEY, OBJECT_KEY, ENTITY_KEY, CUSTOM_KEY
}
# Map from Keys to Item Class
KEY_CLASS = {
CHAR_KEY : nwItemClass.CHARACTER,
POV_KEY : nwItemClass.CHARACTER,
FOCUS_KEY : nwItemClass.CHARACTER,
CHAR_KEY : nwItemClass.CHARACTER,
PLOT_KEY : nwItemClass.PLOT,
TIME_KEY : nwItemClass.TIMELINE,
WORLD_KEY : nwItemClass.WORLD,
@@ -177,6 +182,7 @@ class nwLabels():
KEY_NAME = {
nwKeyWords.TAG_KEY : "Tag",
nwKeyWords.POV_KEY : "Point of View",
nwKeyWords.FOCUS_KEY : "Focus",
nwKeyWords.CHAR_KEY : "Characters",
nwKeyWords.PLOT_KEY : "Plot",
nwKeyWords.TIME_KEY : "Timeline",
@@ -194,6 +200,7 @@ class nwLabels():
nwOutline.WCOUNT : "Words",
nwOutline.PCOUNT : "Pars",
nwOutline.POV : "POV",
nwOutline.FOCUS : "Focus",
nwOutline.CHAR : KEY_NAME[nwKeyWords.CHAR_KEY],
nwOutline.PLOT : KEY_NAME[nwKeyWords.PLOT_KEY],
nwOutline.TIME : KEY_NAME[nwKeyWords.TIME_KEY],
@@ -262,7 +269,7 @@ class nwUnicode:
U_LCQUO = "\u300c" # Left corner bracket
U_RCQUO = "\u300d" # Right corner bracket
U_LWCQUO = "\u300e" # Left white corner bracket
U_RECQUO = "\u300f" # Right white corner bracket
U_RWCQUO = "\u300f" # Right white corner bracket
## Punctuation
U_FGDASH = "\u2012" # Figure dash
@@ -328,7 +335,7 @@ class nwUnicode:
H_LCQUO = "&#12300;"
H_RCQUO = "&#12301;"
H_LWCQUO = "&#12302;"
H_LWCQUO = "&#12302;"
H_RWCQUO = "&#12303;"
## Punctuation
H_FGDASH = "&#8210;"
@@ -371,3 +378,60 @@ class nwUnicode:
H_LTRIS = "&#9666;"
# END Class nwUnicode
class nwHtmlUnicode():
U_TO_H = {
## Quotes
nwUnicode.U_QUOT : nwUnicode.H_QUOT,
nwUnicode.U_APOS : nwUnicode.H_APOS,
nwUnicode.U_LAQUO : nwUnicode.H_LAQUO,
nwUnicode.U_RAQUO : nwUnicode.H_RAQUO,
nwUnicode.U_LSQUO : nwUnicode.H_LSQUO,
nwUnicode.U_RSQUO : nwUnicode.H_RSQUO,
nwUnicode.U_SBQUO : nwUnicode.H_SBQUO,
nwUnicode.U_SUQUO : nwUnicode.H_SUQUO,
nwUnicode.U_LDQUO : nwUnicode.H_LDQUO,
nwUnicode.U_RDQUO : nwUnicode.H_RDQUO,
nwUnicode.U_BDQUO : nwUnicode.H_BDQUO,
nwUnicode.U_UDQUO : nwUnicode.H_UDQUO,
nwUnicode.U_LSAQUO : nwUnicode.H_LSAQUO,
nwUnicode.U_RSAQUO : nwUnicode.H_RSAQUO,
nwUnicode.U_BDRQUO : nwUnicode.H_BDRQUO,
nwUnicode.U_LCQUO : nwUnicode.H_LCQUO,
nwUnicode.U_RCQUO : nwUnicode.H_RCQUO,
nwUnicode.U_LWCQUO : nwUnicode.H_LWCQUO,
nwUnicode.U_RWCQUO : nwUnicode.H_RWCQUO,
## Punctuation
nwUnicode.U_FGDASH : nwUnicode.H_FGDASH,
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HBAR : nwUnicode.H_HBAR,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_MAPOSS : nwUnicode.H_MAPOSS,
nwUnicode.U_PRIME : nwUnicode.H_PRIME,
nwUnicode.U_DPRIME : nwUnicode.H_DPRIME,
## Spaces
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
nwUnicode.U_THSP : nwUnicode.H_THSP,
nwUnicode.U_THNBSP : nwUnicode.H_THNBSP,
nwUnicode.U_ENSP : nwUnicode.H_ENSP,
nwUnicode.U_EMSP : nwUnicode.H_EMSP,
## Symbols
nwUnicode.U_CHECK : nwUnicode.H_CHECK,
nwUnicode.U_CROSS : nwUnicode.H_CROSS,
nwUnicode.U_BULL : nwUnicode.H_BULL,
nwUnicode.U_TRBULL : nwUnicode.H_TRBULL,
nwUnicode.U_HYBULL : nwUnicode.H_HYBULL,
nwUnicode.U_FLOWER : nwUnicode.H_FLOWER,
nwUnicode.U_PERMIL : nwUnicode.H_PERMIL,
nwUnicode.U_DEGREE : nwUnicode.H_DEGREE,
nwUnicode.U_MINUS : nwUnicode.H_MINUS,
nwUnicode.U_TIMES : nwUnicode.H_TIMES,
nwUnicode.U_DIVIDE : nwUnicode.H_DIVIDE,
}
# END Class nwHtmlUnicode
+9 -8
View File
@@ -127,13 +127,14 @@ class nwOutline(Enum):
WCOUNT = 5
PCOUNT = 6
POV = 7
CHAR = 8
PLOT = 9
TIME = 10
WORLD = 11
OBJECT = 12
ENTITY = 13
CUSTOM = 14
SYNOP = 15
FOCUS = 8
CHAR = 9
PLOT = 10
TIME = 11
WORLD = 12
OBJECT = 13
ENTITY = 14
CUSTOM = 15
SYNOP = 16
# END Enum nwOutline
+4
View File
@@ -5,6 +5,8 @@ from nw.core.index import NWIndex
from nw.core.project import NWProject
from nw.core.spellcheck import NWSpellCheck, NWSpellEnchant, NWSpellSimple
from nw.core.tohtml import ToHtml
from nw.core.toodt import ToOdt
from nw.core.tomd import ToMarkdown
from nw.core.tools import countWords, numberToRoman, numberToWord
__all__ = [
@@ -18,4 +20,6 @@ __all__ = [
"NWSpellEnchant",
"NWSpellSimple",
"ToHtml",
"ToOdt",
"ToMarkdown",
]
+179 -59
View File
@@ -36,11 +36,13 @@ from nw.constants import (
)
from nw.core.document import NWDoc
from nw.core.tools import countWords
from nw.common import isHandle, isTitleTag, isItemClass, isItemLayout
logger = logging.getLogger(__name__)
class NWIndex():
H_VALID = ("H0", "H1", "H2", "H3", "H4")
H_LEVEL = {"H0": 0, "H1": 1, "H2": 2, "H3": 3, "H4": 4}
def __init__(self, theProject, theParent):
@@ -152,9 +154,14 @@ class NWIndex():
try:
with open(indexFile, mode="r", encoding="utf8") as inFile:
theData = json.load(inFile)
except Exception as e:
except Exception:
logger.error("Failed to load index file")
logger.error(str(e))
nw.logException()
self.indexBroken = True
self.theParent.makeAlert(
"Could not load cached index file. Rebuilding index.",
nwAlert.WARN
)
return False
self._tagIndex = theData.get("tagIndex", {})
@@ -188,9 +195,9 @@ class NWIndex():
"noteIndex" : self._noteIndex,
"textCounts" : self._textCounts,
}, outFile, indent=2)
except Exception as e:
except Exception:
logger.error("Failed to save index file")
logger.error(str(e))
nw.logException()
return False
return True
@@ -200,37 +207,25 @@ class NWIndex():
elements it should.
"""
logger.debug("Checking index")
self.indexBroken = False
tStart = time()
try:
for tTag in self._tagIndex:
if len(self._tagIndex[tTag]) != 4:
self.indexBroken = True
for tHandle in self._refIndex:
for sTitle in self._refIndex[tHandle]:
for tEntry in self._refIndex[tHandle][sTitle]["tags"]:
if len(tEntry) != 3:
self.indexBroken = True
for tHandle in self._novelIndex:
for sLine in self._novelIndex[tHandle]:
if len(self._novelIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True
for tHandle in self._noteIndex:
for sLine in self._noteIndex[tHandle]:
if len(self._noteIndex[tHandle][sLine].keys()) != 8:
self.indexBroken = True
for tHandle in self._textCounts:
if len(self._textCounts[tHandle]) != 3:
self.indexBroken = True
self._checkTagIndex()
self._checkRefIndex()
self._checkNovelNoteIndex("novelIndex")
self._checkNovelNoteIndex("noteIndex")
self._checkTextCounts()
self.indexBroken = False
except Exception:
logger.error("Error while checking index")
nw.logException()
self.indexBroken = True
tEnd = time()
logger.debug("Index check took %.3f ms" % ((tEnd - tStart)*1000))
logger.debug("Index check complete")
if self.indexBroken:
self.clearIndex()
self.theParent.makeAlert(
@@ -317,7 +312,7 @@ class NWIndex():
if nChar == 0:
continue
if aLine.startswith(r"#"):
if aLine.startswith("#"):
isTitle = self._indexTitle(tHandle, isNovel, aLine, nLine, itemLayout)
if isTitle and nLine > 0:
if nTitle > 0:
@@ -325,11 +320,10 @@ class NWIndex():
self._indexWordCounts(tHandle, isNovel, lastText, nTitle)
nTitle = nLine
elif aLine.startswith(r"@"):
self._indexNoteRef(tHandle, aLine, nLine, nTitle)
self._indexTag(tHandle, aLine, nLine, nTitle, itemClass)
elif aLine.startswith("@"):
self._indexKeyword(tHandle, aLine, nLine, nTitle, itemClass)
elif aLine.startswith(r"%"):
elif aLine.startswith("%"):
if nTitle > 0:
toCheck = aLine[1:].lstrip()
synTag = toCheck[:9].lower()
@@ -468,41 +462,28 @@ class NWIndex():
self._noteIndex[tHandle][sTitle]["updated"] = round(time())
return
def _indexNoteRef(self, tHandle, aLine, nLine, nTitle):
def _indexKeyword(self, tHandle, aLine, nLine, nTitle, itemClass):
"""Validate and save the information about a reference to a tag
in another file.
"""
isValid, theBits, _ = self.scanThis(aLine)
if not isValid or len(theBits) == 0:
return False
sTitle = "T%06d" % nTitle
if sTitle not in self._refIndex[tHandle]:
return False
if theBits[0] == nwKeyWords.TAG_KEY:
return False
if not isValid or len(theBits) < 2:
logger.warning("Skipping keyword with %d value(s) in %s" % (len(theBits), tHandle))
return
if theBits[0] not in nwKeyWords.VALID_KEYS:
return False
for aVal in theBits[1:]:
self._refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
return True
def _indexTag(self, tHandle, aLine, nLine, nTitle, itemClass):
"""Validate and save the information from a tag.
"""
isValid, theBits, thePos = self.scanThis(aLine)
if not isValid or len(theBits) != 2:
return False
logger.warning("Skipping invalid keyword '%s' in %s" % (theBits[0], tHandle))
return
sTitle = "T%06d" % nTitle
if theBits[0] == nwKeyWords.TAG_KEY:
sTitle = "T%06d" % nTitle
self._tagIndex[theBits[1]] = [nLine, tHandle, itemClass.name, sTitle]
return True
elif sTitle in self._refIndex[tHandle]:
for aVal in theBits[1:]:
self._refIndex[tHandle][sTitle]["tags"].append([nLine, theBits[0], aVal])
return
##
# Check @ Lines
@@ -722,7 +703,7 @@ class NWIndex():
for refTitle in self._refIndex[tHandle]:
for aTag in self._refIndex[tHandle][refTitle].get("tags", []):
if len(aTag) == 3 and (sTitle is None or sTitle == refTitle):
if aTag[1] in theRefs: # Future-compatible. Check can be removed in 1.2.
if aTag[1] in theRefs:
theRefs[aTag[1]].append(aTag[2])
return theRefs
@@ -784,4 +765,143 @@ class NWIndex():
return theHandles
##
# Index Checkers
##
def _checkTagIndex(self):
"""Scan the tag index for errors.
Waring: This function raises exceptions.
"""
for tTag in self._tagIndex:
if not isinstance(tTag, str):
raise KeyError("tagIndex key is not a string")
tEntry = self._tagIndex[tTag]
if len(tEntry) != 4:
raise IndexError("tagIndex[a] expected 4 values")
if not isinstance(tEntry[0], int):
raise ValueError("tagIndex[a][0] is not an integer")
if not isHandle(tEntry[1]):
raise ValueError("tagIndex[a][1] is not a handle")
if not isItemClass(tEntry[2]):
raise ValueError("tagIndex[a][2] is not an nwItemClass")
if not isTitleTag(tEntry[3]):
raise ValueError("tagIndex[a][3] is not a title tag")
return
def _checkRefIndex(self):
"""Scan the reference index for errors.
Waring: This function raises exceptions.
"""
for tHandle in self._refIndex:
if not isHandle(tHandle):
raise KeyError("refIndex key is not a handle")
hEntry = self._refIndex[tHandle]
for sTitle in hEntry:
if not isTitleTag(sTitle):
raise KeyError("refIndex[a] key is not a title tag")
sEntry = hEntry[sTitle]
if "tags" not in sEntry:
raise KeyError("refIndex[a][b] has no 'tag' key")
for tEntry in sEntry["tags"]:
if len(tEntry) != 3:
raise IndexError("refIndex[a][b][tags][i] expected 3 values")
if not isinstance(tEntry[0], int):
raise ValueError("refIndex[a][b][tags][i][0] is not an integer")
if not tEntry[1] in nwKeyWords.VALID_KEYS:
raise ValueError("refIndex[a][b][tags][i][1] is not a keyword")
if not isinstance(tEntry[2], str):
raise ValueError("refIndex[a][b][tags][i][2] is not a string")
if "updated" not in sEntry:
raise KeyError("refIndex[a][b] has no 'updated' key")
if not isinstance(sEntry["updated"], int):
raise ValueError("%refIndex[a][b][updated] is not an integer")
return
def _checkNovelNoteIndex(self, idxName):
"""Scan the novel or note index for errors.
Waring: This function raises exceptions.
"""
if idxName == "novelIndex":
theIndex = self._novelIndex
elif idxName == "noteIndex":
theIndex = self._noteIndex
else:
raise IndexError("Unknown index %s" % idxName)
for tHandle in theIndex:
if not isHandle(tHandle):
raise KeyError("%s key is not a handle" % idxName)
hEntry = theIndex[tHandle]
for sTitle in theIndex[tHandle]:
if not isTitleTag(sTitle):
raise KeyError("%s[a] key is not a title tag" % idxName)
sEntry = hEntry[sTitle]
if len(sEntry) != 8:
raise IndexError("%s[a][b] expected 8 values" % idxName)
if "level" not in sEntry:
raise KeyError("%s[a][b] has no 'level' key" % idxName)
if "title" not in sEntry:
raise KeyError("%s[a][b] has no 'title' key" % idxName)
if "layout" not in sEntry:
raise KeyError("%s[a][b] has no 'layout' key" % idxName)
if "synopsis" not in sEntry:
raise KeyError("%s[a][b] has no 'synopsis' key" % idxName)
if "cCount" not in sEntry:
raise KeyError("%s[a][b] has no 'cCount' key" % idxName)
if "wCount" not in sEntry:
raise KeyError("%s[a][b] has no 'wCount' key" % idxName)
if "pCount" not in sEntry:
raise KeyError("%s[a][b] has no 'pCount' key" % idxName)
if "updated" not in sEntry:
raise KeyError("%s[a][b] has no 'updated' key" % idxName)
if not sEntry["level"] in self.H_VALID:
raise ValueError("%s[a][b][level] is not a header level" % idxName)
if not isinstance(sEntry["title"], str):
raise ValueError("%s[a][b][title] is not a string" % idxName)
if not isItemLayout(sEntry["layout"]):
raise ValueError("%s[a][b][layout] is not an nwItemLayout" % idxName)
if not isinstance(sEntry["synopsis"], str):
raise ValueError("%s[a][b][synopsis] is not a string" % idxName)
if not isinstance(sEntry["cCount"], int):
raise ValueError("%s[a][b][cCount] is not an integer" % idxName)
if not isinstance(sEntry["wCount"], int):
raise ValueError("%s[a][b][wCount] is not an integer" % idxName)
if not isinstance(sEntry["pCount"], int):
raise ValueError("%s[a][b][pCount] is not an integer" % idxName)
if not isinstance(sEntry["updated"], int):
raise ValueError("%s[a][b][updated] is not an integer" % idxName)
return
def _checkTextCounts(self):
"""Scan the text counts index for errors.
Waring: This function raises exceptions.
"""
for tHandle in self._textCounts:
if not isHandle(tHandle):
raise KeyError("textCounts key is not a handle")
tEntry = self._textCounts[tHandle]
if len(tEntry) != 3:
raise IndexError("textCounts[a] expected 3 values")
if not isinstance(tEntry[0], int):
raise ValueError("textCounts[a][0] is not an integer")
if not isinstance(tEntry[1], int):
raise ValueError("textCounts[a][1] is not an integer")
if not isinstance(tEntry[2], int):
raise ValueError("textCounts[a][2] is not an integer")
return
# END Class NWIndex
+6 -4
View File
@@ -28,8 +28,10 @@ import logging
from lxml import etree
from nw.common import checkInt, isHandle
from nw.constants import nwItemType, nwItemClass, nwItemLayout
from nw.common import (
checkInt, isHandle, isItemClass, isItemLayout, isItemType
)
logger = logging.getLogger(__name__)
@@ -201,7 +203,7 @@ class NWItem():
"""
if isinstance(theType, nwItemType):
self.itemType = theType
elif theType in nwItemType.__members__:
elif isItemType(theType):
self.itemType = nwItemType[theType]
else:
logger.error("Unrecognised item type '%s'" % theType)
@@ -214,7 +216,7 @@ class NWItem():
"""
if isinstance(theClass, nwItemClass):
self.itemClass = theClass
elif theClass in nwItemClass.__members__:
elif isItemClass(theClass):
self.itemClass = nwItemClass[theClass]
else:
logger.error("Unrecognised item class '%s'" % theClass)
@@ -227,7 +229,7 @@ class NWItem():
"""
if isinstance(theLayout, nwItemLayout):
self.itemLayout = theLayout
elif theLayout in nwItemLayout.__members__:
elif isItemLayout(theLayout):
self.itemLayout = nwItemLayout[theLayout]
else:
logger.error("Unrecognised item layout '%s'" % theLayout)
+13 -4
View File
@@ -25,6 +25,7 @@ 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 nw
import logging
import json
import os
@@ -46,6 +47,7 @@ class OptionState():
"widthCol0",
"widthCol1",
"widthCol2",
"widthCol3",
"sortCol",
"sortOrder",
"incNovel",
@@ -53,6 +55,7 @@ class OptionState():
"hideZeros",
"hideNegative",
"groupByDay",
"showIdleTime",
"histMax",
},
"GuiDocSplit": {
@@ -70,12 +73,14 @@ class OptionState():
"excludeBody",
"textFont",
"textSize",
"lineHeight",
"noStyling",
"incSynopsis",
"incComments",
"incKeywords",
"incBodyText",
"replaceTabs",
"replaceUCode",
},
"GuiOutline": {
"headerOrder",
@@ -99,6 +104,10 @@ class OptionState():
"countFrom",
"clearDouble",
},
"GuiWordList": {
"winWidth",
"winHeight",
}
}
return
@@ -121,9 +130,9 @@ class OptionState():
try:
with open(stateFile, mode="r", encoding="utf8") as inFile:
theState = json.load(inFile)
except Exception as e:
except Exception:
logger.error("Failed to load GUI options file")
logger.error(str(e))
nw.logException()
return False
# Filter out unused variables
@@ -148,9 +157,9 @@ class OptionState():
try:
with open(stateFile, mode="w+", encoding="utf8") as outFile:
json.dump(self.theState, outFile, indent=2)
except Exception as e:
except Exception:
logger.error("Failed to save GUI options file")
logger.error(str(e))
nw.logException()
return False
return True
+30 -24
View File
@@ -726,12 +726,12 @@ class NWProject():
return True
def closeProject(self):
def closeProject(self, idleTime=0):
"""Close the current project and clear all meta data.
"""
self.optState.saveSettings()
self.projTree.writeToCFile()
self._appendSessionStats()
self._appendSessionStats(idleTime)
self._clearLockFile()
self.clearProject()
self.lockedBy = None
@@ -1219,9 +1219,9 @@ class NWProject():
if len(theLines) != 4:
return ["ERROR"]
except Exception as e:
except Exception:
logger.error("Failed to read project lockfile")
logger.error(str(e))
nw.logException()
return ["ERROR"]
return theLines
@@ -1240,9 +1240,9 @@ class NWProject():
outFile.write("%s\n" % self.mainConf.kernelVer)
outFile.write("%d\n" % time())
except Exception as e:
except Exception:
logger.error("Failed to write project lockfile")
logger.error(str(e))
nw.logException()
return False
return True
@@ -1257,9 +1257,9 @@ class NWProject():
if os.path.isfile(lockFile):
try:
os.unlink(lockFile)
except Exception as e:
except Exception:
logger.error("Failed to remove project lockfile")
logger.error(str(e))
nw.logException()
return False
return True
@@ -1389,7 +1389,7 @@ class NWProject():
return True
def _appendSessionStats(self):
def _appendSessionStats(self, idleTime):
"""Append session statistics to the sessions log file.
"""
if not self.ensureFolderStructure():
@@ -1413,20 +1413,21 @@ class NWProject():
# It's a new file, so add a header
if self.lastWCount > 0:
outFile.write("# Offset %d\n" % self.lastWCount)
outFile.write("# %-17s %-19s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes"
outFile.write("# %-17s %-19s %8s %8s %8s\n" % (
"Start Time", "End Time", "Novel", "Notes", "Idle"
))
outFile.write("%-19s %-19s %8d %8d\n" % (
outFile.write("%-19s %-19s %8d %8d %8d\n" % (
formatTimeStamp(self.projOpened),
formatTimeStamp(nowTime),
self.novelWCount,
self.notesWCount,
int(idleTime),
))
except Exception as e:
except Exception:
logger.error("Failed to write session stats file")
logger.error(str(e))
nw.logException()
return False
return True
@@ -1462,17 +1463,19 @@ class NWProject():
os.rename(theFile, newPath)
logger.info("Moved file: %s" % theFile)
logger.info("New location: %s" % newPath)
except Exception as e:
logger.error(str(e))
except Exception:
errList.append("Could not move: %s" % theFile)
logger.error("Could not move: %s" % theFile)
nw.logException()
elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
try:
os.unlink(theFile)
logger.info("Deleted file: %s" % theFile)
except Exception as e:
logger.error(str(e))
except Exception:
errList.append("Could not delete: %s" % theFile)
logger.error("Could not delete: %s" % theFile)
nw.logException()
else:
theErr = self._moveUnknownItem(theData, dataItem)
@@ -1484,9 +1487,10 @@ class NWProject():
try:
os.rmdir(theData)
logger.info("Removed folder: %s" % theFolder)
except Exception as e:
logger.error(str(e))
except Exception:
errList.append("Failed to remove: %s" % theFolder)
logger.error("Failed to remove: %s" % theFolder)
nw.logException()
return errList
@@ -1504,8 +1508,9 @@ class NWProject():
try:
os.rename(theSrc, theDst)
logger.info("Moved to junk: %s" % theSrc)
except Exception as e:
logger.error(str(e))
except Exception:
logger.error("Could not move item %s to junk." % theSrc)
nw.logException()
return "Could not move item %s to junk." % theSrc
return ""
@@ -1538,8 +1543,9 @@ class NWProject():
logger.info("Deleting: %s" % rmFile)
try:
os.unlink(rmFile)
except Exception as e:
logger.error(str(e))
except Exception:
logger.error("Could not delete: %s" % rmFile)
nw.logException()
return False
return True
+17 -9
View File
@@ -72,9 +72,9 @@ class NWSpellCheck():
with open(self.projectDict, mode="a+", encoding="utf-8") as outFile:
outFile.write("%s\n" % newWord)
self.projDict.append(newWord)
except Exception as e:
except Exception:
logger.error("Failed to add word to project word list %s" % str(self.projectDict))
logger.error(str(e))
nw.logException()
return False
return True
return False
@@ -123,9 +123,10 @@ class NWSpellCheck():
if len(theLine) > 0 and theLine not in self.projDict:
self.projDict.append(theLine)
logger.debug("Project word list contains %d words" % len(self.projDict))
except Exception as e:
except Exception:
logger.error("Failed to load project word list")
logger.error(str(e))
nw.logException()
return False
return True
@@ -141,6 +142,7 @@ class NWSpellEnchant(NWSpellCheck):
def __init__(self):
NWSpellCheck.__init__(self)
logger.debug("Enchant spell checking activated")
self.theBroker = None
return
def setLanguage(self, theLang, projectDict=None):
@@ -150,9 +152,15 @@ class NWSpellEnchant(NWSpellCheck):
"""
try:
import enchant
self.theDict = enchant.Dict(theLang)
if self.theBroker is not None:
logger.verbose("Deleting old pyenchant broker")
del self.theBroker
self.theBroker = enchant.Broker()
self.theDict = self.theBroker.request_dict(theLang)
self.spellLanguage = theLang
logger.debug("Enchant spell checking for language %s loaded" % theLang)
except Exception:
logger.error("Failed to load enchant spell checking for language %s" % theLang)
self.theDict = NWSpellEnchantDummy()
@@ -201,9 +209,9 @@ class NWSpellEnchant(NWSpellCheck):
try:
spTag = self.theDict.tag
spName = self.theDict.provider.name
except Exception as e:
except Exception:
logger.error("Failed to extract information about the dictionary")
logger.error(str(e))
nw.logException()
spTag = ""
spName = ""
@@ -263,9 +271,9 @@ class NWSpellSimple(NWSpellCheck):
logger.debug("Dictionary contains %d words" % len(self.theWords))
self.spellLanguage = theLang
except Exception as e:
except Exception:
logger.error("Failed to load spell check word list for language %s" % theLang)
logger.error(str(e))
nw.logException()
self.spellLanguage = None
self._readProjectDictionary(projectDict)
+179 -80
View File
@@ -25,10 +25,9 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import logging
import re
from nw.core.tokenizer import Tokenizer
from nw.constants import nwUnicode, nwLabels, nwKeyWords
from nw.constants import nwLabels, nwKeyWords, nwHtmlUnicode
logger = logging.getLogger(__name__)
@@ -41,25 +40,13 @@ class ToHtml(Tokenizer):
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.genMode = self.M_EXPORT
self.genMode = self.M_EXPORT
self.cssStyles = True
self.fullHTML = []
self.repDict = {
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
nwUnicode.U_ENDASH : nwUnicode.H_ENDASH,
nwUnicode.U_EMDASH : nwUnicode.H_EMDASH,
nwUnicode.U_HELLIP : nwUnicode.H_HELLIP,
nwUnicode.U_NBSP : nwUnicode.H_NBSP,
nwUnicode.U_THSP : nwUnicode.H_THSP,
nwUnicode.U_THNBSP : nwUnicode.H_THNBSP,
nwUnicode.U_MAPOSS : nwUnicode.H_RSQUO,
}
self.revDict = {}
self.reReplace = []
self.reReverse = []
self._buildRegEx()
# Internals
self._trMap = {}
self.setReplaceUnicode(False)
return
@@ -86,31 +73,38 @@ class ToHtml(Tokenizer):
self.cssStyles = cssStyles
return
def setReplaceUnicode(self, doReplace):
"""Set the translation map to either minimal or full unicode to
html entities replacement.
"""
# Control characters must always be replaced
self._trMap = str.maketrans({
"<" : "&lt;",
">" : "&gt;",
"&" : "&amp;",
})
if doReplace:
# Extend to all relevant Unicode characters
self._trMap.update(str.maketrans(nwHtmlUnicode.U_TO_H))
return
##
# Class Methods
##
def doAutoReplace(self):
def getFullResultSize(self):
"""Return the size of the full HTML result.
"""
return sum([len(x) for x in self.fullHTML])
def doPreProcessing(self):
"""Extend the auto-replace to also properly encode some unicode
characters into their respective HTML entities.
"""
Tokenizer.doAutoReplace(self)
self.theText = self.reReplace.sub(
lambda x: self.repDict[x.group(0)], self.theText
)
return
def doPostProcessing(self):
"""Reverse the html entities replacement on the markdown text.
Otherwise, all the &something; bits will also be in there.
"""
Tokenizer.doPostProcessing(self)
if self.genMode == self.M_PREVIEW:
# Doesn't matter for preview as we don't use the markdown
return
self.theMarkdown = self.reReverse.sub(
lambda x: self.revDict[x.group(0)], self.theMarkdown
)
Tokenizer.doPreProcessing(self)
self.theText = self.theText.translate(self._trMap)
return
def doConvert(self):
@@ -158,6 +152,7 @@ class ToHtml(Tokenizer):
parStyle = None
tmpResult = []
hasHardBreak = False
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Styles
@@ -165,24 +160,27 @@ class ToHtml(Tokenizer):
if tStyle is not None and self.cssStyles:
if tStyle & self.A_LEFT:
aStyle.append("text-align: left;")
if tStyle & self.A_RIGHT:
elif tStyle & self.A_RIGHT:
aStyle.append("text-align: right;")
if tStyle & self.A_CENTRE:
elif tStyle & self.A_CENTRE:
aStyle.append("text-align: center;")
if tStyle & self.A_JUSTIFY:
elif tStyle & self.A_JUSTIFY:
aStyle.append("text-align: justify;")
if tStyle & self.A_PBB:
aStyle.append("page-break-before: always;")
if tStyle & self.A_PBB_AV:
aStyle.append("page-break-before: avoid;")
if tStyle & self.A_PBB_NO:
aStyle.append("page-break-before: never;")
elif tStyle & self.A_PBB_AUT:
aStyle.append("page-break-before: auto;")
if tStyle & self.A_PBA:
aStyle.append("page-break-after: always;")
if tStyle & self.A_PBA_AV:
aStyle.append("page-break-after: avoid;")
if tStyle & self.A_PBA_NO:
aStyle.append("page-break-after: never;")
elif tStyle & self.A_PBA_AUT:
aStyle.append("page-break-after: auto;")
if tStyle & self.A_Z_BTMMRG:
aStyle.append("margin-bottom: 0;")
if tStyle & self.A_Z_TOPMRG:
aStyle.append("margin-top: 0;")
if len(aStyle) > 0:
hStyle = " style='%s'" % (" ".join(aStyle))
@@ -240,12 +238,12 @@ class ToHtml(Tokenizer):
if parStyle is None:
parStyle = hStyle
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
tTemp = tTemp[:xPos] + htmlTags[xFmt] + tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip()+"<br/>")
thisPar.append(tTemp.rstrip() + "<br/>")
hasHardBreak = True
else:
thisPar.append(tTemp.rstrip()+" ")
thisPar.append(tTemp.rstrip() + " ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append(self._formatSynopsis(tText))
@@ -254,11 +252,60 @@ class ToHtml(Tokenizer):
tmpResult.append(self._formatComments(tText))
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText))
tTemp = "<p%s>%s</p>\n" % (hStyle, self._formatKeywords(tText))
tmpResult.append(tTemp)
self.theResult = "".join(tmpResult)
tmpResult = []
if self.genMode != self.M_PREVIEW:
self.fullHTML.append(self.theResult)
return
def saveHTML5(self, savePath):
"""Save the data to an .html file.
"""
with open(savePath, mode="w", encoding="utf8") as outFile:
theStyle = self.getStyleSheet()
theStyle.append("article {width: 800px; margin: 40px auto;}")
bodyText = "".join(self.fullHTML)
bodyText = bodyText.replace("\t", "&#09;").rstrip()
theHtml = (
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<meta charset='utf-8'>\n"
"<title>{projTitle:s}</title>\n"
"</head>\n"
"<style>\n"
"{htmlStyle:s}\n"
"</style>\n"
"<body>\n"
"<article>\n"
"{bodyText:s}\n"
"</article>\n"
"</body>\n"
"</html>\n"
).format(
projTitle = self.theProject.projName,
htmlStyle = "\n".join(theStyle),
bodyText = bodyText,
)
outFile.write(theHtml)
return
def replaceTabs(self, nSpaces=8, spaceChar="&nbsp;"):
"""Replace tabs with spaces in the html.
"""
htmlText = []
eightSpace = spaceChar*nSpaces
for aLine in self.fullHTML:
htmlText.append(aLine.replace("\t", eightSpace))
self.fullHTML = htmlText
return
def getStyleSheet(self):
@@ -268,22 +315,86 @@ class ToHtml(Tokenizer):
if not self.cssStyles:
return theStyles
if self.doJustify:
theStyles.append(r"p {text-align: justify;}")
else:
theStyles.append(r"p {text-align: left;}")
mScale = self.lineHeight/1.15
textAlign = "justify" if self.doJustify else "left"
theStyles.append(r"h1, h2 {color: rgb(66, 113, 174);}")
theStyles.append(r"h3, h4 {color: rgb(50, 50, 50);}")
theStyles.append(r"h1, h2, h3, h4 {page-break-after: avoid;}")
theStyles.append(r"a {color: rgb(66, 113, 174);}")
theStyles.append(r".title {font-size: 2.5em;}")
theStyles.append(r".tags {color: rgb(245, 135, 31); font-weight: bold;}")
theStyles.append(r".break {text-align: left;}")
theStyles.append(r".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}")
theStyles.append(r".skip {margin-top: 1em; margin-bottom: 1em;}")
theStyles.append(r".synopsis {font-style: italic;}")
theStyles.append(r".comment {font-style: italic; color: rgb(100, 100, 100);}")
theStyles.append("body {font-family: '%s'; font-size: %dpt;}" % (
self.textFont, self.textSize
))
theStyles.append((
"p {"
"text-align: %s; line-height: %d%%; "
"margin-top: %.2fem; margin-bottom: %.2fem;"
"}"
) % (
textAlign,
round(100 * self.lineHeight),
mScale * self.marginText[0],
mScale * self.marginText[1],
))
theStyles.append((
"h1 {"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
mScale * self.marginHead1[0], mScale * self.marginHead1[1]
))
theStyles.append((
"h2 {"
"color: rgb(66, 113, 174); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
mScale * self.marginHead2[0], mScale * self.marginHead2[1]
))
theStyles.append((
"h3 {"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
mScale * self.marginHead3[0], mScale * self.marginHead3[1]
))
theStyles.append((
"h4 {"
"color: rgb(50, 50, 50); "
"page-break-after: avoid; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
mScale * self.marginHead4[0], mScale * self.marginHead4[1]
))
theStyles.append((
".title {"
"font-size: 2.5em; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;"
"}"
) % (
mScale * self.marginTitle[0], mScale * self.marginTitle[1]
))
theStyles.append((
".sep, .skip {"
"text-align: center; "
"margin-top: %.2fem; "
"margin-bottom: %.2fem;}"
) % (
mScale, mScale
))
theStyles.append("a {color: rgb(66, 113, 174);}")
theStyles.append(".tags {color: rgb(245, 135, 31); font-weight: bold;}")
theStyles.append(".break {text-align: left;}")
theStyles.append(".synopsis {font-style: italic;}")
theStyles.append(".comment {font-style: italic; color: rgb(100, 100, 100);}")
return theStyles
@@ -337,18 +448,6 @@ class ToHtml(Tokenizer):
))
retText += ", ".join(refTags)
return "<div>%s</div>\n" % retText
def _buildRegEx(self):
"""Build the regular expressions
"""
self.revDict = dict(map(reversed, self.repDict.items()))
self.reReplace = re.compile(
"|".join([re.escape(k) for k in self.repDict.keys()]), flags=re.DOTALL
)
self.reReverse = re.compile(
"|".join([re.escape(k) for k in self.revDict.keys()]), flags=re.DOTALL
)
return
return retText
# END Class ToHtml
+166 -193
View File
@@ -32,7 +32,7 @@ from PyQt5.QtCore import QRegularExpression
from nw.core.document import NWDoc
from nw.core.tools import numberToWord, numberToRoman
from nw.constants import nwConst, nwItemLayout, nwItemType, nwRegEx
from nw.constants import nwConst, nwUnicode, nwItemLayout, nwItemType, nwRegEx
logger = logging.getLogger(__name__)
@@ -67,11 +67,11 @@ class Tokenizer():
A_CENTRE = 0x0004 # Centred
A_JUSTIFY = 0x0008 # Justified
A_PBB = 0x0010 # Page break before always
A_PBB_AV = 0x0020 # Page break before avoid
A_PBB_NO = 0x0040 # Page break before never
A_PBA = 0x0080 # Page break after always
A_PBA_AV = 0x0100 # Page break after avoid
A_PBA_NO = 0x0200 # Page break after avoid
A_PBB_AUT = 0x0020 # Page break before auto
A_PBA = 0x0040 # Page break after always
A_PBA_AUT = 0x0080 # Page break after auto
A_Z_TOPMRG = 0x0100 # Zero top margin
A_Z_BTMMRG = 0x0200 # Zero bottom margin
def __init__(self, theProject, theParent):
@@ -79,20 +79,36 @@ class Tokenizer():
self.theParent = theParent
# Data Variables
self.theText = None # The raw text to be tokenized
self.theText = "" # The raw text to be tokenized
self.theHandle = None # The handle associated with the text
self.theItem = None # The NWItem associated with the handle
self.theTokens = None # The list of the processed tokens
self.theResult = None # The result text after conversion
self.theMarkdown = None # The result text in novelWriter markdown
self.theTokens = [] # The list of the processed tokens
self.theResult = "" # The result of the last document
self.keepMarkdown = False # Whether to keep the markdown text
self.theMarkdown = [] # The result novelWriter markdown of all documents
# User Settings
self.doBodyText = True # Include body text
self.doSynopsis = False # Also process synopsis comments
self.doComments = False # Also process comments
self.doKeywords = False # Also process keywords like tags and references
self.doJustify = False # Justify text
self.textFont = "Serif" # Output text font
self.textSize = 11 # Output text size
self.textFixed = False # Fixed width text
self.lineHeight = 1.15 # Line height
self.doJustify = False # Justify text
self.doBodyText = True # Include body text
self.doSynopsis = False # Also process synopsis comments
self.doComments = False # Also process comments
self.doKeywords = False # Also process keywords like tags and references
## Title Margins
self.marginTitle = (1.000, 0.500)
self.marginHead1 = (1.000, 0.500)
self.marginHead2 = (0.834, 0.500)
self.marginHead3 = (0.584, 0.500)
self.marginHead4 = (0.584, 0.500)
self.marginText = (0.000, 0.584)
self.marginMeta = (0.000, 0.584)
## Title Formats
self.fmtTitle = "%title%" # Formatting for titles
self.fmtChapter = "%title%" # Formatting for numbered chapters
self.fmtUnNum = "%title%" # Formatting for unnumbered chapters
@@ -153,6 +169,48 @@ class Tokenizer():
self.hideSection = hideSection
return
def setFont(self, textFont, textSize, textFixed=False):
self.textFont = textFont
self.textSize = round(int(textSize))
self.textFixed = textFixed
return
def setLineHeight(self, lineHeight):
self.lineHeight = float(lineHeight)
return
def setJustify(self, doJustify):
self.doJustify = doJustify
return
def setTitleMargins(self, mUpper, mLower):
self.marginTitle = (float(mUpper), float(mLower))
return
def setHead1Margins(self, mUpper, mLower):
self.marginHead1 = (float(mUpper), float(mLower))
return
def setHead2Margins(self, mUpper, mLower):
self.marginHead2 = (float(mUpper), float(mLower))
return
def setHead3Margins(self, mUpper, mLower):
self.marginHead3 = (float(mUpper), float(mLower))
return
def setHead4Margins(self, mUpper, mLower):
self.marginHead4 = (float(mUpper), float(mLower))
return
def setTextMargins(self, mUpper, mLower):
self.marginText = (float(mUpper), float(mLower))
return
def setMetaMargins(self, mUpper, mLower):
self.marginMeta = (float(mUpper), float(mLower))
return
def setLinkHeaders(self, linkHeaders):
self.linkHeaders = linkHeaders
return
@@ -173,8 +231,8 @@ class Tokenizer():
self.doKeywords = doKeywords
return
def setJustify(self, doJustify):
self.doJustify = doJustify
def setKeepMarkdown(self, keepMarkdown):
self.keepMarkdown = keepMarkdown
return
##
@@ -196,7 +254,8 @@ class Tokenizer():
self.theTokens.append((
self.T_TITLE, 0, theTitle, None, self.A_PBB | self.A_CENTRE
))
self.theMarkdown = "# %s\n\n" % theTitle
if self.keepMarkdown:
self.theMarkdown.append("# %s\n\n" % theTitle)
return True
@@ -238,26 +297,10 @@ class Tokenizer():
return True
def getResult(self):
"""Return the result from the conversion.
"""
return self.theResult
def getResultSize(self):
"""Return the size of the result from the conversion.
"""
if self.theResult is None:
return 0
return len(self.theResult)
def getFilteredMarkdown(self):
"""Return the novelWriter markdown after the filters have been applied.
"""
return self.theMarkdown
def doAutoReplace(self):
"""Run through the user's auto-replace dictionary.
def doPreProcessing(self):
"""Reun trough the various replace doctionaries.
"""
# Process the user's auto-replace dictionary
if len(self.theProject.autoReplace) > 0:
repDict = {}
for aKey, aVal in self.theProject.autoReplace.items():
@@ -265,6 +308,10 @@ class Tokenizer():
xRep = re.compile("|".join([re.escape(k) for k in repDict.keys()]), flags=re.DOTALL)
self.theText = xRep.sub(lambda x: repDict[x.group(0)], self.theText)
# Process the character translation map
trDict = {nwUnicode.U_MAPOSS: nwUnicode.U_RSQUO}
self.theText = self.theText.translate(str.maketrans(trDict))
return
def doPostProcessing(self):
@@ -307,7 +354,6 @@ class Tokenizer():
]
self.theTokens = []
self.theMarkdown = ""
tmpMarkdown = []
nLine = 0
for aLine in self.theText.splitlines():
@@ -316,88 +362,61 @@ class Tokenizer():
# Tag lines starting with specific characters
if len(aLine.strip()) == 0:
self.theTokens.append((
self.T_EMPTY,
nLine,
"",
None,
self.A_NONE
self.T_EMPTY, nLine, "", None, self.A_NONE
))
tmpMarkdown.append("\n")
if self.keepMarkdown:
tmpMarkdown.append("\n")
elif aLine[0] == "%":
cLine = aLine[1:].lstrip()
synTag = cLine[:9].lower()
if synTag == "synopsis:":
self.theTokens.append((
self.T_SYNOPSIS,
nLine,
cLine[9:].strip(),
None,
self.A_NONE
self.T_SYNOPSIS, nLine, cLine[9:].strip(), None, self.A_NONE
))
if self.doSynopsis:
if self.doSynopsis and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
self.theTokens.append((
self.T_COMMENT,
nLine,
aLine[1:].strip(),
None,
self.A_NONE
self.T_COMMENT, nLine, aLine[1:].strip(), None, self.A_NONE
))
if self.doComments:
if self.doComments and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[0] == "@":
self.theTokens.append((
self.T_KEYWORD,
nLine,
aLine[1:].strip(),
None,
self.A_NONE
self.T_KEYWORD, nLine, aLine[1:].strip(), None, self.A_NONE
))
if self.doKeywords:
if self.doKeywords and self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:2] == "# ":
self.theTokens.append((
self.T_HEAD1,
nLine,
aLine[2:].strip(),
None,
self.A_NONE
self.T_HEAD1, nLine, aLine[2:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:3] == "## ":
self.theTokens.append((
self.T_HEAD2,
nLine,
aLine[3:].strip(),
None,
self.A_NONE
self.T_HEAD2, nLine, aLine[3:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:4] == "### ":
self.theTokens.append((
self.T_HEAD3,
nLine,
aLine[4:].strip(),
None,
self.A_NONE
self.T_HEAD3, nLine, aLine[4:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
elif aLine[:5] == "#### ":
self.theTokens.append((
self.T_HEAD4,
nLine,
aLine[5:].strip(),
None,
self.A_NONE
self.T_HEAD4, nLine, aLine[5:].strip(), None, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
else:
if not self.doBodyText:
@@ -420,26 +439,44 @@ class Tokenizer():
# sorted by position
fmtPos = sorted(fmtPos, key=itemgetter(0))
self.theTokens.append((
self.T_TEXT,
nLine,
aLine,
fmtPos,
self.A_NONE
self.T_TEXT, nLine, aLine, fmtPos, self.A_NONE
))
tmpMarkdown.append("%s\n" % aLine)
if self.keepMarkdown:
tmpMarkdown.append("%s\n" % aLine)
# Always add an empty line at the end
self.theTokens.append((
self.T_EMPTY,
nLine,
"",
None,
self.A_NONE
self.T_EMPTY, nLine, "", None, self.A_NONE
))
tmpMarkdown.append("\n")
if self.keepMarkdown:
tmpMarkdown.append("\n")
self.theMarkdown = "".join(tmpMarkdown)
tmpMarkdown = []
if self.keepMarkdown:
self.theMarkdown.append("".join(tmpMarkdown))
# Second Pass
# ===========
# Some items need a second pass
pToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
nToken = (self.T_EMPTY, 0, "", None, self.A_NONE)
tCount = len(self.theTokens)
for n, tToken in enumerate(self.theTokens):
if n > 0:
pToken = self.theTokens[n-1]
if n < tCount - 1:
nToken = self.theTokens[n+1]
if tToken[0] == self.T_KEYWORD:
aStyle = tToken[4]
if pToken[0] == self.T_KEYWORD:
aStyle |= self.A_Z_TOPMRG
if nToken[0] == self.T_KEYWORD:
aStyle |= self.A_Z_BTMMRG
self.theTokens[n] = (
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
return
@@ -454,9 +491,7 @@ class Tokenizer():
# For novel files, we need to handle chapter numbering, scene
# numbering, and scene breaks
if self.isNovel:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
for n, tToken in enumerate(self.theTokens):
# In case we see text before a scene, we reset the flag
if tToken[0] == self.T_TEXT:
@@ -468,11 +503,7 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtTitle, tToken[2])
self.theTokens[n] = (
tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
elif tToken[0] == self.T_HEAD2:
@@ -490,11 +521,7 @@ class Tokenizer():
# Format the chapter header
self.theTokens[n] = (
tToken[0],
tToken[1],
tTemp,
None,
self.A_PBB
tToken[0], tToken[1], tTemp, None, self.A_PBB
)
# Set scene variables
@@ -511,53 +538,29 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtScene, tToken[2])
if tTemp == "" and self.hideScene:
self.theTokens[n] = (
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SKIP,
tToken[1],
"",
None,
self.A_NONE
self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
else:
self.theTokens[n] = (
self.T_SEP,
tToken[1],
tTemp,
None,
self.A_CENTRE
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# Definitely no longer the first scene
@@ -570,35 +573,19 @@ class Tokenizer():
tTemp = self._formatHeading(self.fmtSection, tToken[2])
if tTemp == "" and self.hideSection:
self.theTokens[n] = (
self.T_EMPTY,
tToken[1],
"",
None,
self.A_NONE
self.T_EMPTY, tToken[1], "", None, self.A_NONE
)
elif tTemp == "" and not self.hideSection:
self.theTokens[n] = (
self.T_SKIP,
tToken[1],
"",
None,
self.A_NONE
self.T_SKIP, tToken[1], "", None, self.A_NONE
)
elif tTemp == self.fmtSection:
self.theTokens[n] = (
self.T_SEP,
tToken[1],
tTemp,
None,
self.A_CENTRE
self.T_SEP, tToken[1], tTemp, None, self.A_CENTRE
)
else:
self.theTokens[n] = (
tToken[0],
tToken[1],
tTemp,
None,
self.A_NONE
tToken[0], tToken[1], tTemp, None, self.A_NONE
)
# For title page and partitions, we need to centre all text.
@@ -609,28 +596,18 @@ class Tokenizer():
for n, tToken in enumerate(self.theTokens):
if tToken[0] == self.T_HEAD1:
if self.isTitle:
aStyle = self.A_PBB_AUT | self.A_CENTRE
self.theTokens[n] = (
self.T_TITLE,
tToken[1],
tToken[2],
tToken[3],
self.A_PBB_NO | self.A_CENTRE
self.T_TITLE, tToken[1], tToken[2], tToken[3], aStyle
)
else:
aStyle = self.A_PBB | self.A_CENTRE
self.theTokens[n] = (
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_PBB | self.A_CENTRE
tToken[0], tToken[1], tToken[2], tToken[3], aStyle
)
else:
self.theTokens[n] = (
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_CENTRE
tToken[0], tToken[1], tToken[2], tToken[3], self.A_CENTRE
)
# Add a page break after the last entry
@@ -638,11 +615,7 @@ class Tokenizer():
if n >= 0:
tToken = self.theTokens[n]
self.theTokens[n] = (
tToken[0],
tToken[1],
tToken[2],
tToken[3],
tToken[4] | self.A_PBA
tToken[0], tToken[1], tToken[2], tToken[3], tToken[4] | self.A_PBA
)
# A single page is always left-aligned and starts on a fresh
@@ -651,23 +624,23 @@ class Tokenizer():
for n, tToken in enumerate(self.theTokens):
if n == 0:
self.theTokens[n] = (
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_LEFT | self.A_PBB
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT | self.A_PBB
)
else:
self.theTokens[n] = (
tToken[0],
tToken[1],
tToken[2],
tToken[3],
self.A_LEFT
tToken[0], tToken[1], tToken[2], tToken[3], self.A_LEFT
)
return True
def saveRawMarkdown(self, savePath):
"""Save the data to a plain text file.
"""
with open(savePath, mode="w", encoding="utf8") as outFile:
for nwdPage in self.theMarkdown:
outFile.write(nwdPage)
return
##
# Internal Functions
##
+197
View File
@@ -0,0 +1,197 @@
# -*- coding: utf-8 -*-
"""
novelWriter Markdown Text Converter
=====================================
Extends the Tokenizer class to generate Makrdown output
File History:
Created: 2021-02-06 [1.2a0]
This file is a part of novelWriter
Copyright 20182021, 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 logging
from nw.core.tokenizer import Tokenizer
from nw.constants import nwLabels
logger = logging.getLogger(__name__)
class ToMarkdown(Tokenizer):
M_STD = 0 # Standard Markdown
M_GH = 1 # GitHub Markdown
def __init__(self, theProject, theParent):
Tokenizer.__init__(self, theProject, theParent)
self.genMode = self.M_STD
self.fullMD = []
return
##
# Setters
##
def setStandardMarkdown(self):
self.genMode = self.M_STD
return
def setGitHubMarkdown(self):
self.genMode = self.M_GH
return
##
# Class Methods
##
def getFullResultSize(self):
"""Return the size of the full Markdown result.
"""
return sum([len(x) for x in self.fullMD])
def doConvert(self):
"""Convert the list of text tokens into a HTML document saved
to theResult.
"""
if self.genMode == self.M_STD:
# Standard
mdTags = {
self.FMT_B_B : "**", self.FMT_B_E : "**",
self.FMT_I_B : "_", self.FMT_I_E : "_",
self.FMT_D_B : "", self.FMT_D_E : "",
}
else:
# GitHub and novelWriter
mdTags = {
self.FMT_B_B : "**", self.FMT_B_E : "**",
self.FMT_I_B : "_", self.FMT_I_E : "_",
self.FMT_D_B : "~~", self.FMT_D_E : "~~",
}
self.theResult = ""
thisPar = []
tmpResult = []
for tType, tLine, tText, tFormat, tStyle in self.theTokens:
# Process Text Type
if tType == self.T_EMPTY:
if len(thisPar) > 0:
tTemp = "".join(thisPar)
tmpResult.append("%s\n\n" % tTemp.rstrip(" "))
thisPar = []
elif tType == self.T_TITLE:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
elif tType == self.T_HEAD1:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("# %s\n\n" % tHead)
elif tType == self.T_HEAD2:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("## %s\n\n" % tHead)
elif tType == self.T_HEAD3:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("### %s\n\n" % tHead)
elif tType == self.T_HEAD4:
tHead = tText.replace(r"\\", "\n")
tmpResult.append("#### %s\n\n" % tHead)
elif tType == self.T_SEP:
tmpResult.append("%s\n\n" % tText)
elif tType == self.T_SKIP:
tmpResult.append("\n\n\n")
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos] + mdTags[xFmt] + tTemp[xPos+xLen:]
if tText.endswith(" "):
thisPar.append(tTemp.rstrip() + " \n")
else:
thisPar.append(tTemp.rstrip() + " ")
elif tType == self.T_SYNOPSIS and self.doSynopsis:
tmpResult.append("**Synopsis:** %s\n\n" % tText)
elif tType == self.T_COMMENT and self.doComments:
tmpResult.append("**Comment:** %s\n\n" % tText)
elif tType == self.T_KEYWORD and self.doKeywords:
tmpResult.append(self._formatKeywords(tText, tStyle))
self.theResult = "".join(tmpResult)
tmpResult = []
self.fullMD.append(self.theResult)
return
def saveMarkdown(self, savePath):
"""Save the data to a plain text file.
"""
with open(savePath, mode="w", encoding="utf8") as outFile:
theText = "".join(self.fullMD)
outFile.write(theText)
return
def replaceTabs(self, nSpaces=8, spaceChar=" "):
"""Replace tabs with spaces.
"""
fullMD = []
eightSpace = spaceChar*nSpaces
for aPage in self.fullMD:
fullMD.append(aPage.replace("\t", eightSpace))
self.fullMD = fullMD
return
##
# Internal Functions
##
def _formatKeywords(self, tText, tStyle):
"""Apply Markdown formatting to keywords.
"""
isValid, theBits, thePos = self.theParent.theIndex.scanThis("@"+tText)
if not isValid or not theBits:
return ""
retText = ""
if theBits[0] in nwLabels.KEY_NAME:
retText += "**%s:** " % nwLabels.KEY_NAME[theBits[0]]
if len(theBits) > 1:
retText += ", ".join(theBits[1:])
if tStyle & self.A_Z_BTMMRG:
retText += " \n"
else:
retText += "\n\n"
return retText
# END Class ToMarkdown
+1285
View File
File diff suppressed because it is too large Load Diff
+53 -3
View File
@@ -24,6 +24,7 @@ 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 nw
import logging
import os
@@ -33,10 +34,35 @@ from time import time
from nw.core.item import NWItem
from nw.common import checkHandle
from nw.constants import nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst
from nw.constants import (
nwFiles, nwItemType, nwItemClass, nwItemLayout, nwConst, nwLists
)
logger = logging.getLogger(__name__)
# Layout Translation Map
LAYOUT_MAP = {
nwItemLayout.SCENE: {
"H1": nwItemLayout.BOOK,
"H2": nwItemLayout.CHAPTER,
},
nwItemLayout.CHAPTER: {
"H1": nwItemLayout.BOOK,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
nwItemLayout.UNNUMBERED: {
"H1": nwItemLayout.BOOK,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
nwItemLayout.PARTITION: {
"H2": nwItemLayout.CHAPTER,
"H3": nwItemLayout.SCENE,
"H4": nwItemLayout.SCENE,
},
}
class NWTree():
def __init__(self, theProject):
@@ -179,8 +205,9 @@ class NWTree():
outFile.write("\n".join(tocList))
outFile.write("\n")
except Exception as e:
logger.error(str(e))
except Exception:
logger.error("Could not write ToC file")
nw.logException()
return False
return True
@@ -202,6 +229,29 @@ class NWTree():
novelWords += tItem.wordCount
return novelWords, noteWords
def updateItemLayout(self, tHandle, hLevel):
"""Check if the item layout needs updating based on the header
given level.
"""
tItem = self.__getitem__(tHandle)
if tItem is None:
return False
if tItem.itemClass not in nwLists.CLS_NOVEL:
return False
if hLevel not in ("H1", "H2", "H3", "H4"):
return False
iLayout = tItem.itemLayout
if iLayout in LAYOUT_MAP:
if hLevel in LAYOUT_MAP[iLayout]:
tItem.itemLayout = LAYOUT_MAP[iLayout][hLevel]
logger.debug("Changed layout for %s from %s to %s" % (
tHandle, iLayout.name, tItem.itemLayout.name
))
return True
return False
##
# Tree Structure Methods
##
+19 -4
View File
@@ -24,12 +24,31 @@ 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 logging
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
qApp, QDialog, QGridLayout, QStyle, QPlainTextEdit, QLabel,
QDialogButtonBox
)
logger = logging.getLogger(__name__)
# =============================================================================================== #
# Utility Functions
# =============================================================================================== #
def logException():
"""Log the content of an exception message.
"""
exType, exValue, _ = sys.exc_info()
logger.error("%s: %s" % (exType.__name__, str(exValue).strip("'")))
# =============================================================================================== #
# Error Handler
# =============================================================================================== #
class NWErrorMessage(QDialog):
def __init__(self, parent):
@@ -72,7 +91,6 @@ class NWErrorMessage(QDialog):
"""Generate a message and append session data, error info and
error traceback.
"""
import sys
from traceback import format_tb
from nw import __issuesurl__, __version__
from PyQt5.Qt import PYQT_VERSION_STR
@@ -133,15 +151,12 @@ class NWErrorMessage(QDialog):
# END Class NWErrorMessage
def exceptionHandler(exType, exValue, exTrace):
"""Function to catch unhandled global exceptions.
"""
import logging
from traceback import print_tb
from PyQt5.QtWidgets import qApp
logger = logging.getLogger(__name__)
logger.critical("%s: %s" % (exType.__name__, str(exValue)))
print_tb(exTrace)
+2
View File
@@ -20,6 +20,7 @@ from nw.gui.projtree import GuiProjectTree
from nw.gui.projwizard import GuiProjectWizard
from nw.gui.statusbar import GuiMainStatus
from nw.gui.theme import GuiTheme
from nw.gui.wordlist import GuiWordList
from nw.gui.writingstats import GuiWritingStats
__all__ = [
@@ -44,5 +45,6 @@ __all__ = [
"GuiProjectTree",
"GuiProjectWizard",
"GuiTheme",
"GuiWordList",
"GuiWritingStats",
]
+462 -374
View File
File diff suppressed because it is too large Load Diff
+34 -12
View File
@@ -54,7 +54,7 @@ from nw.gui.dochighlight import GuiDocHighlighter
from nw.common import transferCase
from nw.constants import (
nwConst, nwAlert, nwUnicode, nwDocAction, nwDocInsert, nwItemClass,
nwKeyWords
nwKeyWords, nwLabels
)
logger = logging.getLogger(__name__)
@@ -91,6 +91,7 @@ class GuiDocEditor(QTextEdit):
self.wordCount = 0 # Word count
self.paraCount = 0 # Paragraph count
self.lastEdit = 0 # Time stamp of last edit
self.lastActive = 0 # Time stamp of last activity
self.lastFind = None # Position of the last found search word
self.bigDoc = False # Flag for very large document size
self.doReplace = False # Switch to temporarily disable auto-replace
@@ -169,15 +170,16 @@ class GuiDocEditor(QTextEdit):
self.clear()
self.wcTimer.stop()
self.theHandle = None
self.charCount = 0
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.queuePos = None
self.theHandle = None
self.charCount = 0
self.wordCount = 0
self.paraCount = 0
self.lastEdit = 0
self.lastActive = 0
self.lastFind = None
self.bigDoc = False
self.doReplace = False
self.queuePos = None
self.setDocumentChanged(False)
self.docHeader.setTitleFromHandle(self.theHandle)
@@ -319,6 +321,7 @@ class GuiDocEditor(QTextEdit):
logger.debug("Document highlighted in %.3f ms" % (1000*(afTime-bfTime)))
self.lastEdit = time()
self.lastActive = time()
self._runCounter()
self.wcTimer.start()
self.theHandle = tHandle
@@ -415,11 +418,21 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(False)
self.theIndex.scanText(tHandle, docText)
if self._updateHeaders(checkLevel=True):
self.theParent.requestNovelTreeRefresh()
else:
self.theParent.novelView.updateWordCounts(tHandle)
hLevel = "H0"
if self.theHeaders:
hLevel = self.theHeaders[0][1]
if self.theProject.projTree.updateItemLayout(tHandle, hLevel):
self.theParent.treeView.setTreeItemValues(tHandle)
self.nwDocument.saveDocument(docText)
self.docFooter.updateInfo()
return True
def updateDocMargins(self):
@@ -647,6 +660,10 @@ class GuiDocEditor(QTextEdit):
this class when calling these actions from other classes.
"""
logger.verbose("Requesting action: %s" % theAction.name)
if not self.hasFocus():
logger.verbose("Editor does not have focus")
return False
if self.theHandle is None:
logger.error("No document open")
return False
@@ -708,6 +725,7 @@ class GuiDocEditor(QTextEdit):
return False
self._allowAutoReplace(True)
self.lastActive = time()
return True
@@ -825,6 +843,7 @@ class GuiDocEditor(QTextEdit):
* The undo/redo/select all sequences bypasses the docAction
pathway from the menu, so we redirect them back from here.
"""
self.lastActive = time()
isReturn = keyEvent.key() == Qt.Key_Return
isReturn |= keyEvent.key() == Qt.Key_Enter
if isReturn and self.docSearch.anyFocus():
@@ -1069,7 +1088,7 @@ class GuiDocEditor(QTextEdit):
logger.verbose("Word counter is busy")
return
if time() - self.lastEdit < 5*self.wcInterval:
if time() - self.lastEdit < 5 * self.wcInterval:
logger.verbose("Running word counter")
self.theParent.threadPool.start(self.wCounter)
@@ -2511,8 +2530,11 @@ class GuiDocEditFooter(QWidget):
else:
iStatus = self.theProject.importItems.checkEntry(iStatus)
theIcon = self.theParent.importIcons[iStatus]
sIcon = theIcon.pixmap(self.sPx, self.sPx)
sText = self.theItem.itemStatus
sClass = nwLabels.CLASS_NAME[self.theItem.itemClass]
sLayout = nwLabels.LAYOUT_NAME[self.theItem.itemLayout]
sText = f"{self.theItem.itemStatus} / {sClass} / {sLayout}"
self.statusIcon.setPixmap(sIcon)
self.statusText.setText(sText)
+3 -3
View File
@@ -176,13 +176,13 @@ class GuiDocViewer(QTextBrowser):
# See issue #298
try:
aDoc.setText(tHandle)
aDoc.doAutoReplace()
aDoc.doPreProcessing()
aDoc.tokenizeText()
aDoc.doConvert()
aDoc.doPostProcessing()
except Exception as e:
except Exception:
logger.error("Failed to generate preview for document with handle '%s'" % tHandle)
logger.error(str(e))
nw.logException()
self.setText("An error occurred while generating the preview.")
return False
+45 -38
View File
@@ -115,22 +115,25 @@ class GuiMainMenu(QMenuBar):
##
def setSpellCheck(self, theMode):
"""Set the spell check check box to theMode. This is controlled
by the document editor class, which holds the master spell check
flag.
"""Forward spell check check state to its action.
"""
self.aSpellCheck.setChecked(theMode)
return
def setAutoOutline(self, theMode):
"""Set the auto outline check box to theMode. Used during
initialisation.
"""Forward auto outline check state to its action.
"""
self.aAutoOutline.setChecked(theMode)
return
def setFocusMode(self, theMode):
"""Forward focus mode check state to its action.
"""
self.aFocusMode.setChecked(theMode)
return
##
# Menu Action
# Slots
##
def _toggleSpellCheck(self, isChecked=False):
@@ -164,17 +167,11 @@ class GuiMainMenu(QMenuBar):
return True
def _openWebsite(self, theUrl):
"""Open an URL in the system's default browser.
"""Open a URL in the system's default browser.
"""
QDesktopServices.openUrl(QUrl(theUrl))
return True
def _openIssue(self):
"""Open the issue tracker URL in the system's default browser.
"""
QDesktopServices.openUrl(QUrl(nw.__issuesurl__))
return True
##
# Menu Builders
##
@@ -263,19 +260,40 @@ class GuiMainMenu(QMenuBar):
self.projMenu.addSeparator()
# Project > Edit
self.aEditItem = QAction("Edit Project Item", self)
self.aEditItem.setStatusTip("Change item settings")
self.aEditItem = QAction("Edit Item", self)
self.aEditItem.setStatusTip("Change project item settings")
self.aEditItem.setShortcuts(["Ctrl+E", "F2"])
self.aEditItem.triggered.connect(lambda: self.theParent.editItem(None))
self.projMenu.addAction(self.aEditItem)
# Project > Delete
self.aDeleteItem = QAction("Delete Project Item", self)
self.aDeleteItem.setStatusTip("Delete selected item")
self.aDeleteItem = QAction("Delete Item", self)
self.aDeleteItem.setStatusTip("Delete selected project item")
self.aDeleteItem.setShortcut("Ctrl+Shift+Del")
self.aDeleteItem.triggered.connect(lambda: self.theParent.treeView.deleteItem(None))
self.projMenu.addAction(self.aDeleteItem)
# Project > Move Up
self.aMoveUp = QAction("Move Item Up", self)
self.aMoveUp.setStatusTip("Move project item up")
self.aMoveUp.setShortcut("Ctrl+Up")
self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1))
self.projMenu.addAction(self.aMoveUp)
# Project > Move Down
self.aMoveDown = QAction("Move Item Down", self)
self.aMoveDown.setStatusTip("Move project item down")
self.aMoveDown.setShortcut("Ctrl+Down")
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
self.projMenu.addAction(self.aMoveDown)
# Project > Undo Last Action
self.aMoveUndo = QAction("Undo Last Move", self)
self.aMoveUndo.setStatusTip("Undo last item move")
self.aMoveUndo.setShortcut("Ctrl+Shift+Z")
self.aMoveUndo.triggered.connect(lambda: self.theParent.treeView.undoLastMove())
self.projMenu.addAction(self.aMoveUndo)
# Project > Empty Trash
self.aEmptyTrash = QAction("Empty Trash", self)
self.aEmptyTrash.setStatusTip("Permanently delete all files in the Trash folder")
@@ -710,6 +728,7 @@ class GuiMainMenu(QMenuBar):
self.mInsKWItems = {}
self.mInsKWItems[nwKeyWords.TAG_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, G")
self.mInsKWItems[nwKeyWords.POV_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, V")
self.mInsKWItems[nwKeyWords.FOCUS_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, F")
self.mInsKWItems[nwKeyWords.CHAR_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, C")
self.mInsKWItems[nwKeyWords.PLOT_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, P")
self.mInsKWItems[nwKeyWords.TIME_KEY] = (QAction(self.mInsKeywords), "Ctrl+K, T")
@@ -891,40 +910,28 @@ class GuiMainMenu(QMenuBar):
# Tools
self.toolsMenu = self.addMenu("&Tools")
# Tools > Move Up
self.aMoveUp = QAction("Move Tree Item Up", self)
self.aMoveUp.setStatusTip("Move item up")
self.aMoveUp.setShortcut("Ctrl+Shift+Up")
self.aMoveUp.triggered.connect(lambda: self._moveTreeItem(-1))
self.toolsMenu.addAction(self.aMoveUp)
# Tools > Move Down
self.aMoveDown = QAction("Move Tree Item Down", self)
self.aMoveDown.setStatusTip("Move item down")
self.aMoveDown.setShortcut("Ctrl+Shift+Down")
self.aMoveDown.triggered.connect(lambda: self._moveTreeItem(1))
self.toolsMenu.addAction(self.aMoveDown)
# Tools > Separator
self.toolsMenu.addSeparator()
# Tools > Toggle Spell Check
# Tools > Check Spelling
self.aSpellCheck = QAction("Check Spelling", self)
self.aSpellCheck.setStatusTip("Toggle check spelling")
self.aSpellCheck.setCheckable(True)
self.aSpellCheck.setChecked(self.theProject.spellCheck)
# Here we must used triggered, not toggled, to avoid recursion
self.aSpellCheck.triggered.connect(self._toggleSpellCheck)
self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled!
self.aSpellCheck.setShortcut("Ctrl+F7")
self.toolsMenu.addAction(self.aSpellCheck)
# Tools > Update Spell Check
# Tools > Re-Run Spell Check
self.aReRunSpell = QAction("Re-Run Spell Check", self)
self.aReRunSpell.setStatusTip("Run the spell checker on current document")
self.aReRunSpell.setShortcut("F7")
self.aReRunSpell.triggered.connect(lambda: self.theParent.docEditor.spellCheckDocument())
self.toolsMenu.addAction(self.aReRunSpell)
# Tools > Project Word List
self.aEditWordList = QAction("Project Word List", self)
self.aEditWordList.setStatusTip("Edit the project's word list")
self.aEditWordList.triggered.connect(lambda: self.theParent.showProjectWordListDialog())
self.toolsMenu.addAction(self.aEditWordList)
# Tools > Separator
self.toolsMenu.addSeparator()
+3
View File
@@ -49,6 +49,7 @@ class GuiOutline(QTreeWidget):
nwOutline.WCOUNT : 50,
nwOutline.PCOUNT : 50,
nwOutline.POV : 100,
nwOutline.FOCUS : 100,
nwOutline.CHAR : 100,
nwOutline.PLOT : 100,
nwOutline.TIME : 100,
@@ -68,6 +69,7 @@ class GuiOutline(QTreeWidget):
nwOutline.WCOUNT : False,
nwOutline.PCOUNT : False,
nwOutline.POV : False,
nwOutline.FOCUS : True,
nwOutline.CHAR : False,
nwOutline.PLOT : False,
nwOutline.TIME : True,
@@ -451,6 +453,7 @@ class GuiOutline(QTreeWidget):
theRefs = self.theIndex.getReferences(tHandle, sTitle)
newItem.setText(self.colIndex[nwOutline.POV], ", ".join(theRefs[nwKeyWords.POV_KEY]))
newItem.setText(self.colIndex[nwOutline.FOCUS], ", ".join(theRefs[nwKeyWords.FOCUS_KEY]))
newItem.setText(self.colIndex[nwOutline.CHAR], ", ".join(theRefs[nwKeyWords.CHAR_KEY]))
newItem.setText(self.colIndex[nwOutline.PLOT], ", ".join(theRefs[nwKeyWords.PLOT_KEY]))
newItem.setText(self.colIndex[nwOutline.TIME], ", ".join(theRefs[nwKeyWords.TIME_KEY]))
+25 -15
View File
@@ -105,6 +105,7 @@ class GuiOutlineDetails(QScrollArea):
# Tags
self.povKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.POV_KEY])
self.focKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.FOCUS_KEY])
self.chrKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CHAR_KEY])
self.pltKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.PLOT_KEY])
self.timKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.TIME_KEY])
@@ -114,6 +115,7 @@ class GuiOutlineDetails(QScrollArea):
self.cstKeyLabel = QLabel("<b>%s</b>" % nwLabels.KEY_NAME[nwKeyWords.CUSTOM_KEY])
self.povKeyLWrap = QHBoxLayout()
self.focKeyLWrap = QHBoxLayout()
self.chrKeyLWrap = QHBoxLayout()
self.pltKeyLWrap = QHBoxLayout()
self.timKeyLWrap = QHBoxLayout()
@@ -123,6 +125,7 @@ class GuiOutlineDetails(QScrollArea):
self.cstKeyLWrap = QHBoxLayout()
self.povKeyValue = QLabel("")
self.focKeyValue = QLabel("")
self.chrKeyValue = QLabel("")
self.pltKeyValue = QLabel("")
self.timKeyValue = QLabel("")
@@ -132,6 +135,7 @@ class GuiOutlineDetails(QScrollArea):
self.cstKeyValue = QLabel("")
self.povKeyValue.setWordWrap(True)
self.focKeyValue.setWordWrap(True)
self.chrKeyValue.setWordWrap(True)
self.pltKeyValue.setWordWrap(True)
self.timKeyValue.setWordWrap(True)
@@ -141,6 +145,7 @@ class GuiOutlineDetails(QScrollArea):
self.cstKeyValue.setWordWrap(True)
self.povKeyValue.linkActivated.connect(self._tagClicked)
self.focKeyValue.linkActivated.connect(self._tagClicked)
self.chrKeyValue.linkActivated.connect(self._tagClicked)
self.pltKeyValue.linkActivated.connect(self._tagClicked)
self.timKeyValue.linkActivated.connect(self._tagClicked)
@@ -150,6 +155,7 @@ class GuiOutlineDetails(QScrollArea):
self.cstKeyValue.linkActivated.connect(self._tagClicked)
self.povKeyLWrap.addWidget(self.povKeyValue, 1)
self.focKeyLWrap.addWidget(self.focKeyValue, 1)
self.chrKeyLWrap.addWidget(self.chrKeyValue, 1)
self.pltKeyLWrap.addWidget(self.pltKeyValue, 1)
self.timKeyLWrap.addWidget(self.timKeyValue, 1)
@@ -190,23 +196,25 @@ class GuiOutlineDetails(QScrollArea):
self.tagsForm.addWidget(self.povKeyLabel, 0, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.povKeyLWrap, 0, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.focKeyLabel, 1, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.focKeyLWrap, 1, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.chrKeyLabel, 2, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.chrKeyLWrap, 2, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.pltKeyLabel, 3, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.pltKeyLWrap, 3, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.timKeyLabel, 4, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.timKeyLWrap, 4, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.wldKeyLabel, 5, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.wldKeyLWrap, 5, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.objKeyLabel, 6, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.objKeyLWrap, 6, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.entKeyLabel, 7, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.entKeyLWrap, 7, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addWidget(self.cstKeyLabel, 8, 0, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.addLayout(self.cstKeyLWrap, 8, 1, 1, 1, Qt.AlignTop | Qt.AlignLeft)
self.tagsForm.setColumnStretch(1, 1)
self.tagsForm.setRowStretch(7, 1)
self.tagsForm.setRowStretch(8, 1)
self.tagsForm.setHorizontalSpacing(hSpace)
self.tagsForm.setVerticalSpacing(vSpace)
@@ -257,6 +265,7 @@ class GuiOutlineDetails(QScrollArea):
self.pCValue.setText("")
self.synopValue.setText("")
self.povKeyValue.setText("")
self.focKeyValue.setText("")
self.chrKeyValue.setText("")
self.pltKeyValue.setText("")
self.timKeyValue.setText("")
@@ -296,6 +305,7 @@ class GuiOutlineDetails(QScrollArea):
self.synopValue.setText(novIdx["synopsis"])
self.povKeyValue.setText(self._formatTags(theRefs, nwKeyWords.POV_KEY))
self.focKeyValue.setText(self._formatTags(theRefs, nwKeyWords.FOCUS_KEY))
self.chrKeyValue.setText(self._formatTags(theRefs, nwKeyWords.CHAR_KEY))
self.pltKeyValue.setText(self._formatTags(theRefs, nwKeyWords.PLOT_KEY))
self.timKeyValue.setText(self._formatTags(theRefs, nwKeyWords.TIME_KEY))
+31
View File
@@ -358,6 +358,33 @@ class GuiPreferencesProjects(QWidget):
"If off, backups will run in the background."
)
# Session Timer
# =============
self.mainForm.addGroupLabel("Session Timer")
## Pause when idle
self.stopWhenIdle = QSwitch()
self.stopWhenIdle.setChecked(self.mainConf.stopWhenIdle)
self.mainForm.addRow(
"Pause the session timer when not writing",
self.stopWhenIdle,
"Also pauses when the application window does not have focus."
)
## Inactive time for idle
self.userIdleTime = QDoubleSpinBox()
self.userIdleTime.setMinimum(0.5)
self.userIdleTime.setMaximum(600.0)
self.userIdleTime.setSingleStep(0.5)
self.userIdleTime.setDecimals(1)
self.userIdleTime.setValue(self.mainConf.userIdleTime/60.0)
self.mainForm.addRow(
"Editor inactive time before pausing timer",
self.userIdleTime,
"User activity includes typing and changing the content.",
theUnit="minutes"
)
return
def saveValues(self):
@@ -372,6 +399,10 @@ class GuiPreferencesProjects(QWidget):
self.mainConf.backupOnClose = self.backupOnClose.isChecked()
self.mainConf.askBeforeBackup = self.askBeforeBackup.isChecked()
# Session Timer
self.mainConf.stopWhenIdle = self.stopWhenIdle.isChecked()
self.mainConf.userIdleTime = round(self.userIdleTime.value() * 60)
self.mainConf.confChanged = True
return
+173 -51
View File
@@ -33,12 +33,12 @@ from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal
from PyQt5.QtGui import QIcon
from PyQt5.QtWidgets import (
qApp, QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
QTreeWidget, QTreeWidgetItem, QAbstractItemView, QMenu, QAction
)
from nw.core import NWDoc
from nw.constants import (
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst
nwLabels, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwConst, nwLists
)
logger = logging.getLogger(__name__)
@@ -68,6 +68,7 @@ class GuiProjectTree(QTreeWidget):
self._treeMap = {}
self._treeChanged = False
self._timeChanged = 0
self._lastMove = {}
##
# Build GUI
@@ -269,10 +270,43 @@ class GuiProjectTree(QTreeWidget):
logger.error("Failed to add new item")
return False
# If there is no handle set, return here
if tHandle is None:
return True
# Add the new item to the tree
if tHandle is not None:
self.revealNewTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle)
self.revealNewTreeItem(tHandle, nHandle)
self.theParent.editItem(tHandle)
nwItem = self.theProject.projTree[tHandle]
# If this is a folder, return here
if nwItem.itemType != nwItemType.FILE:
return True
# This is a new files, so let's add some content
newDoc = NWDoc(self.theProject, self.theParent)
curTxt = newDoc.openDocument(tHandle, showStatus=False)
if curTxt == "":
if nwItem.itemLayout == nwItemLayout.CHAPTER:
newText = f"## {nwItem.itemName}\n\n"
elif nwItem.itemLayout == nwItemLayout.UNNUMBERED:
newText = f"## {nwItem.itemName}\n\n"
elif nwItem.itemLayout == nwItemLayout.SCENE:
newText = f"### {nwItem.itemName}\n\n"
else:
newText = f"# {nwItem.itemName}\n\n"
# Save the text and index it
newDoc.saveDocument(newText)
self.theIndex.scanText(tHandle, newText)
# Get Word Counts
cC, wC, pC = self.theIndex.getCounts(tHandle)
nwItem.setCharCount(cC)
nwItem.setWordCount(wC)
nwItem.setParaCount(pC)
self.propagateCount(tHandle, wC)
self.projectWordCount()
return True
@@ -302,7 +336,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("No project open")
return False
if qApp.focusWidget() != self:
if not self.hasFocus():
return False
tHandle = self.getSelectedHandle()
@@ -328,6 +362,7 @@ class GuiProjectTree(QTreeWidget):
return False
cItem = pItem.takeChild(tIndex)
pItem.insertChild(nIndex, cItem)
self._recordLastMove(cItem, pItem, tIndex)
self.clearSelection()
cItem.setSelected(True)
@@ -416,7 +451,7 @@ class GuiProjectTree(QTreeWidget):
for tHandle in self.getTreeFromHandle(trashHandle):
if tHandle == trashHandle:
continue
self.deleteItem(tHandle, True)
self.deleteItem(tHandle, alreadyAsked=True)
if nTrash > 0:
self._setTreeChanged(True)
@@ -484,8 +519,9 @@ class GuiProjectTree(QTreeWidget):
theDoc = NWDoc(self.theProject, self.theParent)
theDoc.deleteDocument(tHandle)
del self.theProject.projTree[tHandle]
self.theIndex.deleteHandle(tHandle)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
else:
# The file is not already in the trash folder, so we
@@ -508,11 +544,12 @@ class GuiProjectTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
nwItemS.setParent(self.theProject.projTree.trashRoot())
self._updateItemParent(tHandle)
self.propagateCount(tHandle, wCount)
self._setTreeChanged(True)
self.theIndex.deleteHandle(tHandle)
self._recordLastMove(trItemS, trItemP, tIndex)
self._setTreeChanged(True)
elif nwItemS.itemType == nwItemType.FOLDER:
logger.debug("User requested folder %s deleted" % tHandle)
@@ -523,7 +560,8 @@ class GuiProjectTree(QTreeWidget):
tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0:
trItemP.takeChild(tIndex)
del self.theProject.projTree[tHandle]
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
else:
self.makeAlert((
"Cannot delete folder. It is not empty. "
@@ -537,7 +575,7 @@ class GuiProjectTree(QTreeWidget):
tIndex = self.indexOfTopLevelItem(trItemS)
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
del self.theProject.projTree[tHandle]
self._deleteTreeItem(tHandle)
self.theParent.mainMenu.setAvailableRoot()
self._setTreeChanged(True)
else:
@@ -646,6 +684,52 @@ class GuiProjectTree(QTreeWidget):
logger.debug("%d items added to the project tree" % iCount)
return True
def undoLastMove(self):
"""Attempt to undo the last action.
"""
srcItem = self._lastMove.get("item", None)
dstItem = self._lastMove.get("parent", None)
dstIndex = self._lastMove.get("index", None)
if not self.hasFocus():
return False
if srcItem is None or dstItem is None or dstIndex is None:
logger.verbose("No tree move to undo")
return False
if srcItem not in self._treeMap.values():
logger.warning("Source item no longer exists")
return False
if dstItem not in self._treeMap.values():
logger.warning("Previous parent item no longer exists")
return False
dstIndex = min(max(0, dstIndex), dstItem.childCount())
wCount = int(srcItem.data(self.C_COUNT, Qt.UserRole))
sHandle = srcItem.data(self.C_NAME, Qt.UserRole)
dHandle = dstItem.data(self.C_NAME, Qt.UserRole)
logger.debug("Moving item %s back to %s, index %d" % (
sHandle, dHandle, dstIndex
))
self.propagateCount(sHandle, 0)
parItem = srcItem.parent()
srcIndex = parItem.indexOfChild(srcItem)
movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem)
snItem = self.theProject.projTree[sHandle]
dnItem = self.theProject.projTree[dHandle]
self._postItemMove(sHandle, snItem, dnItem, wCount)
self.clearSelection()
movItem.setSelected(True)
self._lastMove = {}
return True
def getSelectedHandle(self):
"""Get the currently selected handle. If multiple items are
selected, return the first.
@@ -754,46 +838,30 @@ class GuiProjectTree(QTreeWidget):
self.makeAlert("The item cannot be moved to that location.", nwAlert.ERROR)
return
pItem = sItem.parent()
pIndex = 0
if pItem is not None:
pIndex = pItem.indexOfChild(sItem)
wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
isFile = snItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onFile = dnItem.itemType == nwItemType.FILE
isSame = snItem.itemClass == dnItem.itemClass
isNone = snItem.itemClass == nwItemClass.NO_CLASS
isNote = snItem.itemLayout == nwItemLayout.NOTE
onFile = dnItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onFree = dnItem.itemClass == nwItemClass.ARCHIVE
onFree |= dnItem.itemClass == nwItemClass.TRASH
onFree &= snItem.itemType == nwItemType.FILE
isOnTop = self.dropIndicatorPosition() == QAbstractItemView.OnItem
if (isSame or isNone or isNote or onFree) and not (onFile and isOnTop) and not isRoot:
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
allowDrop = isSame or isNone or isNote or onFree
allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
if allowDrop and not isRoot:
logger.debug("Drag'n'drop of item %s accepted" % sHandle)
self.propagateCount(sHandle, 0)
QTreeWidget.dropEvent(self, theEvent)
self._updateItemParent(sHandle)
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
if not (isSame or onFree):
logger.debug("Item %s class has been changed from %s to %s" % (
sHandle,
snItem.itemClass.name,
dnItem.itemClass.name
))
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
self.propagateCount(sHandle, wCount)
# The items dropped into archive or trash should be removed
# from the project index, for all other items, we rescan the
# file to ensure the index is up to date.
if onFree:
self.theIndex.deleteHandle(sHandle)
else:
self.theIndex.reIndexHandle(sHandle)
# Trigger dependent updates
self._setTreeChanged(True)
self._emitItemChange(sHandle)
self._postItemMove(sHandle, snItem, dnItem, wCount)
self._recordLastMove(sItem, pItem, pIndex)
else:
theEvent.ignore()
@@ -806,22 +874,63 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions
##
def _postItemMove(self, sHandle, snItem, dnItem, wCount):
"""Run various maintenance tasks for a moved item.
"""
isFile = snItem.itemType == nwItemType.FILE
isSame = snItem.itemClass == dnItem.itemClass
onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
self._updateItemParent(sHandle)
# If the item does not have the same class as the target,
# and the target is not a free root folder, update its class
if not (isSame or onFree):
logger.debug("Item %s class has been changed from %s to %s" % (
sHandle, snItem.itemClass.name, dnItem.itemClass.name
))
snItem.setClass(dnItem.itemClass)
self.setTreeItemValues(sHandle)
self.propagateCount(sHandle, wCount)
# The items dropped into archive or trash should be removed
# from the project index, for all other items, we rescan the
# file to ensure the index is up to date.
if onFree:
self.theIndex.deleteHandle(sHandle)
else:
self.theIndex.reIndexHandle(sHandle)
# Trigger dependent updates
self._setTreeChanged(True)
self._emitItemChange(sHandle)
return
def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle.
"""
return self._treeMap.get(tHandle, None)
def _scanChildren(self, theList, theItem, theIndex):
def _deleteTreeItem(self, tHandle):
"""Delete a tree item from the project and the map.
"""
del self.theProject.projTree[tHandle]
self._treeMap.pop(tHandle, None)
return
def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
"""
tHandle = theItem.data(self.C_NAME, Qt.UserRole)
tHandle = tItem.data(self.C_NAME, Qt.UserRole)
nwItem = self.theProject.projTree[tHandle]
nwItem.setExpanded(theItem.isExpanded())
nwItem.setOrder(theIndex)
nwItem.setExpanded(tItem.isExpanded())
nwItem.setOrder(tIndex)
theList.append(tHandle)
for i in range(theItem.childCount()):
self._scanChildren(theList, theItem.child(i), i)
for i in range(tItem.childCount()):
self._scanChildren(theList, tItem.child(i), i)
return theList
def _addTreeItem(self, nwItem, nHandle=None):
@@ -951,6 +1060,19 @@ class GuiProjectTree(QTreeWidget):
return
def _recordLastMove(self, srcItem, parItem, parIndex):
"""Record the last action so that it can be undone.
"""
prevItem = self._lastMove.get("item", None)
if prevItem is None or srcItem != prevItem:
self._lastMove = {
"item": srcItem,
"parent": parItem,
"index": parIndex,
}
return
# END Class GuiProjectTree
class GuiProjectTreeMenu(QMenu):
+27 -14
View File
@@ -29,7 +29,6 @@ import logging
from time import time
from PyQt5.QtCore import QTimer
from PyQt5.QtGui import QColor, QPainter
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel, QAbstractButton
@@ -49,6 +48,7 @@ class GuiMainStatus(QStatusBar):
self.theParent = theParent
self.theTheme = theParent.theTheme
self.refTime = None
self.userIdle = False
colNone = QColor(*self.theTheme.statNone)
colTrue = QColor(*self.theTheme.statUnsaved)
@@ -97,9 +97,12 @@ class GuiMainStatus(QStatusBar):
## The Session Clock
### Set the mimimum width so the label doesn't rescale every second
self.timePixmap = self.theTheme.getPixmap("status_time", (iPx, iPx))
self.idlePixmap = self.theTheme.getPixmap("status_idle", (iPx, iPx))
self.timeIcon = QLabel()
self.timeText = QLabel("")
self.timeIcon.setPixmap(self.theTheme.getPixmap("status_time", (iPx, iPx)))
self.timeIcon.setPixmap(self.timePixmap)
self.timeText.setToolTip("Session Time")
self.timeText.setMinimumWidth(self.theTheme.getTextWidth("00:00:00:"))
self.timeIcon.setContentsMargins(0, 0, 0, 0)
@@ -110,12 +113,6 @@ class GuiMainStatus(QStatusBar):
# Other Settings
self.setSizeGripEnabled(True)
# Start the Clock
self.sessionTimer = QTimer()
self.sessionTimer.setInterval(1000)
self.sessionTimer.timeout.connect(self._updateTime)
self.sessionTimer.start()
logger.debug("GuiMainStatus initialisation complete")
self.clearStatus()
@@ -130,7 +127,7 @@ class GuiMainStatus(QStatusBar):
self.setStats(0, 0)
self.setProjectStatus(None)
self.setDocumentStatus(None)
self._updateTime()
self.updateTime()
return True
##
@@ -182,17 +179,33 @@ class GuiMainStatus(QStatusBar):
self.statsText.setToolTip("Project word count (session change)")
return
##
# Internal Functions
##
def setUserIdle(self, userIdle):
"""Change the idle status icon.
"""
if not self.mainConf.stopWhenIdle:
userIdle = False
def _updateTime(self):
if self.userIdle != userIdle:
if userIdle:
self.timeIcon.setPixmap(self.idlePixmap)
else:
self.timeIcon.setPixmap(self.timePixmap)
self.userIdle = userIdle
return
def updateTime(self, idleTime=0.0):
"""Update the session clock.
"""
if self.refTime is None:
self.timeText.setText("00:00:00")
else:
self.timeText.setText(formatTime(round(time() - self.refTime)))
if self.mainConf.stopWhenIdle:
sessTime = round(time() - self.refTime - idleTime)
else:
sessTime = round(time() - self.refTime)
self.timeText.setText(formatTime(sessTime))
return
# END Class GuiMainStatus
+9 -8
View File
@@ -268,9 +268,9 @@ class GuiTheme:
if os.path.isfile(self.cssFile):
with open(self.cssFile, mode="r", encoding="utf8") as inFile:
cssData = inFile.read()
except Exception as e:
except Exception:
logger.error("Could not load theme css file")
logger.error(str(e))
nw.logException()
return False
# Config File
@@ -278,9 +278,9 @@ class GuiTheme:
try:
with open(self.confFile, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
except Exception:
logger.error("Could not load theme settings from: %s" % self.confFile)
logger.error(str(e))
nw.logException()
return False
## Main
@@ -336,9 +336,9 @@ class GuiTheme:
try:
with open(self.syntaxFile, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
except Exception:
logger.error("Could not load syntax colours from: %s" % self.syntaxFile)
logger.error(str(e))
nw.logException()
return False
## Main
@@ -539,6 +539,7 @@ class GuiIcons:
"proj_nwx" : (None, None),
"status_lang" : (None, None),
"status_time" : (None, None),
"status_idle" : (None, None),
"status_stats" : (None, None),
"status_lines" : (None, None),
"doc_h0" : (QStyle.SP_FileIcon, "x-office-document"),
@@ -640,9 +641,9 @@ class GuiIcons:
try:
with open(self.confFile, mode="r", encoding="utf8") as inFile:
confParser.read_file(inFile)
except Exception as e:
except Exception:
logger.error("Could not load icon theme settings from: %s" % self.confFile)
logger.error(str(e))
nw.logException()
return False
## Main
+213
View File
@@ -0,0 +1,213 @@
# -*- coding: utf-8 -*-
"""
novelWriter GUI User Wordlist
===============================
Class holding the user's wordlist dialog
File History:
Created: 2021-02-12 [1.2b1]
This file is a part of novelWriter
Copyright 20182021, 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 nw
import logging
import os
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
QAbstractItemView, QPushButton, QLineEdit, QLabel
)
from nw.constants import nwFiles, nwAlert
logger = logging.getLogger(__name__)
class GuiWordList(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiWordList ...")
self.setObjectName("GuiWordList")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.theProject = theProject
self.optState = theProject.optState
self.setWindowTitle("Project Word List")
mS = self.mainConf.pxInt(250)
wW = self.mainConf.pxInt(320)
wH = self.mainConf.pxInt(340)
self.setMinimumWidth(mS)
self.setMinimumHeight(mS)
self.resize(
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winWidth", wW)),
self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winHeight", wH))
)
# Main Widgets
# ============
self.headLabel = QLabel("<b>Project Word List</b>")
self.listBox = QListWidget()
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setSortingEnabled(True)
self.newEntry = QLineEdit()
self.addButton = QPushButton(self.theTheme.getIcon("add"), "")
self.addButton.setToolTip("Add new entry")
self.addButton.clicked.connect(self._doAdd)
self.delButton = QPushButton(self.theTheme.getIcon("remove"), "")
self.delButton.setToolTip("Delete selected entry")
self.delButton.clicked.connect(self._doDelete)
self.editBox = QHBoxLayout()
self.editBox.addWidget(self.newEntry, 1)
self.editBox.addWidget(self.addButton, 0)
self.editBox.addWidget(self.delButton, 0)
self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close)
self.buttonBox.accepted.connect(self._doSave)
self.buttonBox.rejected.connect(self._doClose)
# Assemble
# ========
self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.headLabel)
self.outerBox.addSpacing(self.mainConf.pxInt(8))
self.outerBox.addWidget(self.listBox, 1)
self.outerBox.addLayout(self.editBox, 0)
self.outerBox.addSpacing(self.mainConf.pxInt(12))
self.outerBox.addWidget(self.buttonBox, 0)
self.setLayout(self.outerBox)
self._loadWordList()
logger.debug("GuiWordList initialisation complete")
return
##
# Slots
##
def _doAdd(self):
"""Add a new word to the word list.
"""
newWord = self.newEntry.text().strip()
if newWord == "":
self.theParent.makeAlert("Cannot add a blank word.", nwAlert.ERROR)
return False
if self.listBox.findItems(newWord, Qt.MatchExactly):
self.theParent.makeAlert(
"The word '%s' is already in the word list." % newWord, nwAlert.ERROR
)
return False
self.listBox.addItem(newWord)
self.newEntry.setText("")
return True
def _doDelete(self):
"""Delete the selected item.
"""
selItem = self.listBox.selectedItems()
if selItem:
self.listBox.takeItem(self.listBox.row(selItem[0]))
return
def _doSave(self):
"""Save the new word list and close.
"""
self._saveGuiSettings()
dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
tmpFile = dctFile + "~"
try:
with open(tmpFile, mode="w", encoding="utf8") as outFile:
for i in range(self.listBox.count()):
outFile.write(self.listBox.item(i).text() + "\n")
except Exception:
logger.error("Could not save new word list")
nw.logException()
self.reject()
return False
if os.path.isfile(dctFile):
os.unlink(dctFile)
os.rename(tmpFile, dctFile)
self.accept()
return True
def _doClose(self):
"""Close without saving the word list.
"""
self._saveGuiSettings()
self.reject()
return
##
# Internal Functions
##
def _loadWordList(self):
"""Load the project's word list, if it exists.
"""
self.listBox.clear()
wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT)
if not os.path.isfile(wordList):
logger.debug("No project dictionary file found")
return False
with open(wordList, mode="r", encoding="utf8") as inFile:
for inLine in inFile:
theWord = inLine.strip()
if len(theWord) == 0:
continue
self.listBox.addItem(theWord)
return True
def _saveGuiSettings(self):
"""Save GUI settings.
"""
winWidth = self.mainConf.rpxInt(self.width())
winHeight = self.mainConf.rpxInt(self.height())
self.optState.setValue("GuiWordList", "winWidth", winWidth)
self.optState.setValue("GuiWordList", "winHeight", winHeight)
return
# END Class GuiWordList
+79 -26
View File
@@ -48,8 +48,9 @@ class GuiWritingStats(QDialog):
C_TIME = 0
C_LENGTH = 1
C_COUNT = 2
C_BAR = 3
C_IDLE = 2
C_COUNT = 3
C_BAR = 4
FMT_JSON = 0
FMT_CSV = 1
@@ -89,16 +90,21 @@ class GuiWritingStats(QDialog):
wCol2 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol2", 80)
)
wCol3 = self.mainConf.pxInt(
self.optState.getInt("GuiWritingStats", "widthCol3", 80)
)
self.listBox = QTreeWidget()
self.listBox.setHeaderLabels(["Session Start", "Length", "Words", "Histogram"])
self.listBox.setHeaderLabels(["Session Start", "Length", "Idle", "Words", "Histogram"])
self.listBox.setIndentation(0)
self.listBox.setColumnWidth(self.C_TIME, wCol0)
self.listBox.setColumnWidth(self.C_LENGTH, wCol1)
self.listBox.setColumnWidth(self.C_COUNT, wCol2)
self.listBox.setColumnWidth(self.C_IDLE, wCol2)
self.listBox.setColumnWidth(self.C_COUNT, wCol3)
hHeader = self.listBox.headerItem()
hHeader.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
hHeader.setTextAlignment(self.C_IDLE, Qt.AlignRight)
hHeader.setTextAlignment(self.C_COUNT, Qt.AlignRight)
sortValid = (Qt.AscendingOrder, Qt.DescendingOrder)
@@ -127,6 +133,10 @@ class GuiWritingStats(QDialog):
self.labelTotal.setFont(self.theTheme.guiFontFixed)
self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.labelIdleT = QLabel(formatTime(0))
self.labelIdleT.setFont(self.theTheme.guiFontFixed)
self.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.labelFilter = QLabel(formatTime(0))
self.labelFilter.setFont(self.theTheme.guiFontFixed)
self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
@@ -144,16 +154,18 @@ class GuiWritingStats(QDialog):
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.infoForm.addWidget(QLabel("Total Time:"), 0, 0)
self.infoForm.addWidget(QLabel("Filtered Time:"), 1, 0)
self.infoForm.addWidget(QLabel("Novel Word Count:"), 2, 0)
self.infoForm.addWidget(QLabel("Notes Word Count:"), 3, 0)
self.infoForm.addWidget(QLabel("Total Word Count:"), 4, 0)
self.infoForm.addWidget(QLabel("Idle Time:"), 1, 0)
self.infoForm.addWidget(QLabel("Filtered Time:"), 2, 0)
self.infoForm.addWidget(QLabel("Novel Word Count:"), 3, 0)
self.infoForm.addWidget(QLabel("Notes Word Count:"), 4, 0)
self.infoForm.addWidget(QLabel("Total Word Count:"), 5, 0)
self.infoForm.addWidget(self.labelTotal, 0, 1)
self.infoForm.addWidget(self.labelFilter, 1, 1)
self.infoForm.addWidget(self.novelWords, 2, 1)
self.infoForm.addWidget(self.notesWords, 3, 1)
self.infoForm.addWidget(self.totalWords, 4, 1)
self.infoForm.setRowStretch(5, 1)
self.infoForm.addWidget(self.labelIdleT, 1, 1)
self.infoForm.addWidget(self.labelFilter, 2, 1)
self.infoForm.addWidget(self.novelWords, 3, 1)
self.infoForm.addWidget(self.notesWords, 4, 1)
self.infoForm.addWidget(self.totalWords, 5, 1)
self.infoForm.setRowStretch(6, 1)
# Filter Options
sPx = self.theTheme.baseIconSize
@@ -192,17 +204,25 @@ class GuiWritingStats(QDialog):
)
self.groupByDay.clicked.connect(self._updateListBox)
self.showIdleTime = QSwitch(width=2*sPx, height=sPx)
self.showIdleTime.setChecked(
self.optState.getBool("GuiWritingStats", "showIdleTime", False)
)
self.showIdleTime.clicked.connect(self._updateListBox)
self.filterForm.addWidget(QLabel("Count novel files"), 0, 0)
self.filterForm.addWidget(QLabel("Count note files"), 1, 0)
self.filterForm.addWidget(QLabel("Hide zero word count"), 2, 0)
self.filterForm.addWidget(QLabel("Hide negative word count"), 3, 0)
self.filterForm.addWidget(QLabel("Group entries by day"), 4, 0)
self.filterForm.addWidget(QLabel("Show idle time"), 5, 0)
self.filterForm.addWidget(self.incNovel, 0, 1)
self.filterForm.addWidget(self.incNotes, 1, 1)
self.filterForm.addWidget(self.hideZeros, 2, 1)
self.filterForm.addWidget(self.hideNegative, 3, 1)
self.filterForm.addWidget(self.groupByDay, 4, 1)
self.filterForm.setRowStretch(5, 1)
self.filterForm.addWidget(self.showIdleTime, 5, 1)
self.filterForm.setRowStretch(6, 1)
# Settings
self.histMax = QSpinBox(self)
@@ -278,6 +298,7 @@ class GuiWritingStats(QDialog):
widthCol0 = self.mainConf.rpxInt(self.listBox.columnWidth(0))
widthCol1 = self.mainConf.rpxInt(self.listBox.columnWidth(1))
widthCol2 = self.mainConf.rpxInt(self.listBox.columnWidth(2))
widthCol3 = self.mainConf.rpxInt(self.listBox.columnWidth(3))
sortCol = self.listBox.sortColumn()
sortOrder = self.listBox.header().sortIndicatorOrder()
incNovel = self.incNovel.isChecked()
@@ -285,6 +306,7 @@ class GuiWritingStats(QDialog):
hideZeros = self.hideZeros.isChecked()
hideNegative = self.hideNegative.isChecked()
groupByDay = self.groupByDay.isChecked()
showIdleTime = self.showIdleTime.isChecked()
histMax = self.histMax.value()
self.optState.setValue("GuiWritingStats", "winWidth", winWidth)
@@ -292,6 +314,7 @@ class GuiWritingStats(QDialog):
self.optState.setValue("GuiWritingStats", "widthCol0", widthCol0)
self.optState.setValue("GuiWritingStats", "widthCol1", widthCol1)
self.optState.setValue("GuiWritingStats", "widthCol2", widthCol2)
self.optState.setValue("GuiWritingStats", "widthCol3", widthCol3)
self.optState.setValue("GuiWritingStats", "sortCol", sortCol)
self.optState.setValue("GuiWritingStats", "sortOrder", sortOrder)
self.optState.setValue("GuiWritingStats", "incNovel", incNovel)
@@ -299,6 +322,7 @@ class GuiWritingStats(QDialog):
self.optState.setValue("GuiWritingStats", "hideZeros", hideZeros)
self.optState.setValue("GuiWritingStats", "hideNegative", hideNegative)
self.optState.setValue("GuiWritingStats", "groupByDay", groupByDay)
self.optState.setValue("GuiWritingStats", "showIdleTime", showIdleTime)
self.optState.setValue("GuiWritingStats", "histMax", histMax)
self.optState.saveSettings()
@@ -347,23 +371,25 @@ class GuiWritingStats(QDialog):
with open(savePath, mode="w", encoding="utf8") as outFile:
if dataFmt == self.FMT_JSON:
jsonData = []
for _, sD, tT, wD, wA, wB in self.filterData:
for _, sD, tT, wD, wA, wB, tI in self.filterData:
jsonData.append({
"date": sD,
"length": tT,
"newWords": wD,
"novelWords": wA,
"noteWords": wB,
"idleTime": tI,
})
json.dump(jsonData, outFile, indent=2)
wSuccess = True
if dataFmt == self.FMT_CSV:
outFile.write(
'"Date","Length (sec)","Words Changed","Novel Words","Note Words"\n'
'"Date","Length (sec)","Words Changed",'
'"Novel Words","Note Words","Idle Time (sec)"\n'
)
for _, sD, tT, wD, wA, wB in self.filterData:
outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB}\n')
for _, sD, tT, wD, wA, wB, tI in self.filterData:
outFile.write(f'"{sD}",{tT:.0f},{wD},{wA},{wB},{tI}\n')
wSuccess = True
except Exception as e:
@@ -401,6 +427,7 @@ class GuiWritingStats(QDialog):
ttNovel = 0
ttNotes = 0
ttTime = 0
ttIdle = 0
logFile = os.path.join(self.theProject.projMeta, nwFiles.SESS_STATS)
if not os.path.isfile(logFile):
@@ -419,7 +446,7 @@ class GuiWritingStats(QDialog):
continue
inData = inLine.split()
if len(inData) != 6:
if len(inData) < 6:
continue
dStart = datetime.strptime(
@@ -429,16 +456,21 @@ class GuiWritingStats(QDialog):
"%s %s" % (inData[2], inData[3]), nwConst.FMT_TSTAMP
)
sIdle = 0
if len(inData) > 6:
sIdle = checkInt(inData[6], 0)
tDiff = dEnd - dStart
sDiff = tDiff.total_seconds()
ttTime += sDiff
ttIdle += sIdle
wcNovel = int(inData[4])
wcNotes = int(inData[5])
ttNovel = wcNovel
ttNotes = wcNotes
self.logData.append((dStart, sDiff, wcNovel, wcNotes))
self.logData.append((dStart, sDiff, wcNovel, wcNotes, sIdle))
except Exception as e:
self.theParent.makeAlert(
@@ -448,12 +480,17 @@ class GuiWritingStats(QDialog):
ttWords = ttNovel + ttNotes
self.labelTotal.setText(formatTime(round(ttTime)))
self.labelIdleT.setText(formatTime(round(ttIdle)))
self.novelWords.setText(f"{ttNovel:n}")
self.notesWords.setText(f"{ttNotes:n}")
self.totalWords.setText(f"{ttWords:n}")
return True
##
# Slots
##
def _updateListBox(self, dummyVar=None):
"""Load/reload the content of the list box. The dummyVar
variable captures the variable sent from the widgets connecting
@@ -474,25 +511,28 @@ class GuiWritingStats(QDialog):
tempData = []
sessDate = None
sessTime = 0
sessIdle = 0
lstNovel = 0
lstNotes = 0
for n, (dStart, sDiff, wcNovel, wcNotes) in enumerate(self.logData):
for n, (dStart, sDiff, wcNovel, wcNotes, sIdle) in enumerate(self.logData):
if n == 0:
sessDate = dStart.date()
if sessDate != dStart.date():
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sessIdle))
sessDate = dStart.date()
sessTime = sDiff
sessIdle = sIdle
lstNovel = wcNovel
lstNotes = wcNotes
else:
sessTime += sDiff
sessIdle += sIdle
lstNovel = wcNovel
lstNotes = wcNotes
if sessDate is not None:
tempData.append((sessDate, sessTime, lstNovel, lstNotes))
tempData.append((sessDate, sessTime, lstNovel, lstNotes, sessIdle))
else:
tempData = self.logData
@@ -502,7 +542,7 @@ class GuiWritingStats(QDialog):
pcTotal = 0
listMax = 0
isFirst = True
for dStart, sDiff, wcNovel, wcNotes in tempData:
for dStart, sDiff, wcNovel, wcNotes, sIdle in tempData:
wcTotal = 0
if incNovel:
@@ -528,16 +568,24 @@ class GuiWritingStats(QDialog):
else:
sStart = dStart.strftime(nwConst.FMT_TSTAMP)
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes))
self.filterData.append((dStart, sStart, sDiff, dwTotal, wcNovel, wcNotes, sIdle))
listMax = min(max(listMax, dwTotal), histMax)
pcTotal = wcTotal
# Populate the list
for _, sStart, sDiff, nWords, _, _ in self.filterData:
showIdleTime = self.showIdleTime.isChecked()
for _, sStart, sDiff, nWords, _, _, sIdle in self.filterData:
if showIdleTime:
idleEntry = formatTime(sIdle)
else:
sRatio = sIdle/sDiff if sDiff > 0.0 else 0.0
idleEntry = "%d %%" % round(100.0 * sRatio)
newItem = QTreeWidgetItem()
newItem.setText(self.C_TIME, sStart)
newItem.setText(self.C_LENGTH, formatTime(round(sDiff)))
newItem.setText(self.C_IDLE, idleEntry)
newItem.setText(self.C_COUNT, f"{nWords:n}")
if nWords > 0 and listMax > 0:
@@ -550,12 +598,17 @@ class GuiWritingStats(QDialog):
newItem.setData(self.C_BAR, Qt.DecorationRole, theBar)
newItem.setTextAlignment(self.C_LENGTH, Qt.AlignRight)
newItem.setTextAlignment(self.C_IDLE, Qt.AlignRight)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight)
newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setFont(self.C_TIME, self.theTheme.guiFontFixed)
newItem.setFont(self.C_LENGTH, self.theTheme.guiFontFixed)
newItem.setFont(self.C_COUNT, self.theTheme.guiFontFixed)
if showIdleTime:
newItem.setFont(self.C_IDLE, self.theTheme.guiFontFixed)
else:
newItem.setFont(self.C_IDLE, self.theTheme.guiFont)
self.listBox.addTopLevelItem(newItem)
self.timeFilter += sDiff
+62 -6
View File
@@ -43,7 +43,7 @@ from nw.gui import (
GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor,
GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails,
GuiPreferences, GuiProjectDetails, GuiProjectLoad, GuiProjectSettings,
GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWritingStats
GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWordList, GuiWritingStats
)
from nw.core import NWProject, NWDoc, NWIndex
from nw.constants import nwItemType, nwItemClass, nwAlert, nwLists
@@ -86,6 +86,8 @@ class GuiMain(QMainWindow):
self.theIndex = NWIndex(self.theProject, self)
self.hasProject = False
self.isFocusMode = False
self.idleRefTime = time()
self.idleTime = 0.0
# Prepare Main Window
self.resize(*self.mainConf.getWinSize())
@@ -241,6 +243,12 @@ class GuiMain(QMainWindow):
self.asDocTimer = QTimer()
self.asDocTimer.timeout.connect(self._autoSaveDocument)
# Main Clock
self.mainTimer = QTimer()
self.mainTimer.setInterval(1000)
self.mainTimer.timeout.connect(self._timeTick)
self.mainTimer.start()
# Shortcuts and Actions
self._connectMenuActions()
@@ -412,7 +420,11 @@ class GuiMain(QMainWindow):
self.closeDocument()
self.docViewer.clearNavHistory()
self.projView.closeOutline()
self.theProject.closeProject()
self.theProject.closeProject(self.idleTime)
self.idleRefTime = time()
self.idleTime = 0.0
self.theIndex.clearIndex()
self.clearGUI()
self.hasProject = False
@@ -477,7 +489,9 @@ class GuiMain(QMainWindow):
return False
# Project is loaded
self.hasProject = True
self.hasProject = True
self.idleRefTime = time()
self.idleTime = 0.0
# Load the tag index
self.theIndex.loadIndex()
@@ -786,7 +800,11 @@ class GuiMain(QMainWindow):
return False
if tHandle is None:
tHandle = self.treeView.getSelectedHandle()
if self.treeView.hasFocus():
tHandle = self.treeView.getSelectedHandle()
elif self.docEditor.hasFocus():
tHandle = self.docEditor.theHandle
if tHandle is None:
logger.warning("No item selected")
return
@@ -999,6 +1017,22 @@ class GuiMain(QMainWindow):
return
def showProjectWordListDialog(self):
"""Open the project word list dialog.
"""
if not self.hasProject:
logger.error("No project open")
return
dlgWords = GuiWordList(self, self.theProject)
dlgWords.exec_()
if dlgWords.result() == QDialog.Accepted:
logger.debug("Reloading word list")
self.docEditor.setDictionaries()
return
def showWritingStatsDialog(self):
"""Open the session log dialog.
"""
@@ -1175,11 +1209,11 @@ class GuiMain(QMainWindow):
"""
if self.docEditor.theHandle is None:
logger.error("No document open, so not activating Focus Mode")
self.mainMenu.aFocusMode.setChecked(self.isFocusMode)
self.mainMenu.setFocusMode(self.isFocusMode)
return False
self.isFocusMode = not self.isFocusMode
self.mainMenu.aFocusMode.setChecked(self.isFocusMode)
self.mainMenu.setFocusMode(self.isFocusMode)
if self.isFocusMode:
logger.debug("Activating Focus Mode")
self.mainTabs.setCurrentWidget(self.splitDocs)
@@ -1414,6 +1448,28 @@ class GuiMain(QMainWindow):
# Slots
##
@pyqtSlot()
def _timeTick(self):
"""Triggered on every tick of the timer.
"""
if not self.hasProject:
return
currTime = time()
editIdle = currTime - self.docEditor.lastActive > self.mainConf.userIdleTime
userIdle = qApp.applicationState() != Qt.ApplicationActive
if editIdle or userIdle:
self.idleTime += currTime - self.idleRefTime
self.statusBar.setUserIdle(True)
else:
self.statusBar.setUserIdle(False)
self.idleRefTime = currTime
self.statusBar.updateTime(idleTime=self.idleTime)
return
@pyqtSlot()
def _treeSingleClick(self):
"""Single click on a project tree item just updates the details
-23
View File
@@ -1,23 +0,0 @@
Table of Contents
=================
File Name Class Layout Document Label
---------------------------------------------------------------------
content/53b69b83cdafc.nwd NOVEL TITLE Title Page
content/974e400180a99.nwd NOVEL PAGE Page
content/edca4be2fcaf8.nwd NOVEL PARTITION Part One
content/6a2d6d5f4f401.nwd NOVEL CHAPTER Chapter One
content/636b6aa9b697b.nwd NOVEL SCENE Making a Scene
content/bc0cbd2a407f3.nwd NOVEL SCENE Another Scene
content/ba8a28a246524.nwd NOVEL UNNUMBERED Interlude
content/96b68994dfa3d.nwd NOVEL NOTE A Note on Structure
content/88706ddc78b1b.nwd NOVEL CHAPTER Chapter Two
content/ae7339df26ded.nwd NOVEL SCENE We Found John!
content/14298de4d9524.nwd CHARACTER NOTE John Smith
content/bb2c23b3c42cc.nwd CHARACTER NOTE Jane Smith
content/b3e74dbc1f584.nwd WORLD NOTE Earth
content/f1471bef9f2ae.nwd WORLD NOTE Space
content/5eaea4e8cdee8.nwd WORLD NOTE Mars
content/8a5deb88c0e97.nwd NOVEL SCENE Old File
content/b8136a5a774a0.nwd NOVEL SCENE Delete Me!
+1 -1
View File
@@ -9,7 +9,7 @@
A scene is defined by a level three heading, like the one at the top of this page. The scene will be assigned to the chapter preceding it in the project tree.
Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isnt fully Markdown compliant. If the syntax highlighter doesnt show it correctly, the export tool will not either.
Each paragraph in the scene is separated by a blank line. The text supports minimal formatting, like **bold**, _italic_ and **_bold italic_**. You can also ~~strike through~~ text. There is **some support for _nested_ emphasis**, but it isnt fully Markdown compliant. If the syntax highlighter doesnt show it correctly, the export tool will not either.
In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
+2
View File
@@ -4,6 +4,7 @@
### Another Scene
@pov: John
@focus: Jane
@location: Earth
Adding more scenes to a chapter is as easy as adding more scene files, with a level three heading, or just adding another level three heading in the same file if that works for the way you want to structure your files.
@@ -13,6 +14,7 @@ In fact, if you wish, you can add all the scenes in the chapter file too. All no
### More Scenes
@pov: Jane
@focus: John
@location: Earth
This is a second scene in the same file as the previous scene. You can always split the files up later.
+7 -7
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.1.1" hexVersion="0x010101f0" fileVersion="1.2" timeStamp="2021-02-20 16:38:01">
<novelWriterXML appVersion="1.2rc1" hexVersion="0x010200c1" fileVersion="1.2" timeStamp="2021-02-20 17:08:19">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>881</saveCount>
<autoCount>158</autoCount>
<editTime>43034</editTime>
<saveCount>937</saveCount>
<autoCount>161</autoCount>
<editTime>46797</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -117,10 +117,10 @@
<status>1st Draft</status>
<exported>True</exported>
<layout>SCENE</layout>
<charCount>1811</charCount>
<charCount>1810</charCount>
<wordCount>318</wordCount>
<paraCount>8</paraCount>
<cursorPos>1880</cursorPos>
<cursorPos>3</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>
@@ -132,7 +132,7 @@
<charCount>476</charCount>
<wordCount>93</wordCount>
<paraCount>3</paraCount>
<cursorPos>428</cursorPos>
<cursorPos>430</cursorPos>
</item>
<item handle="ba8a28a246524" order="3" parent="e7ded148d6e4a">
<name>Interlude</name>
-22
View File
@@ -1,22 +0,0 @@
Table of Contents
===================
File Name Class Document Label
--------------------------------------------------------------------------------
content/04468803b92e1.nwd WORLD Ancient Europe
content/2426c6f0ca922.nwd PLOT Main
content/441420a886d82.nwd NOVEL Chapter Two
content/47666c91c7ccf.nwd NOVEL Scene Five
content/4c4f28287af27.nwd CHARACTER Mr. Nobody
content/7a992350f3eb6.nwd NOVEL Lorem Ipsum
content/846352075de7d.nwd NOVEL Interlude
content/88243afbe5ed8.nwd NOVEL Scene One
content/88d59a277361b.nwd NOVEL Prologue
content/8c58a65414c23.nwd NOVEL Front Matter
content/db7e733775d4d.nwd NOVEL Act One
content/eb103bc70c90c.nwd NOVEL Scene Three
content/f8c0562e50f1b.nwd NOVEL Scene Four
content/f96ec11c6a3da.nwd NOVEL Scene Two
content/fb609cd8319dc.nwd NOVEL Chapter One
-10
View File
@@ -1,10 +0,0 @@
Table of Contents
===================
File Name Class Document Label
--------------------------------------------------------------------------------
content/8c659a11cd429.nwd NOVEL New Scene
content/a35baf2e93843.nwd NOVEL Title Page
content/f5ab3e30151e1.nwd NOVEL New Chapter
+4 -2
View File
@@ -1,12 +1,12 @@
[Main]
timestamp = 2020-10-11 22:50:45
timestamp = 2021-02-09 21:07:03
theme = default
syntax = default_light
icons = typicons_colour_light
guidark = False
guifont =
guifontsize = 11
lastnotes = 1.0
lastnotes = 0x0
[Sizes]
geometry = 1200, 650
@@ -56,6 +56,8 @@ highlightquotes = True
allowopensquote = False
allowopendquote = True
highlightemph = True
stopwhenidle = True
useridletime = 300
[Backup]
backuppath =
@@ -0,0 +1,88 @@
# 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…”
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
_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 1: Chapter One
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.
* * *
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.
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.
* * *
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.
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.
## Chapter 2: Chapter Two
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.
* * *
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.
* * *
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.
* * *
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.
@@ -5,26 +5,27 @@
<title>Lorem Ipsum</title>
</head>
<style>
p {text-align: left;}
h1, h2 {color: rgb(66, 113, 174);}
h3, h4 {color: rgb(50, 50, 50);}
h1, h2, h3, h4 {page-break-after: avoid;}
body {font-family: 'DejaVu Sans'; font-size: 11pt;}
p {text-align: left; line-height: 115%; margin-top: 0.00em; margin-bottom: 0.58em;}
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.00em; margin-bottom: 0.50em;}
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 0.83em; margin-bottom: 0.50em;}
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
.title {font-size: 2.5em; margin-top: 1.00em; margin-bottom: 0.50em;}
.sep, .skip {text-align: center; margin-top: 1.00em; margin-bottom: 1.00em;}
a {color: rgb(66, 113, 174);}
.title {font-size: 2.5em;}
.tags {color: rgb(245, 135, 31); font-weight: bold;}
.break {text-align: left;}
.sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}
.skip {margin-top: 1em; margin-bottom: 1em;}
.synopsis {font-style: italic;}
.comment {font-style: italic; color: rgb(100, 100, 100);}
article {width: 800px; margin: 40px auto;}
</style>
<body>
<article>
<h1 class='title' style='text-align: center; page-break-before: never;'>Lorem Ipsum</h1>
<h1 class='title' style='text-align: center; page-break-before: auto;'>Lorem Ipsum</h1>
<p style='text-align: center;'><strong>By lipsum.com</strong></p>
<p style='text-align: center;'>“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit&hellip;</p>
<p style='text-align: center;'>“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain&hellip;</p>
<p style='text-align: center;'>“Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit</p>
<p style='text-align: center;'>“There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain</p>
<p style='text-align: left;'>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.</p>
<p style='text-align: left;'>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.</p>
<h1 style='page-break-before: always;'>Prologue</h1>
@@ -65,7 +66,6 @@ article {width: 800px; margin: 40px auto;}
<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>
</article>
</body>
</html>
@@ -0,0 +1,88 @@
# 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…”
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
_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 1: Chapter One
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.
* * *
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.
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.
* * *
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.
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.
## Chapter 2: Chapter Two
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.
* * *
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.
* * *
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.
* * *
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.
@@ -5,44 +5,45 @@
<title>Lorem Ipsum</title>
</head>
<style>
p {text-align: justify;}
h1, h2 {color: rgb(66, 113, 174);}
h3, h4 {color: rgb(50, 50, 50);}
h1, h2, h3, h4 {page-break-after: avoid;}
body {font-family: 'DejaVu Sans'; font-size: 11pt;}
p {text-align: justify; line-height: 115%; margin-top: 0.00em; margin-bottom: 0.58em;}
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.00em; margin-bottom: 0.50em;}
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 0.83em; margin-bottom: 0.50em;}
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
.title {font-size: 2.5em; margin-top: 1.00em; margin-bottom: 0.50em;}
.sep, .skip {text-align: center; margin-top: 1.00em; margin-bottom: 1.00em;}
a {color: rgb(66, 113, 174);}
.title {font-size: 2.5em;}
.tags {color: rgb(245, 135, 31); font-weight: bold;}
.break {text-align: left;}
.sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}
.skip {margin-top: 1em; margin-bottom: 1em;}
.synopsis {font-style: italic;}
.comment {font-style: italic; color: rgb(100, 100, 100);}
article {width: 800px; margin: 40px auto;}
</style>
<body>
<article>
<h1 class='title' style='text-align: center; page-break-before: never;'>Lorem Ipsum</h1>
<h1 class='title' style='text-align: center; page-break-before: auto;'>Lorem Ipsum</h1>
<p style='text-align: center;'><strong>By lipsum.com</strong></p>
<p style='text-align: center;'>Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit&hellip;</p>
<p style='text-align: center;'>There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain&hellip;</p>
<p style='text-align: center;'>&ldquo;Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit&hellip;&rdquo;</p>
<p style='text-align: center;'>&ldquo;There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain&hellip;&rdquo;</p>
<p class='comment'><strong>Comment:</strong> Exctracted from the lipsum.com website.</p>
<p style='text-align: left;'>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.</p>
<p style='text-align: left;'>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.</p>
<p style='text-align: left;'>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 &ldquo;de Finibus Bonorum et Malorum&rdquo; (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, &ldquo;Lorem ipsum dolor sit amet..&rdquo;, comes from a line in section 1.10.32.</p>
<p style='text-align: left;'>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 &ldquo;de Finibus Bonorum et Malorum&rdquo; 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>
<p><em>Lorem Ipsum</em> is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry&#39;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 style='text-align: center; page-break-before: always;'>Act One</h1>
<p style='text-align: center;'>Fusce maximus felis libero</p>
<p style='text-align: center;'>&ldquo;Fusce maximus felis libero&rdquo;</p>
<h1 style='page-break-before: always;'>Chapter One: Chapter One</h1>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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 1.1: Scene One</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -50,9 +51,9 @@ article {width: 800px; margin: 40px auto;}
<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 1.2: Scene Two</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -64,27 +65,27 @@ article {width: 800px; margin: 40px auto;}
<h1 style='page-break-before: always;'>Chapter Two: 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>&#09;The point of using Lorem Ipsum is that it has a more-or-less normal distribution of letters, as opposed to using &#39;Content here, content here&#39;, 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 &#39;lorem ipsum&#39; 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 Three: Chapter Two</h1>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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 3.1: Scene Three</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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 3.2: Scene Four</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -93,9 +94,9 @@ article {width: 800px; margin: 40px auto;}
<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 3.3: Scene Five</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -104,22 +105,21 @@ article {width: 800px; margin: 40px auto;}
<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>
<div><span class='tags'>Tag:</span> <a name='tag_Bod'>Bod</a></div>
<p><span class='tags'>Tag:</span> <a name='tag_Bod'>Bod</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>
<div><span class='tags'>Tag:</span> <a name='tag_Main'>Main</a></div>
<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>
<div><span class='tags'>Tag:</span> <a name='tag_Europe'>Europe</a></div>
<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>
</article>
</body>
</html>
@@ -0,0 +1,182 @@
# 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…”
**Comment:** 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: Chapter One
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 1.1: Scene One
**Point of View:** Bod
**Plot:** Main
**Locations:** 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.
#### 1.1.1: 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 1.2: Scene Two
**Point of View:** Bod
**Plot:** Main
**Locations:** 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.
#### 1.2.1: 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.
## Chapter Two: Why do we use it?
**Comment:** 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 Three: Chapter Two
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 3.1: Scene Three
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 3.2: Scene Four
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 3.3: Scene Five
**Point of View:** Bod
**Plot:** Main
**Locations:** 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
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.
@@ -5,44 +5,45 @@
<title>Lorem Ipsum</title>
</head>
<style>
p {text-align: justify;}
h1, h2 {color: rgb(66, 113, 174);}
h3, h4 {color: rgb(50, 50, 50);}
h1, h2, h3, h4 {page-break-after: avoid;}
body {font-family: 'DejaVu Sans'; font-size: 11pt;}
p {text-align: justify; line-height: 115%; margin-top: 0.00em; margin-bottom: 0.58em;}
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.00em; margin-bottom: 0.50em;}
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 0.83em; margin-bottom: 0.50em;}
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
.title {font-size: 2.5em; margin-top: 1.00em; margin-bottom: 0.50em;}
.sep, .skip {text-align: center; margin-top: 1.00em; margin-bottom: 1.00em;}
a {color: rgb(66, 113, 174);}
.title {font-size: 2.5em;}
.tags {color: rgb(245, 135, 31); font-weight: bold;}
.break {text-align: left;}
.sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}
.skip {margin-top: 1em; margin-bottom: 1em;}
.synopsis {font-style: italic;}
.comment {font-style: italic; color: rgb(100, 100, 100);}
article {width: 800px; margin: 40px auto;}
</style>
<body>
<article>
<h1 class='title' style='text-align: center; page-break-before: never;'>Lorem Ipsum</h1>
<h1 class='title' style='text-align: center; page-break-before: auto;'>Lorem Ipsum</h1>
<p style='text-align: center;'><strong>By lipsum.com</strong></p>
<p style='text-align: center;'>Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit&hellip;</p>
<p style='text-align: center;'>There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain&hellip;</p>
<p style='text-align: center;'>&ldquo;Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit&hellip;&rdquo;</p>
<p style='text-align: center;'>&ldquo;There is no one who loves pain itself, who seeks after it and wants to have it, simply because it is pain&hellip;&rdquo;</p>
<p class='comment'><strong>Comment:</strong> Exctracted from the lipsum.com website.</p>
<p style='text-align: left;'>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.</p>
<p style='text-align: left;'>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.</p>
<p style='text-align: left;'>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 &ldquo;de Finibus Bonorum et Malorum&rdquo; (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, &ldquo;Lorem ipsum dolor sit amet..&rdquo;, comes from a line in section 1.10.32.</p>
<p style='text-align: left;'>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 &ldquo;de Finibus Bonorum et Malorum&rdquo; 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>
<p><em>Lorem Ipsum</em> is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry&#39;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 style='text-align: center; page-break-before: always;'>Act One</h1>
<p style='text-align: center;'>Fusce maximus felis libero</p>
<p style='text-align: center;'>&ldquo;Fusce maximus felis libero&rdquo;</p>
<h1 style='page-break-before: always;'>Chapter One: Chapter One</h1>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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 1.1: Scene One</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -50,9 +51,9 @@ article {width: 800px; margin: 40px auto;}
<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 1.2: Scene Two</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -64,27 +65,27 @@ article {width: 800px; margin: 40px auto;}
<h1 style='page-break-before: always;'>Chapter Two: 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>
<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 &#39;Content here, content here&#39;, 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 &#39;lorem ipsum&#39; 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 Three: Chapter Two</h1>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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 3.1: Scene Three</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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 3.2: Scene Four</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -93,9 +94,9 @@ article {width: 800px; margin: 40px auto;}
<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 3.3: Scene Five</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
@@ -104,22 +105,21 @@ article {width: 800px; margin: 40px auto;}
<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>
<div><span class='tags'>Tag:</span> <a name='tag_Bod'>Bod</a></div>
<p><span class='tags'>Tag:</span> <a name='tag_Bod'>Bod</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>
<div><span class='tags'>Tag:</span> <a name='tag_Main'>Main</a></div>
<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>
<div><span class='tags'>Tag:</span> <a name='tag_Europe'>Europe</a></div>
<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>
</article>
</body>
</html>
@@ -0,0 +1,182 @@
# 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…”
**Comment:** 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: Chapter One
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 1.1: Scene One
**Point of View:** Bod
**Plot:** Main
**Locations:** 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.
#### 1.1.1: 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 1.2: Scene Two
**Point of View:** Bod
**Plot:** Main
**Locations:** 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.
#### 1.2.1: 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.
## Chapter Two: Why do we use it?
**Comment:** 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 Three: Chapter Two
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 3.1: Scene Three
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 3.2: Scene Four
**Point of View:** Bod
**Plot:** Main
**Locations:** 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 3.3: Scene Five
**Point of View:** Bod
**Plot:** Main
**Locations:** 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
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.
@@ -5,26 +5,27 @@
"authors": [
"lipsum.com"
],
"buildTime": 1611662802
"buildTime": 1612954696
},
"text": {
"css": [
"p {text-align: justify;}",
"h1, h2 {color: rgb(66, 113, 174);}",
"h3, h4 {color: rgb(50, 50, 50);}",
"h1, h2, h3, h4 {page-break-after: avoid;}",
"body {font-family: 'DejaVu Sans'; font-size: 11pt;}",
"p {text-align: justify; line-height: 115%; margin-top: 0.00em; margin-bottom: 0.58em;}",
"h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.00em; margin-bottom: 0.50em;}",
"h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 0.83em; margin-bottom: 0.50em;}",
"h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}",
"h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}",
".title {font-size: 2.5em; margin-top: 1.00em; margin-bottom: 0.50em;}",
".sep, .skip {text-align: center; margin-top: 1.00em; margin-bottom: 1.00em;}",
"a {color: rgb(66, 113, 174);}",
".title {font-size: 2.5em;}",
".tags {color: rgb(245, 135, 31); font-weight: bold;}",
".break {text-align: left;}",
".sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}",
".skip {margin-top: 1em; margin-bottom: 1em;}",
".synopsis {font-style: italic;}",
".comment {font-style: italic; color: rgb(100, 100, 100);}"
],
"html": [
[
"<h1 class='title' style='text-align: center; page-break-before: never;'>Lorem Ipsum</h1>"
"<h1 class='title' style='text-align: center; page-break-before: auto;'>Lorem Ipsum</h1>"
],
[
""
@@ -38,53 +39,53 @@
],
[
"<h1 style='page-break-before: always;'>Chapter One: Chapter One</h1>",
"<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>",
"<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>",
"<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>",
"<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>"
],
[
"<h2>Scene 1: Scene One</h2>",
"<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>",
"<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>",
"<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>",
"<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>",
"<h3>Section: Scene One, Section Two</h3>"
],
[
"<h2>Scene 2: Scene Two</h2>",
"<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>",
"<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>",
"<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>",
"<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>",
"<h3>Section: Scene Two, Section Two</h3>"
],
[
"<h1 style='page-break-before: always;'>Chapter Two: Chapter Two</h1>",
"<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>",
"<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>",
"<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>",
"<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>"
],
[
"<h2>Scene 3: Scene Three</h2>",
"<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>",
"<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>",
"<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>",
"<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>"
],
[
"<h2>Scene 4: Scene Four</h2>",
"<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>",
"<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>",
"<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>",
"<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>"
],
[
"<h2>Scene 5: Scene Five</h2>",
"<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>",
"<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>",
"<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>",
"<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>"
]
]
@@ -5,64 +5,64 @@
<title>Lorem Ipsum</title>
</head>
<style>
p {text-align: justify;}
h1, h2 {color: rgb(66, 113, 174);}
h3, h4 {color: rgb(50, 50, 50);}
h1, h2, h3, h4 {page-break-after: avoid;}
body {font-family: 'DejaVu Sans'; font-size: 11pt;}
p {text-align: justify; line-height: 115%; margin-top: 0.00em; margin-bottom: 0.58em;}
h1 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 1.00em; margin-bottom: 0.50em;}
h2 {color: rgb(66, 113, 174); page-break-after: avoid; margin-top: 0.83em; margin-bottom: 0.50em;}
h3 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
h4 {color: rgb(50, 50, 50); page-break-after: avoid; margin-top: 0.58em; margin-bottom: 0.50em;}
.title {font-size: 2.5em; margin-top: 1.00em; margin-bottom: 0.50em;}
.sep, .skip {text-align: center; margin-top: 1.00em; margin-bottom: 1.00em;}
a {color: rgb(66, 113, 174);}
.title {font-size: 2.5em;}
.tags {color: rgb(245, 135, 31); font-weight: bold;}
.break {text-align: left;}
.sep {text-align: center; margin-top: 1em; margin-bottom: 1em;}
.skip {margin-top: 1em; margin-bottom: 1em;}
.synopsis {font-style: italic;}
.comment {font-style: italic; color: rgb(100, 100, 100);}
article {width: 800px; margin: 40px auto;}
</style>
<body>
<article>
<h1 class='title' style='text-align: center; page-break-before: never;'>Lorem Ipsum</h1>
<h1 class='title' style='text-align: center; page-break-before: auto;'>Lorem Ipsum</h1>
<h1 style='page-break-before: always;'>Prologue</h1>
<p class='synopsis'><strong>Synopsis:</strong> Explanation from the lipsum.com website.</p>
<h1 style='text-align: center; page-break-before: always;'>Act One</h1>
<h1 style='page-break-before: always;'>Chapter One: Chapter One</h1>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
<h2>Scene 1: Scene One</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
<h3>Section: Scene One, Section Two</h3>
<h2>Scene 2: Scene Two</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
<h3>Section: Scene Two, Section Two</h3>
<h1 style='page-break-before: always;'>Chapter Two: Chapter Two</h1>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
<h2>Scene 3: Scene Three</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
<h2>Scene 4: Scene Four</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
<h2>Scene 5: Scene Five</h2>
<div><span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a></div>
<div><span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a></div>
<div><span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a></div>
<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>
</article>
</body>
</html>
@@ -1,6 +1,6 @@
%%~name: New Scene
%%~path: 31489056e0916/0e17daca5f3e1
%%~kind: NOVEL/SCENE
%%~kind: NOVEL/BOOK
# Novel
## Chapter
@@ -1,11 +1,11 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.0b3" hexVersion="0x010000b3" fileVersion="1.2" timeStamp="2020-09-21 19:58:53">
<novelWriterXML appVersion="1.2a0" hexVersion="0x010200a0" fileVersion="1.2" timeStamp="2021-01-29 01:10:23">
<project>
<name>New Project</name>
<title></title>
<saveCount>4</saveCount>
<autoCount>1</autoCount>
<editTime>11</editTime>
<autoCount>2</autoCount>
<editTime>8</editTime>
</project>
<settings>
<doBackup>True</doBackup>
@@ -83,7 +83,7 @@
<class>NOVEL</class>
<status>New</status>
<exported>True</exported>
<layout>SCENE</layout>
<layout>BOOK</layout>
<charCount>466</charCount>
<wordCount>83</wordCount>
<paraCount>4</paraCount>
@@ -1,18 +1,18 @@
[Main]
timestamp = 2020-06-29 17:34:15
timestamp = 2021-02-09 21:07:17
theme = default
syntax = default_light
icons = typicons_colour_light
guidark = True
guifont = Cantarell
guifont = Sans
guifontsize = 12
lastnotes = 1.0
lastnotes = 0x0
[Sizes]
geometry = 1100, 650
treecols = 120, 30, 50
geometry = 1200, 650
treecols = 200, 50, 30
novelcols = 200, 50
projcols = 140, 55, 140
projcols = 200, 60, 140
mainpane = 300, 800
docpane = 400, 400
viewpane = 500, 150
@@ -26,7 +26,7 @@ autosaveproject = 40
autosavedoc = 20
[Editor]
textfont = Cantarell
textfont = None
textsize = 13
fixedwidth = False
width = 700
@@ -56,6 +56,8 @@ highlightquotes = False
allowopensquote = False
allowopendquote = True
highlightemph = False
stopwhenidle = True
useridletime = 300
[Backup]
backuppath = some/dir
+91 -1
View File
@@ -26,7 +26,8 @@ import pytest
from nw.common import (
checkString, checkBool, checkInt, colRange, formatInt, transferCase,
fuzzyTime, checkHandle, formatTimeStamp, formatTime, hexToInt,
makeFileNameSafe
makeFileNameSafe, isHandle, isTitleTag, isItemClass, isItemType,
isItemLayout
)
from tools import cmpList
@@ -88,6 +89,95 @@ def testBaseCommon_CheckHandle():
# END Test testBaseCommon_CheckHandle
@pytest.mark.base
def testBaseCommon_IsHandle():
"""Test the isHandle function.
"""
assert isHandle("47666c91c7ccf")
assert not isHandle("47666C91C7CCF")
assert not isHandle("h7666c91c7ccf")
assert not isHandle("None")
assert not isHandle(None)
assert not isHandle("STUFF")
# END Test testBaseCommon_IsHandle
@pytest.mark.base
def testBaseCommon_IsTitleTag():
"""Test the isItemClass function.
"""
assert isTitleTag("T123456")
assert not isTitleTag("t123456")
assert not isTitleTag("S123456")
assert not isTitleTag("T12345A")
assert not isTitleTag("T1234567")
assert not isTitleTag("None")
assert not isTitleTag(None)
assert not isTitleTag("STUFF")
# END Test testBaseCommon_IsTitleTag
@pytest.mark.base
def testBaseCommon_IsItemClass():
"""Test the isItemClass function.
"""
assert isItemClass("NO_CLASS")
assert isItemClass("NOVEL")
assert isItemClass("PLOT")
assert isItemClass("CHARACTER")
assert isItemClass("WORLD")
assert isItemClass("TIMELINE")
assert isItemClass("OBJECT")
assert isItemClass("ENTITY")
assert isItemClass("CUSTOM")
assert isItemClass("ARCHIVE")
assert isItemClass("TRASH")
assert not isItemClass("None")
assert not isItemClass(None)
assert not isItemClass("STUFF")
# END Test testBaseCommon_IsItemClass
@pytest.mark.base
def testBaseCommon_IsItemType():
"""Test the isItemType function.
"""
assert isItemType("NO_TYPE")
assert isItemType("ROOT")
assert isItemType("FOLDER")
assert isItemType("FILE")
assert isItemType("TRASH")
assert not isItemType("None")
assert not isItemType(None)
assert not isItemType("STUFF")
# END Test testBaseCommon_IsItemType
@pytest.mark.base
def testBaseCommon_IsItemLayout():
"""Test the isItemLayout function.
"""
assert isItemLayout("NO_LAYOUT")
assert isItemLayout("TITLE")
assert isItemLayout("BOOK")
assert isItemLayout("PAGE")
assert isItemLayout("PARTITION")
assert isItemLayout("UNNUMBERED")
assert isItemLayout("CHAPTER")
assert isItemLayout("SCENE")
assert isItemLayout("NOTE")
assert not isItemLayout("None")
assert not isItemLayout(None)
assert not isItemLayout("STUFF")
# END Test testBaseCommon_IsItemLayout
@pytest.mark.base
def testBaseCommon_HexToInt():
"""Test the hexToInt function.
+531 -52
View File
@@ -115,38 +115,7 @@ def testCoreIndex_LoadSave(monkeypatch, nwLipsum, dummyGUI, outDir, refDir):
# Break the index and check that we notice
assert not theIndex.indexBroken
theIndex._tagIndex["Bod"].append("Stuff") # No longer len() == 4
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex._refIndex["fb609cd8319dc"]["T000001"]["tags"].append("Stuff") # No longer len() == 3
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex._novelIndex["7a992350f3eb6"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex._noteIndex["4c4f28287af27"]["T000001"]["Stuff"] = "" # No longer len(keys()) == 8
theIndex.checkIndex()
assert theIndex.indexBroken
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex._textCounts["7a992350f3eb6"].append("Stuff") # No longer len() == 3
theIndex.checkIndex()
assert theIndex.indexBroken
# Make the try/except trigger as well
assert theIndex.loadIndex()
assert not theIndex.indexBroken
theIndex._refIndex["fb609cd8319dc"]["T000001"] = {"tagssss": []} # Wrong key name
theIndex._tagIndex["Bod"].append("Stuff")
theIndex.checkIndex()
assert theIndex.indexBroken
@@ -229,14 +198,28 @@ def testCoreIndex_CheckThese(nwMinimal, dummyGUI):
assert theIndex.scanText(cHandle, (
"# Jane Smith\n"
"@tag: Jane"
"@tag: Jane\n"
"@tag:\n"
"@:\n"
))
assert theIndex.scanText(nHandle, (
"# Hello World!\n"
"@pov: Jane"
"@pov: Jane\n"
"@invalid: John\n" # Checks for issue #688
))
assert theIndex._tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!"
assert theIndex.getReferences(nHandle, "T000001") == {
"@char": [],
"@custom": [],
"@entity": [],
"@focus": [],
"@location": [],
"@object": [],
"@plot": [],
"@pov": ["Jane"],
"@time": []
}
assert theIndex.novelChangedSince(0)
assert theIndex.notesChangedSince(0)
@@ -312,7 +295,7 @@ def testCoreIndex_ScanText(nwMinimal, dummyGUI):
"This is a story about Jane Smith.\n\n"
"Well, not really.\n"
))
assert str(theIndex._tagIndex) == "{'Jane': [2, '%s', 'CHARACTER', 'T000001']}" % cHandle
assert theIndex._tagIndex == {"Jane": [2, cHandle, "CHARACTER", "T000001"]}
assert theIndex.getNovelData(nHandle, "T000001")["title"] == "Hello World!"
# Check that title sections are indexed properly
@@ -523,9 +506,8 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert wC == 12 # Words in text and title only
assert pC == 2 # Paragraphs in text only
##
# getReferences
##
# getReferences
# =============
# Look up an ivalid handle
theRefs = theIndex.getReferences("Not a handle")
@@ -537,9 +519,8 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert theRefs["@pov"] == ["Jane"]
assert theRefs["@char"] == ["Jane"]
##
# getBackReferenceList
##
# getBackReferenceList
# ====================
# None handle should return an empty dict
assert theIndex.getBackReferenceList(None) == {}
@@ -548,16 +529,15 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
theRefs = theIndex.getBackReferenceList(cHandle)
assert theRefs == {nHandle: "T000001"}
##
# getTagSource
##
# getTagSource
# ============
assert theIndex.getTagSource("Jane") == (cHandle, 2, "T000001")
assert theIndex.getTagSource("John") == (None, 0, "T000000")
##
# getCounts for whole text and sections
##
# getCounts
# =========
# For whole text and sections
# Get section counts for a novel file
assert theIndex.scanText(nHandle, (
@@ -586,7 +566,7 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert wC == 12
assert pC == 2
# First part
# Second part
cC, wC, pC = theIndex.getCounts(nHandle, "T000011")
assert cC == 90
assert wC == 16
@@ -619,15 +599,14 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
assert wC == 12
assert pC == 2
# First part
# Second part
cC, wC, pC = theIndex.getCounts(cHandle, "T000011")
assert cC == 90
assert wC == 16
assert pC == 2
##
# Novel Stats
##
# Novel Stats
# ===========
hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c")
sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c")
@@ -696,3 +675,503 @@ def testCoreIndex_ExtractData(nwMinimal, dummyGUI):
]
# END Test testCoreIndex_ExtractData
@pytest.mark.core
def testCoreIndex_CheckTagIndex(dummyGUI):
"""Test the tag index checker.
"""
theProject = NWProject(dummyGUI)
theIndex = NWIndex(theProject, dummyGUI)
# Valid Index
theIndex._tagIndex = {
"John": [3, "14298de4d9524", "CHARACTER", "T000001"],
"Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001"],
}
assert theIndex._checkTagIndex() is None
# Wrong Key Type
theIndex._tagIndex = {
"John": [3, "14298de4d9524", "CHARACTER", "T000001"],
123456: [3, "bb2c23b3c42cc", "CHARACTER", "T000001"],
}
with pytest.raises(KeyError):
theIndex._checkTagIndex()
# Wrong Length
theIndex._tagIndex = {
"John": [3, "14298de4d9524", "CHARACTER", "T000001"],
"Jane": [3, "bb2c23b3c42cc", "CHARACTER", "T000001", "Stuff"],
}
with pytest.raises(IndexError):
theIndex._checkTagIndex()
# Wrong Type of Entry 0
theIndex._tagIndex = {
"John": [3, "14298de4d9524", "CHARACTER", "T000001"],
"Jane": ["3", "bb2c23b3c42cc", "CHARACTER", "T000001"],
}
with pytest.raises(ValueError):
theIndex._checkTagIndex()
# Wrong Type of Entry 1
theIndex._tagIndex = {
"John": [3, "14298de4d9524", "CHARACTER", "T000001"],
"Jane": [3, 0xbb2c23b3c42cc, "CHARACTER", "T000001"],
}
with pytest.raises(ValueError):
theIndex._checkTagIndex()
# Wrong Type of Entry 2
theIndex._tagIndex = {
"John": [3, "14298de4d9524", "CHARACTER", "T000001"],
"Jane": [3, "bb2c23b3c42cc", "INVALID_CLASS", "T000001"],
}
with pytest.raises(ValueError):
theIndex._checkTagIndex()
# Wrong Type of Entry 3
theIndex._tagIndex = {
"John": [3, "14298de4d9524", "CHARACTER", "T000001"],
"Jane": [3, "bb2c23b3c42cc", "CHARACTER", "INVALID"],
}
with pytest.raises(ValueError):
theIndex._checkTagIndex()
# END Test testCoreIndex_CheckTagIndex
@pytest.mark.core
def testCoreIndex_CheckRefIndex(dummyGUI):
"""Test the reference index checker.
"""
theProject = NWProject(dummyGUI)
theIndex = NWIndex(theProject, dummyGUI)
# Valid Index
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": [
[3, "@pov", "Jane"], [4, "@location", "Earth"]
], "updated": 1611922868}
}
}
assert theIndex._checkRefIndex() is None
# Invalid Handle
theIndex._refIndex = {
"Ha2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": [
[3, "@pov", "Jane"], [4, "@location", "Earth"]
], "updated": 1611922868}
}
}
with pytest.raises(KeyError):
theIndex._checkRefIndex()
# Invalid Title
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"INVALID": {"tags": [
[3, "@pov", "Jane"], [4, "@location", "Earth"]
], "updated": 1611922868}
}
}
with pytest.raises(KeyError):
theIndex._checkRefIndex()
# Missing 'tags'
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"updated": 1611922868}
}
}
with pytest.raises(KeyError):
theIndex._checkRefIndex()
# Wrong Length of 'tags'
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": [
[3, "@pov", "Jane"], [4, "@location", "Earth", "Stuff"]
], "updated": 1611922868}
}
}
with pytest.raises(IndexError):
theIndex._checkRefIndex()
# Wrong Type of 'tags' Entry 0
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": [
[3, "@pov", "Jane"], ["4", "@location", "Earth"]
], "updated": 1611922868}
}
}
with pytest.raises(ValueError):
theIndex._checkRefIndex()
# Wrong Type of 'tags' Entry 1
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": [
[3, "@pov", "Jane"], [4, "@stuff", "Earth"]
], "updated": 1611922868}
}
}
with pytest.raises(ValueError):
theIndex._checkRefIndex()
# Wrong Type of 'tags' Entry 1
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": [
[3, "@pov", "Jane"], [4, "@location", 123456]
], "updated": 1611922868}
}
}
with pytest.raises(ValueError):
theIndex._checkRefIndex()
# Missing 'updated'
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": []}
}
}
with pytest.raises(KeyError):
theIndex._checkRefIndex()
# Wrong Type of 'updated' Entry 1
theIndex._refIndex = {
"6a2d6d5f4f401": {
"T000000": {"tags": [], "updated": 1611922868},
"T000001": {"tags": [], "updated": "1611922868"}
}
}
with pytest.raises(ValueError):
theIndex._checkRefIndex()
# END Test testCoreIndex_CheckRefIndex
@pytest.mark.core
def testCoreIndex_CheckNovelNoteIndex(dummyGUI):
"""Test the novel and note index checkers.
"""
theProject = NWProject(dummyGUI)
theIndex = NWIndex(theProject, dummyGUI)
# Valid Index
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
theIndex._noteIndex = theIndex._novelIndex.copy()
assert theIndex._checkNovelNoteIndex("novelIndex") is None
assert theIndex._checkNovelNoteIndex("noteIndex") is None
with pytest.raises(IndexError):
theIndex._checkNovelNoteIndex("notAnIndex")
# Invalid Handle
theIndex._novelIndex = {
"H3b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Invalid Title
theIndex._novelIndex = {
"53b69b83cdafc": {
"INVALID": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Length
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868, "stuff": None
}
}
}
with pytest.raises(IndexError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing Keys
# ============
# Missing 'level'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"stuff": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing 'title'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "stuff": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing 'layout'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "stuff": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing 'synopsis'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "stuff": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing 'cCount'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"stuff": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing 'wCount'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "stuff": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing 'pCount'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "stuff": 2, "updated": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Missing 'updated'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "stuff": 1611922868
}
}
}
with pytest.raises(KeyError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Types
# ===========
# Wrong Type for 'level'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "XX", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Type for 'title'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": 12345678, "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Type for 'layout'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "INVALID", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Type for 'synopsis'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": 123456,
"cCount": 72, "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Type for 'cCount'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": "72", "wCount": 15, "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Type for 'wCount'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": "15", "pCount": 2, "updated": 1611922868
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Type for 'pCount'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": "2", "updated": 1611922868
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# Wrong Type for 'updated'
theIndex._novelIndex = {
"53b69b83cdafc": {
"T000001": {
"level": "H1", "title": "My Novel", "layout": "TITLE", "synopsis": "text",
"cCount": 72, "wCount": 15, "pCount": 2, "updated": "1611922868"
}
}
}
with pytest.raises(ValueError):
theIndex._checkNovelNoteIndex("novelIndex")
# END Test testCoreIndex_CheckNovelNoteIndex
@pytest.mark.core
def testCoreIndex_CheckTextCounts(dummyGUI):
"""Test the text counts checker.
"""
theProject = NWProject(dummyGUI)
theIndex = NWIndex(theProject, dummyGUI)
# Valid Index
theIndex._textCounts = {
"53b69b83cdafc": [72, 15, 2],
"974e400180a99": [210, 40, 2],
}
assert theIndex._checkTextCounts() is None
# Invalid Handle
theIndex._textCounts = {
"53b69b83cdafc": [72, 15, 2],
"h74e400180a99": [210, 40, 2],
}
with pytest.raises(KeyError):
theIndex._checkTextCounts()
# Wrong Length
theIndex._textCounts = {
"53b69b83cdafc": [72, 15, 2],
"974e400180a99": [210, 40, 2, 8],
}
with pytest.raises(IndexError):
theIndex._checkTextCounts()
# Type of Entry 0
theIndex._textCounts = {
"53b69b83cdafc": [72, 15, 2],
"974e400180a99": ["210", 40, 2],
}
with pytest.raises(ValueError):
theIndex._checkTextCounts()
# Type of Entry 1
theIndex._textCounts = {
"53b69b83cdafc": [72, 15, 2],
"974e400180a99": [210, "40", 2],
}
with pytest.raises(ValueError):
theIndex._checkTextCounts()
# Type of Entry 2
theIndex._textCounts = {
"53b69b83cdafc": [72, 15, 2],
"974e400180a99": [210, 40, "2"],
}
with pytest.raises(ValueError):
theIndex._checkTextCounts()
# END Test testCoreIndex_CheckTextCounts
+5 -5
View File
@@ -859,12 +859,12 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir):
# Session stats
monkeypatch.setattr("os.path.isdir", lambda *args, **kwargs: False)
assert not theProject._appendSessionStats()
assert not theProject._appendSessionStats(idleTime=0)
monkeypatch.undo()
# Block open
monkeypatch.setattr("builtins.open", causeOSError)
assert not theProject._appendSessionStats()
assert not theProject._appendSessionStats(idleTime=0)
monkeypatch.undo()
# Write entry
@@ -876,13 +876,13 @@ def testCoreProject_Methods(monkeypatch, nwMinimal, dummyGUI, tmpDir):
theProject.notesWCount = 100
monkeypatch.setattr("nw.core.project.time", lambda: 1600005600)
assert theProject._appendSessionStats()
assert theProject._appendSessionStats(idleTime=99)
monkeypatch.undo()
assert readFile(statsFile) == (
"# Offset 100\n"
"# Start Time End Time Novel Notes\n"
"%s %s 200 100\n"
"# Start Time End Time Novel Notes Idle\n"
"%s %s 200 100 99\n"
) % (formatTimeStamp(1600002000), formatTimeStamp(1600005600))
# Pack XML Value
+129 -40
View File
@@ -20,8 +20,11 @@ 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 os
import pytest
from tools import readFile
from nw.core import NWProject, NWIndex, ToHtml
@pytest.mark.core
@@ -44,14 +47,12 @@ def testCoreToHtml_Format(dummyGUI):
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>\n"
"<span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<div>"
"<span class='tags'>Characters:</span> "
"<a href='#tag_Bod'>Bod</a>, "
"<a href='#tag_Jane'>Jane</a>"
"</div>\n"
)
# Preview Mode
@@ -68,14 +69,12 @@ def testCoreToHtml_Format(dummyGUI):
assert theHtml._formatKeywords("") == ""
assert theHtml._formatKeywords("tag: Jane") == (
"<div><span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a></div>\n"
"<span class='tags'>Tag:</span> <a name='tag_Jane'>Jane</a>"
)
assert theHtml._formatKeywords("char: Bod, Jane") == (
"<div>"
"<span class='tags'>Characters:</span> "
"<a href='#char=Bod'>Bod</a>, "
"<a href='#char=Jane'>Jane</a>"
"</div>\n"
)
# END Test testCoreToHtml_Format
@@ -200,8 +199,27 @@ def testCoreToHtml_Convert(dummyGUI):
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<div><span class='tags'>Characters:</span> "
"<a href='#tag_Bod'>Bod</a>, <a href='#tag_Jane'>Jane</a></div>\n"
"<p><span class='tags'>Characters:</span> "
"<a href='#tag_Bod'>Bod</a>, <a href='#tag_Jane'>Jane</a></p>\n"
)
# Multiple Keywords
theHtml.setKeywords(True)
theHtml.theText = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<h2>"
"<a name='T000001'></a>Chapter</h2>\n"
"<p style='margin-bottom: 0;'>"
"<span class='tags'>Point of View:</span> <a href='#tag_Bod'>Bod</a>"
"</p>\n"
"<p style='margin-bottom: 0; margin-top: 0;'>"
"<span class='tags'>Plot:</span> <a href='#tag_Main'>Main</a>"
"</p>\n"
"<p style='margin-top: 0;'>"
"<span class='tags'>Locations:</span> <a href='#tag_Europe'>Europe</a>"
"</p>\n"
)
# Direct Tests
@@ -211,12 +229,12 @@ def testCoreToHtml_Convert(dummyGUI):
# Title
theHtml.theTokens = [
(theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_CENTRE),
(theHtml.T_TITLE, 1, "A Title", None, theHtml.A_PBB_AUT | theHtml.A_CENTRE),
(theHtml.T_EMPTY, 1, "", None, theHtml.A_NONE),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' style='text-align: center; page-break-before: never;'>"
"<h1 class='title' style='text-align: center; page-break-before: auto;'>"
"<a name='T000001'></a>A Title</h1>\n"
)
@@ -299,24 +317,14 @@ def testCoreToHtml_Convert(dummyGUI):
"style='page-break-before: always; page-break-after: always;'>A Title</h1>\n"
)
# Page Break Avoid
# Page Break Auto
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AV | theHtml.A_PBA_AV),
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_AUT | theHtml.A_PBA_AUT),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' "
"style='page-break-before: avoid; page-break-after: avoid;'>A Title</h1>\n"
)
# Page Break ANever
theHtml.theTokens = [
(theHtml.T_HEAD1, 1, "A Title", None, theHtml.A_PBB_NO | theHtml.A_PBA_NO),
]
theHtml.doConvert()
assert theHtml.theResult == (
"<h1 class='title' "
"style='page-break-before: never; page-break-after: never;'>A Title</h1>\n"
"style='page-break-before: auto; page-break-after: auto;'>A Title</h1>\n"
)
# Preview Mode
@@ -336,54 +344,135 @@ def testCoreToHtml_Convert(dummyGUI):
# END Test testCoreToHtml_Convert
def testCoreToHtml_Complex(dummyGUI, fncDir):
"""Test the ave method of the ToHtml class.
"""
theProject = NWProject(dummyGUI)
theHtml = ToHtml(theProject, dummyGUI)
# Build Project
# =============
docText = [
"# My Novel\n**By Jane Doh**\n",
"## Chapter 1\n\nThe text of chapter one.\n",
"### Scene 1\n\nThe text of scene one.\n",
"#### A Section\n\nMore text in scene one.\n",
"## Chapter 2\n\nThe text of chapter two.\n",
"### Scene 2\n\nThe text of scene two.\n",
"#### A Section\n\n\tMore text in scene two.\n",
]
resText = [
"<h1>My Novel</h1>\n<p><strong>By Jane Doh</strong></p>\n",
"<h2>Chapter 1</h2>\n<p>The text of chapter one.</p>\n",
"<h3>Scene 1</h3>\n<p>The text of scene one.</p>\n",
"<h4>A Section</h4>\n<p>More text in scene one.</p>\n",
"<h2>Chapter 2</h2>\n<p>The text of chapter two.</p>\n",
"<h3>Scene 2</h3>\n<p>The text of scene two.</p>\n",
"<h4>A Section</h4>\n<p>\tMore text in scene two.</p>\n",
]
for i in range(len(docText)):
theHtml.theText = docText[i]
theHtml.doPreProcessing()
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == resText[i]
assert theHtml.fullHTML == resText
theHtml.replaceTabs(nSpaces=2, spaceChar="&nbsp;")
resText[6] = "<h4>A Section</h4>\n<p>&nbsp;&nbsp;More text in scene two.</p>\n"
# Check File
# ==========
theStyle = theHtml.getStyleSheet()
theStyle.append("article {width: 800px; margin: 40px auto;}")
htmlDoc = (
"<!DOCTYPE html>\n"
"<html>\n"
"<head>\n"
"<meta charset='utf-8'>\n"
"<title></title>\n"
"</head>\n"
"<style>\n"
"{htmlStyle:s}\n"
"</style>\n"
"<body>\n"
"<article>\n"
"{bodyText:s}\n"
"</article>\n"
"</body>\n"
"</html>\n"
).format(
htmlStyle = "\n".join(theStyle),
bodyText = "".join(resText).rstrip()
)
saveFile = os.path.join(fncDir, "outFile.htm")
theHtml.saveHTML5(saveFile)
assert readFile(saveFile) == htmlDoc
# END Test testCoreToHtml_Save
@pytest.mark.core
def testCoreToHtml_Methods(dummyGUI):
"""Test all the other methods of the ToHtml class.
"""
theProject = NWProject(dummyGUI)
theHtml = ToHtml(theProject, dummyGUI)
theHtml.setKeepMarkdown(True)
# Auto-Replace
# Auto-Replace, keep Unicode
docText = "Text with <brackets> & shortdash, long—dash …\n"
theHtml.theText = docText
theHtml.doAutoReplace()
theHtml.setReplaceUnicode(False)
theHtml.doPreProcessing()
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Text with &lt;brackets&gt; &amp; shortdash, long—dash …</p>\n"
)
# Auto-Replace, replace Unicode
docText = "Text with <brackets> & shortdash, long—dash …\n"
theHtml.theText = docText
theHtml.setReplaceUnicode(True)
theHtml.doPreProcessing()
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theResult == (
"<p>Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;</p>\n"
)
# Revert on MD
assert theHtml.theMarkdown == (
"Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;\n\n"
)
theHtml.doPostProcessing()
assert theHtml.theMarkdown == docText + "\n"
# With Preview, No Revert
# With Preview
theHtml.setPreview(True, True)
theHtml.theText = docText
theHtml.doAutoReplace()
theHtml.doPreProcessing()
theHtml.tokenizeText()
theHtml.doConvert()
assert theHtml.theMarkdown == (
assert theHtml.theMarkdown[-1] == (
"Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;\n\n"
)
theHtml.doPostProcessing()
assert theHtml.theMarkdown == (
assert theHtml.theMarkdown[-1] == (
"Text with &lt;brackets&gt; &amp; short&ndash;dash, long&mdash;dash &hellip;\n\n"
)
# Result Size
assert theHtml.getFullResultSize() == 147
# CSS
# ===
assert len(theHtml.getStyleSheet()) > 1
assert "p {text-align: left;}" in theHtml.getStyleSheet()
assert "p {text-align: justify;}" not in theHtml.getStyleSheet()
assert "p {text-align: left;" in " ".join(theHtml.getStyleSheet())
assert "p {text-align: justify;" not in " ".join(theHtml.getStyleSheet())
theHtml.setJustify(True)
assert "p {text-align: left;}" not in theHtml.getStyleSheet()
assert "p {text-align: justify;}" in theHtml.getStyleSheet()
assert "p {text-align: left;" not in " ".join(theHtml.getStyleSheet())
assert "p {text-align: justify;" in " ".join(theHtml.getStyleSheet())
theHtml.setStyles(False)
assert theHtml.getStyleSheet() == []
+78 -35
View File
@@ -20,8 +20,11 @@ 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 os
import pytest
from tools import readFile
from nw.core import NWProject, NWDoc
from nw.core.tokenizer import Tokenizer
@@ -38,6 +41,18 @@ def testCoreToken_Setters(dummyGUI):
assert theToken.fmtUnNum == "%title%"
assert theToken.fmtScene == "%title%"
assert theToken.fmtSection == "%title%"
assert theToken.textFont == "Serif"
assert theToken.textSize == 11
assert theToken.textFixed is False
assert theToken.lineHeight == 1.15
assert theToken.doJustify is False
assert theToken.marginTitle == (1.000, 0.500)
assert theToken.marginHead1 == (1.000, 0.500)
assert theToken.marginHead2 == (0.834, 0.500)
assert theToken.marginHead3 == (0.584, 0.500)
assert theToken.marginHead4 == (0.584, 0.500)
assert theToken.marginText == (0.000, 0.584)
assert theToken.marginMeta == (0.000, 0.584)
assert theToken.hideScene is False
assert theToken.hideSection is False
assert theToken.linkHeaders is False
@@ -45,7 +60,6 @@ def testCoreToken_Setters(dummyGUI):
assert theToken.doSynopsis is False
assert theToken.doComments is False
assert theToken.doKeywords is False
assert theToken.doJustify is False
# Set new values
theToken.setTitleFormat("T: %title%")
@@ -53,12 +67,21 @@ def testCoreToken_Setters(dummyGUI):
theToken.setUnNumberedFormat("U: %title%")
theToken.setSceneFormat("S: %title%", True)
theToken.setSectionFormat("X: %title%", True)
theToken.setFont("Monospace", 10, True)
theToken.setLineHeight(2)
theToken.setJustify(True)
theToken.setTitleMargins(2.0, 2.0)
theToken.setHead1Margins(2.0, 2.0)
theToken.setHead2Margins(2.0, 2.0)
theToken.setHead3Margins(2.0, 2.0)
theToken.setHead4Margins(2.0, 2.0)
theToken.setTextMargins(2.0, 2.0)
theToken.setMetaMargins(2.0, 2.0)
theToken.setLinkHeaders(True)
theToken.setBodyText(False)
theToken.setSynopsis(True)
theToken.setComments(True)
theToken.setKeywords(True)
theToken.setJustify(True)
# Check new values
assert theToken.fmtTitle == "T: %title%"
@@ -66,6 +89,18 @@ def testCoreToken_Setters(dummyGUI):
assert theToken.fmtUnNum == "U: %title%"
assert theToken.fmtScene == "S: %title%"
assert theToken.fmtSection == "X: %title%"
assert theToken.textFont == "Monospace"
assert theToken.textSize == 10
assert theToken.textFixed is True
assert theToken.lineHeight == 2.0
assert theToken.doJustify is True
assert theToken.marginTitle == (2.0, 2.0)
assert theToken.marginHead1 == (2.0, 2.0)
assert theToken.marginHead2 == (2.0, 2.0)
assert theToken.marginHead3 == (2.0, 2.0)
assert theToken.marginHead4 == (2.0, 2.0)
assert theToken.marginText == (2.0, 2.0)
assert theToken.marginMeta == (2.0, 2.0)
assert theToken.hideScene is True
assert theToken.hideSection is True
assert theToken.linkHeaders is True
@@ -73,7 +108,6 @@ def testCoreToken_Setters(dummyGUI):
assert theToken.doSynopsis is True
assert theToken.doComments is True
assert theToken.doKeywords is True
assert theToken.doJustify is True
# END Test testCoreToken_Setters
@@ -84,6 +118,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
theProject = NWProject(dummyGUI)
theProject.projTree.setSeed(42)
theToken = Tokenizer(theProject, dummyGUI)
theToken.setKeepMarkdown(True)
assert theProject.openProject(nwMinimal)
sHandle = "8c659a11cd429"
@@ -112,7 +147,7 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
assert theToken.addRootHeading("dummy") is False
assert theToken.addRootHeading(sHandle) is False
assert theToken.addRootHeading("7695ce551d265") is True
assert theToken.theMarkdown == "# Notes: Plot\n\n"
assert theToken.theMarkdown[-1] == "# Notes: Plot\n\n"
# Set text
assert theToken.setText("dummy") is False
@@ -141,21 +176,20 @@ def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
assert theToken.isNote is False
assert theToken.isNovel is True
# Auto replace
theToken.doAutoReplace()
# Pre Processing
theToken.doPreProcessing()
assert theToken.theText == docTextR
# Access
assert theToken.getResult() is None
assert theToken.getResultSize() == 0
theToken.theResult = ""
assert theToken.getResultSize() == 0
# Post Processing
theToken.theResult = r"This is text with escapes: \** \~~ \__"
theToken.doPostProcessing()
assert theToken.theResult == "This is text with escapes: ** ~~ __"
# Save File
savePath = os.path.join(nwMinimal, "dump.nwd")
theToken.saveRawMarkdown(savePath)
assert readFile(savePath) == "# Notes: Plot\n\n"
# END Test testCoreToken_TextOps
@pytest.mark.core
@@ -164,6 +198,7 @@ def testCoreToken_Tokenize(dummyGUI):
"""
theProject = NWProject(dummyGUI)
theToken = Tokenizer(theProject, dummyGUI)
theToken.setKeepMarkdown(True)
# Header 1
theToken.theText = "# Novel Title\n"
@@ -172,7 +207,7 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_HEAD1, 1, "Novel Title", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "# Novel Title\n\n"
assert theToken.theMarkdown[-1] == "# Novel Title\n\n"
# Header 2
theToken.theText = "## Chapter One\n"
@@ -181,7 +216,7 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_HEAD2, 1, "Chapter One", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "## Chapter One\n\n"
assert theToken.theMarkdown[-1] == "## Chapter One\n\n"
# Header 3
theToken.theText = "### Scene One\n"
@@ -190,7 +225,7 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_HEAD3, 1, "Scene One", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "### Scene One\n\n"
assert theToken.theMarkdown[-1] == "### Scene One\n\n"
# Header 4
theToken.theText = "#### A Section\n"
@@ -199,7 +234,7 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_HEAD4, 1, "A Section", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "#### A Section\n\n"
assert theToken.theMarkdown[-1] == "#### A Section\n\n"
# Comment
theToken.theText = "% A comment\n"
@@ -208,11 +243,11 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_COMMENT, 1, "A comment", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n"
assert theToken.theMarkdown[-1] == "\n"
theToken.setComments(True)
theToken.tokenizeText()
assert theToken.theMarkdown == "% A comment\n\n"
assert theToken.theMarkdown[-1] == "% A comment\n\n"
# Symopsis
theToken.theText = "%synopsis: The synopsis\n"
@@ -227,11 +262,11 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_SYNOPSIS, 1, "The synopsis", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n"
assert theToken.theMarkdown[-1] == "\n"
theToken.setSynopsis(True)
theToken.tokenizeText()
assert theToken.theMarkdown == "% synopsis: The synopsis\n\n"
assert theToken.theMarkdown[-1] == "% synopsis: The synopsis\n\n"
# Keyword
theToken.theText = "@char: Bod\n"
@@ -240,11 +275,24 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_KEYWORD, 1, "char: Bod", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n"
assert theToken.theMarkdown[-1] == "\n"
theToken.setKeywords(True)
theToken.tokenizeText()
assert theToken.theMarkdown == "@char: Bod\n\n"
assert theToken.theMarkdown[-1] == "@char: Bod\n\n"
theToken.theText = "@pov: Bod\n@plot: Main\n@location: Europe\n"
theToken.tokenizeText()
styTop = Tokenizer.A_NONE | Tokenizer.A_Z_BTMMRG
styMid = Tokenizer.A_NONE | Tokenizer.A_Z_BTMMRG | Tokenizer.A_Z_TOPMRG
styBtm = Tokenizer.A_NONE | Tokenizer.A_Z_TOPMRG
assert theToken.theTokens == [
(Tokenizer.T_KEYWORD, 1, "pov: Bod", None, styTop),
(Tokenizer.T_KEYWORD, 2, "plot: Main", None, styMid),
(Tokenizer.T_KEYWORD, 3, "location: Europe", None, styBtm),
(Tokenizer.T_EMPTY, 3, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown[-1] == "@pov: Bod\n@plot: Main\n@location: Europe\n\n"
# Text
theToken.theText = "Some plain text\non two lines\n\n\n"
@@ -256,7 +304,7 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some plain text\non two lines\n\n\n\n"
assert theToken.theMarkdown[-1] == "Some plain text\non two lines\n\n\n\n"
theToken.setBodyText(False)
theToken.tokenizeText()
@@ -265,7 +313,7 @@ def testCoreToken_Tokenize(dummyGUI):
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
(Tokenizer.T_EMPTY, 4, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "\n\n\n"
assert theToken.theMarkdown[-1] == "\n\n\n"
theToken.setBodyText(True)
# Text Emphasis
@@ -283,7 +331,7 @@ def testCoreToken_Tokenize(dummyGUI):
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some **bolded text** on this lines\n\n"
assert theToken.theMarkdown[-1] == "Some **bolded text** on this lines\n\n"
theToken.theText = "Some _italic text_ on this lines\n"
theToken.tokenizeText()
@@ -299,7 +347,7 @@ def testCoreToken_Tokenize(dummyGUI):
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some _italic text_ on this lines\n\n"
assert theToken.theMarkdown[-1] == "Some _italic text_ on this lines\n\n"
theToken.theText = "Some **_bold italic text_** on this lines\n"
theToken.tokenizeText()
@@ -317,7 +365,7 @@ def testCoreToken_Tokenize(dummyGUI):
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some **_bold italic text_** on this lines\n\n"
assert theToken.theMarkdown[-1] == "Some **_bold italic text_** on this lines\n\n"
theToken.theText = "Some ~~strikethrough text~~ on this lines\n"
theToken.tokenizeText()
@@ -333,7 +381,7 @@ def testCoreToken_Tokenize(dummyGUI):
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == "Some ~~strikethrough text~~ on this lines\n\n"
assert theToken.theMarkdown[-1] == "Some ~~strikethrough text~~ on this lines\n\n"
theToken.theText = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
theToken.tokenizeText()
@@ -353,12 +401,7 @@ def testCoreToken_Tokenize(dummyGUI):
),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_NONE),
]
assert theToken.theMarkdown == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
)
# Check the markdown function as well
assert theToken.getFilteredMarkdown() == (
assert theToken.theMarkdown[-1] == (
"Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n\n"
)
@@ -623,7 +666,7 @@ def testCoreToken_Headers(dummyGUI):
theToken.isPart = False
theToken.doHeaders()
assert theToken.theTokens == [
(Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_NO | Tokenizer.A_CENTRE),
(Tokenizer.T_TITLE, 1, "Novel Title", None, Tokenizer.A_PBB_AUT | Tokenizer.A_CENTRE),
(Tokenizer.T_EMPTY, 1, "", None, Tokenizer.A_PBA | Tokenizer.A_CENTRE),
]
+127
View File
@@ -0,0 +1,127 @@
# -*- coding: utf-8 -*-
"""
novelWriter ToOdt Class Tester
=================================
This file is a part of novelWriter
Copyright 20182021, 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 nw
import pytest
from lxml import etree
from nw.core import NWProject, NWIndex, ToOdt
XML_NS = [
' xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"',
' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"',
' xmlns:loext="urn:org:documentfoundation:names:experimental:office:xmlns:loext:1.0"',
' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"',
' xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0"',
' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"',
]
def xmlToText(xElem):
"""Get the text content of an XML element.
"""
rTxt = etree.tostring(xElem, encoding="utf-8", xml_declaration=False).decode()
for nSpace in XML_NS:
rTxt = rTxt.replace(nSpace, "")
return rTxt
@pytest.mark.core
def testCoreToOdt_Convert(tmpConf, dummyGUI):
"""Test the converter of the ToHtml class.
"""
nw.CONFIG = tmpConf
theProject = NWProject(dummyGUI)
dummyGUI.theIndex = NWIndex(theProject, dummyGUI)
theDoc = ToOdt(theProject, dummyGUI, isFlat=True)
# Export Mode
# ===========
theDoc.isNovel = True
# Header 1
theDoc.theText = "# Title\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
assert xmlToText(theDoc._xText) == (
'<office:text>'
'<text:h text:style-name="Heading_1" text:outline-level="1">Title</text:h>'
'</office:text>'
)
# Header 1
theDoc.theText = "## Chapter Title\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
assert xmlToText(theDoc._xText) == (
'<office:text>'
'<text:h text:style-name="Heading_2" text:outline-level="2">Chapter Title</text:h>'
'</office:text>'
)
# Nested Text
theDoc.theText = "Some ~~nested **bold** and _italics_ text~~ text.\nNo format\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
assert xmlToText(theDoc._xText) == (
'<office:text>'
'<text:p text:style-name="Text_Body">Some '
'<text:span text:style-name="T1">nested </text:span>'
'<text:span text:style-name="T2">bold</text:span>'
'<text:span text:style-name="T1"> and </text:span>'
'<text:span text:style-name="T3">italics</text:span>'
'<text:span text:style-name="T1"> text</text:span> text. No format</text:p>'
'</office:text>'
)
# Hard Break
theDoc.theText = "Some text. \nNext line\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
assert xmlToText(theDoc._xText) == (
'<office:text>'
'<text:p text:style-name="Text_Body">Some text.<text:line-break/>Next line</text:p>'
'</office:text>'
)
# Tab
theDoc.theText = "\tItem 1\tItem 2\n"
theDoc.tokenizeText()
theDoc.initDocument()
theDoc.doConvert()
theDoc.closeDocument()
assert xmlToText(theDoc._xText) == (
'<office:text>'
'<text:p text:style-name="Text_Body"><text:tab/>Item 1<text:tab/>Item 2</text:p>'
'</office:text>'
)
# END Test testCoreToOdt_Convert
+139 -14
View File
@@ -123,9 +123,9 @@ def testCoreTree_BuildTree(dummyGUI, dummyItems):
assert not theTree.isTrashRoot("a000000000003")
aHandles = []
for tHandle, pHande, nwItem in dummyItems:
for tHandle, pHandle, nwItem in dummyItems:
aHandles.append(tHandle)
assert theTree.append(tHandle, pHande, nwItem)
assert theTree.append(tHandle, pHandle, nwItem)
assert theTree._treeChanged
@@ -202,13 +202,13 @@ def testCoreTree_BuildTree(dummyGUI, dummyItems):
@pytest.mark.core
def testCoreTree_Methods(dummyGUI, dummyItems):
"""Test building a project tree from a list of items.
"""Test bvarious class methods.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
@@ -256,6 +256,131 @@ def testCoreTree_Methods(dummyGUI, dummyItems):
# END Test testCoreTree_Methods
@pytest.mark.core
def testCoreTree_UpdateItemLayout(dummyGUI, dummyItems):
"""Test building a project tree from a list of items.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
# Check rejected items
assert not theTree.updateItemLayout("0000000000000", "H1") # Non-existent handle
assert not theTree.updateItemLayout("a000000000004", "H2") # Character file
assert not theTree.updateItemLayout("c000000000002", "H0") # Wrong header level
cHandle = "c000000000002"
# Check layouts we won't change
theTree[cHandle].setLayout(nwItemLayout.NO_LAYOUT)
assert not theTree.updateItemLayout("c000000000002", "H1")
theTree[cHandle].setLayout(nwItemLayout.TITLE)
assert not theTree.updateItemLayout("c000000000002", "H1")
theTree[cHandle].setLayout(nwItemLayout.PAGE)
assert not theTree.updateItemLayout("c000000000002", "H1")
theTree[cHandle].setLayout(nwItemLayout.NOTE)
assert not theTree.updateItemLayout("c000000000002", "H1")
# BOOK is also a layout we change to, but never from
theTree[cHandle].setLayout(nwItemLayout.BOOK)
assert not theTree.updateItemLayout("c000000000002", "H1")
# Test SCENE Changes
# ==================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert theTree.updateItemLayout("c000000000002", "H1")
assert theTree[cHandle].itemLayout == nwItemLayout.BOOK
# H2 -> CHAPTER
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert theTree.updateItemLayout("c000000000002", "H2")
assert theTree[cHandle].itemLayout == nwItemLayout.CHAPTER
# H3 -> No CHange
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert not theTree.updateItemLayout("c000000000002", "H3")
# H4 -> No CHange
theTree[cHandle].setLayout(nwItemLayout.SCENE)
assert not theTree.updateItemLayout("c000000000002", "H4")
# Test CHAPTER Changes
# ====================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert theTree.updateItemLayout("c000000000002", "H1")
assert theTree[cHandle].itemLayout == nwItemLayout.BOOK
# H2 -> No Change
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert not theTree.updateItemLayout("c000000000002", "H2")
# H3 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert theTree.updateItemLayout("c000000000002", "H3")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# H4 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.CHAPTER)
assert theTree.updateItemLayout("c000000000002", "H4")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# Test UNNUMBERED Changes
# =======================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert theTree.updateItemLayout("c000000000002", "H1")
assert theTree[cHandle].itemLayout == nwItemLayout.BOOK
# H2 -> No Change
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert not theTree.updateItemLayout("c000000000002", "H2")
# H3 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert theTree.updateItemLayout("c000000000002", "H3")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# H4 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.UNNUMBERED)
assert theTree.updateItemLayout("c000000000002", "H4")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# Test PARTITION Changes
# ======================
# H1 -> BOOK
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert not theTree.updateItemLayout("c000000000002", "H1")
# H2 -> No Change
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert theTree.updateItemLayout("c000000000002", "H2")
assert theTree[cHandle].itemLayout == nwItemLayout.CHAPTER
# H3 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert theTree.updateItemLayout("c000000000002", "H3")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# H4 -> SCENE
theTree[cHandle].setLayout(nwItemLayout.PARTITION)
assert theTree.updateItemLayout("c000000000002", "H4")
assert theTree[cHandle].itemLayout == nwItemLayout.SCENE
# END Test testCoreTree_UpdateItemLayout
@pytest.mark.core
def testCoreTree_MakeHandles(monkeypatch, dummyGUI):
"""Test generating item handles.
@@ -296,8 +421,8 @@ def testCoreTree_Stats(dummyGUI, dummyItems):
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
theTree._treeOrder.append("dummy")
@@ -323,9 +448,9 @@ def testCoreTree_Reorder(dummyGUI, dummyItems):
theTree = NWTree(theProject)
aHandle = []
for tHandle, pHande, nwItem in dummyItems:
for tHandle, pHandle, nwItem in dummyItems:
aHandle.append(tHandle)
theTree.append(tHandle, pHande, nwItem)
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
@@ -348,13 +473,13 @@ def testCoreTree_Reorder(dummyGUI, dummyItems):
@pytest.mark.core
def testCoreTree_XMLPackUnpack(dummyGUI, dummyItems):
"""Test changing tree order.
"""Test packing and unpacking the tree to and from XML.
"""
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
@@ -408,8 +533,8 @@ def testCoreTree_ToCFile(monkeypatch, dummyGUI, dummyItems, tmpDir):
theProject = NWProject(dummyGUI)
theTree = NWTree(theProject)
for tHandle, pHande, nwItem in dummyItems:
theTree.append(tHandle, pHande, nwItem)
for tHandle, pHandle, nwItem in dummyItems:
theTree.append(tHandle, pHandle, nwItem)
assert len(theTree) == len(dummyItems)
theTree._treeOrder.append("dummy")
+33 -8
View File
@@ -59,6 +59,9 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
nwBuild = getGuiItem("GuiBuildNovel")
assert isinstance(nwBuild, GuiBuildNovel)
nwBuild.textFont.setText("DejaVu Sans")
nwBuild.textSize.setValue(11)
# Default Settings
qtbot.mouseClick(nwBuild.buildNovel, Qt.LeftButton)
@@ -77,6 +80,20 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_MD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md")
testFile = os.path.join(outDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md")
compFile = os.path.join(refDir, "guiBuild_Tool_Step1_Lorem_Ipsum.md")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_GH)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md")
testFile = os.path.join(outDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md")
compFile = os.path.join(refDir, "guiBuild_Tool_Step1G_Lorem_Ipsum.md")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
# Change Title Formats and Flip Switches
nwBuild.fmtChapter.setText(r"Chapter %chw%: %title%")
qtbot.wait(stepDelay)
@@ -93,6 +110,8 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
qtbot.wait(stepDelay)
qtbot.mouseClick(nwBuild.includeKeywords, Qt.LeftButton)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwBuild.replaceUCode, Qt.LeftButton)
qtbot.wait(stepDelay)
qtbot.mouseClick(nwBuild.noteFiles, Qt.LeftButton)
qtbot.wait(stepDelay)
@@ -116,6 +135,13 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_MD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md")
testFile = os.path.join(outDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md")
compFile = os.path.join(refDir, "guiBuild_Tool_Step2_Lorem_Ipsum.md")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
# Replace Tabs with Spaces
qtbot.mouseClick(nwBuild.replaceTabs, Qt.LeftButton)
qtbot.wait(stepDelay)
@@ -137,6 +163,13 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
assert nwBuild._saveDocument(nwBuild.FMT_MD)
projFile = os.path.join(nwLipsum, "Lorem Ipsum.md")
testFile = os.path.join(outDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md")
compFile = os.path.join(refDir, "guiBuild_Tool_Step3_Lorem_Ipsum.md")
copyfile(projFile, testFile)
assert cmpFiles(testFile, compFile)
# Putline Mode
nwBuild.fmtChapter.setText(r"Chapter %chw%: %title%")
qtbot.wait(stepDelay)
@@ -196,16 +229,9 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
assert nwBuild._saveDocument(nwBuild.FMT_PDF)
assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.pdf"))
assert nwBuild._saveDocument(nwBuild.FMT_MD)
assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.md"))
assert nwBuild._saveDocument(nwBuild.FMT_TXT)
assert os.path.isfile(os.path.join(nwLipsum, "Lorem Ipsum.txt"))
# Close the build tool
htmlText = nwBuild.htmlText
htmlStyle = nwBuild.htmlStyle
nwdText = nwBuild.nwdText
buildTime = nwBuild.buildTime
nwBuild._doClose()
@@ -219,7 +245,6 @@ def testGuiBuild_Tool(qtbot, monkeypatch, nwGUI, nwLipsum, refDir, outDir):
assert nwBuild.viewCachedDoc()
assert nwBuild.htmlText == htmlText
assert nwBuild.htmlStyle == htmlStyle
assert nwBuild.nwdText == nwdText
assert nwBuild.buildTime == buildTime
nwBuild._doClose()
+1 -1
View File
@@ -61,7 +61,7 @@ def testGuiDialogs_Quotes(qtbot, monkeypatch, nwGUI, nwMinimal):
# END Test testDialogs_Quotes
@pytest.mark.gui
def testGuiDialogs_Other(qtbot, monkeypatch, nwGUI, nwMinimal, tmpDir):
def testGuiDialogs_Other(qtbot, monkeypatch, nwGUI, tmpDir):
"""Various other dialog tests.
"""
monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: tmpDir)
+8 -1
View File
@@ -28,8 +28,10 @@ from tools import cmpFiles
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import QAction, QMessageBox
from PyQt5.QtWidgets import QAction, QMessageBox, QDialog
from nw.gui.itemeditor import GuiItemEditor
from nw.gui.doceditor import GuiDocEditor
from nw.gui.projtree import GuiProjectTree
from nw.constants import nwItemType, nwDocAction
@@ -43,7 +45,11 @@ def testGuiEditor_Main(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir, outDi
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(GuiItemEditor, "exec_", lambda *args: None)
monkeypatch.setattr(GuiItemEditor, "result", lambda *args: QDialog.Accepted)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True)
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True)
# Create new, save, close project
nwGUI.theProject.projTree.setSeed(42)
@@ -353,6 +359,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, nwLipsum):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True)
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.openProject(nwLipsum)
+1
View File
@@ -38,6 +38,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes)
# Open project
nwGUI.theProject.projTree.setSeed(42)
+2 -1
View File
@@ -28,7 +28,7 @@ from tools import cmpFiles, getGuiItem
from PyQt5.QtWidgets import QAction, QMessageBox
from nw.gui import GuiItemEditor
from nw.gui import GuiItemEditor, GuiProjectTree
from nw.constants import nwItemLayout
keyDelay = 2
@@ -45,6 +45,7 @@ def testGuiItemEditor_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, refDir,
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True)
# Create new, save, open project
nwGUI.theProject.projTree.setSeed(42)
+6
View File
@@ -27,6 +27,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextCursor, QTextBlock
from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
from nw.gui.doceditor import GuiDocEditor
from nw.constants import nwUnicode, nwDocAction, nwDocInsert, nwKeyWords
keyDelay = 2
@@ -39,6 +40,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, nwLipsum):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(GuiDocEditor, "hasFocus", lambda *args: True)
# Test Document Action with No Project
assert not nwGUI.docEditor.docAction(nwDocAction.COPY)
@@ -498,6 +500,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
nwGUI.mainMenu.mInsKWItems[nwKeyWords.POV_KEY][0].activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.POV_KEY
nwGUI.docEditor.setText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.FOCUS_KEY][0].activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.FOCUS_KEY
nwGUI.docEditor.setText("Stuff")
nwGUI.mainMenu.mInsKWItems[nwKeyWords.CHAR_KEY][0].activate(QAction.Trigger)
assert nwGUI.docEditor.getText() == "Stuff\n%s: " % nwKeyWords.CHAR_KEY
+1
View File
@@ -37,6 +37,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, nwLipsum):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes)
assert nwGUI.openProject(nwLipsum)
nwGUI.mainConf.lastPath = nwLipsum
+1
View File
@@ -48,6 +48,7 @@ def testGuiProjSettings_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj, outDi
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes)
# Check that we cannot open when there is no project
nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger)
+14 -2
View File
@@ -38,6 +38,9 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "warning", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(GuiMain, "editItem", lambda *args: None)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True)
@@ -96,12 +99,12 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
nwTree.setSelectedHandle("8c659a11cd429")
# Shift focus and try to move item
monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: None)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: False)
assert not nwTree.moveTreeItem(1)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
]
monkeypatch.setattr("PyQt5.QtWidgets.qApp.focusWidget", lambda: nwTree)
monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *args: True)
# Move second item up twice (should give same result)
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
@@ -131,6 +134,15 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move up twice, and undo
nwTree._lastMove = {}
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
assert nwTree.getTreeFromHandle("a6d311a93600a") == [
"a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
]
# Move a root item (top level items are different) twice
nwTree.flushTreeOrder()
assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
+1
View File
@@ -46,6 +46,7 @@ def testGuiProjectWizard_Main(qtbot, monkeypatch, nwGUI, nwMinimal):
"""
# Block message box
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes)
if sys.platform.startswith("darwin"):
# Disable for macOS because the test segfaults on QWizard.show()
+133
View File
@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""
novelWriter Other Dialog Classes Tester
=========================================
This file is a part of novelWriter
Copyright 20182021, 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 os
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QMessageBox, QAction
from tools import writeFile, readFile, getGuiItem
from dummy import causeOSError
from nw.gui.wordlist import GuiWordList
from nw.constants import nwFiles
keyDelay = 2
typeDelay = 1
stepDelay = 20
def testGuiWordList_Dialog(qtbot, monkeypatch, nwGUI, nwMinimal, tmpDir):
"""test the word list editor.
"""
monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes)
monkeypatch.setattr(GuiWordList, "exec_", lambda *args: None)
monkeypatch.setattr(GuiWordList, "result", lambda *args: QDialog.Accepted)
monkeypatch.setattr(GuiWordList, "accept", lambda *args: None)
# Open project
nwGUI.openProject(nwMinimal)
qtbot.wait(stepDelay)
dictFile = os.path.join(nwMinimal, "meta", nwFiles.PROJ_DICT)
# Load the dialog
nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger)
qtbot.waitUntil(lambda: getGuiItem("GuiWordList") is not None, timeout=1000)
wList = getGuiItem("GuiWordList")
assert isinstance(wList, GuiWordList)
wList.show()
qtbot.wait(stepDelay)
# List should be blank
assert wList.listBox.count() == 0
# Add words
writeFile(dictFile, (
"word_a\n"
"word_c\n"
"word_g\n"
" \n" # Should be ignored
"word_f\n"
"word_b\n"
))
qtbot.wait(stepDelay)
assert wList._loadWordList()
# Check that the content was loaded
assert wList.listBox.item(0).text() == "word_a"
assert wList.listBox.item(1).text() == "word_b"
assert wList.listBox.item(2).text() == "word_c"
assert wList.listBox.item(3).text() == "word_f"
assert wList.listBox.item(4).text() == "word_g"
# Add a blank word
wList.newEntry.setText(" ")
assert not wList._doAdd()
# Add an existing word
wList.newEntry.setText("word_c")
assert not wList._doAdd()
# Add a new word
wList.newEntry.setText("word_d")
assert wList._doAdd()
# Check that the content now
assert wList.listBox.item(0).text() == "word_a"
assert wList.listBox.item(1).text() == "word_b"
assert wList.listBox.item(2).text() == "word_c"
assert wList.listBox.item(3).text() == "word_d"
assert wList.listBox.item(4).text() == "word_f"
assert wList.listBox.item(5).text() == "word_g"
# Delete a word
wList.newEntry.setText("delete_me")
assert wList._doAdd()
assert wList.listBox.item(0).text() == "delete_me"
delItem = wList.listBox.findItems("delete_me", Qt.MatchExactly)[0]
assert delItem.text() == "delete_me"
delItem.setSelected(True)
wList._doDelete()
assert wList.listBox.findItems("delete_me", Qt.MatchExactly) == []
assert wList.listBox.item(0).text() == "word_a"
# Save files
assert wList._doSave()
assert readFile(dictFile) == (
"word_a\n"
"word_b\n"
"word_c\n"
"word_d\n"
"word_f\n"
"word_g\n"
)
# Save again and make it fail
monkeypatch.setattr("builtins.open", causeOSError)
assert not wList._doSave()
# qtbot.stopForInteraction()
wList._doClose()
# END Test testGuiWordList_Dialog
+60 -60
View File
@@ -72,7 +72,7 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
# Make a test log file
writeFile(sessFile, (
"# Offset 123\n"
"# Start Time End Time Novel Notes\n"
"# Start Time End Time Novel Notes Idle\n"
"2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n"
"2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n"
"2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n"
@@ -86,8 +86,8 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
# Make sure a faulty file can still be read
writeFile(sessFile, (
"# Offset abc123\n"
"# Start Time End Time Novel Notes\n"
"2020-01-01 21:00:00 2020-01-01 21:00:05 6 0\n"
"# Start Time End Time Novel Notes Idle\n"
"2020-01-01 21:00:00 2020-01-01 21:00:05 6 0 50\n"
"2020-01-03 21:00:00 2020-01-03 21:00:15 125 0\n"
"2020-01-03 21:30:00 2020-01-03 21:30:15 125 5\n"
"2020-01-06 21:00:00 2020-01-06 21:00:10 125\n"
@@ -101,17 +101,17 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
writeFile(sessFile, (
"# Offset 1075\n"
"# Start Time End Time Novel Notes\n"
"2021-01-31 19:00:00 2021-01-31 19:30:00 700 375\n"
"2021-02-01 19:00:00 2021-02-01 19:30:00 700 375\n"
"2021-02-01 20:00:00 2021-02-01 20:30:00 600 275\n"
"2021-02-02 19:00:00 2021-02-02 19:30:00 750 425\n"
"2021-02-02 20:00:00 2021-02-02 20:30:00 690 365\n"
"2021-02-03 19:00:00 2021-02-03 19:30:00 680 355\n"
"2021-02-04 19:00:00 2021-02-04 19:30:00 700 375\n"
"2021-02-05 19:00:00 2021-02-05 19:30:00 500 175\n"
"2021-02-06 19:00:00 2021-02-06 19:30:00 600 275\n"
"2021-02-07 19:00:00 2021-02-07 19:30:00 600 275\n"
"# Start Time End Time Novel Notes Idle\n"
"2021-01-31 19:00:00 2021-01-31 19:30:00 700 375 0\n"
"2021-02-01 19:00:00 2021-02-01 19:30:00 700 375 10\n"
"2021-02-01 20:00:00 2021-02-01 20:30:00 600 275 20\n"
"2021-02-02 19:00:00 2021-02-02 19:30:00 750 425 30\n"
"2021-02-02 20:00:00 2021-02-02 20:30:00 690 365 40\n"
"2021-02-03 19:00:00 2021-02-03 19:30:00 680 355 50\n"
"2021-02-04 19:00:00 2021-02-04 19:30:00 700 375 60\n"
"2021-02-05 19:00:00 2021-02-05 19:30:00 500 175 70\n"
"2021-02-06 19:00:00 2021-02-06 19:30:00 600 275 80\n"
"2021-02-07 19:00:00 2021-02-07 19:30:00 600 275 90\n"
))
sessLog.populateGUI()
@@ -156,28 +156,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 600, "noteWords": 275
"newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 300, "novelWords": 750, "noteWords": 425
"newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -120, "novelWords": 690, "noteWords": 365
"newWords": -120, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -20, "novelWords": 680, "noteWords": 355
"newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -400, "novelWords": 500, "noteWords": 175
"newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -206,28 +206,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -100, "novelWords": 600, "noteWords": 275
"newWords": -100, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 150, "novelWords": 750, "noteWords": 425
"newWords": 150, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -60, "novelWords": 690, "noteWords": 365
"newWords": -60, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -10, "novelWords": 680, "noteWords": 355
"newWords": -10, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 20, "novelWords": 700, "noteWords": 375
"newWords": 20, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 500, "noteWords": 175
"newWords": -200, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 100, "novelWords": 600, "noteWords": 275
"newWords": 100, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -254,28 +254,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -100, "novelWords": 600, "noteWords": 275
"newWords": -100, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 150, "novelWords": 750, "noteWords": 425
"newWords": 150, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -60, "novelWords": 690, "noteWords": 365
"newWords": -60, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -10, "novelWords": 680, "noteWords": 355
"newWords": -10, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 20, "novelWords": 700, "noteWords": 375
"newWords": 20, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 500, "noteWords": 175
"newWords": -200, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 100, "novelWords": 600, "noteWords": 275
"newWords": 100, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -300,16 +300,16 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 300, "novelWords": 750, "noteWords": 425
"newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80
}
]
@@ -338,34 +338,34 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31 19:00:00", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01 19:00:00", "length": 1800.0,
"newWords": 0, "novelWords": 700, "noteWords": 375
"newWords": 0, "novelWords": 700, "noteWords": 375, "idleTime": 10
}, {
"date": "2021-02-01 20:00:00", "length": 1800.0,
"newWords": -200, "novelWords": 600, "noteWords": 275
"newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 20
}, {
"date": "2021-02-02 19:00:00", "length": 1800.0,
"newWords": 300, "novelWords": 750, "noteWords": 425
"newWords": 300, "novelWords": 750, "noteWords": 425, "idleTime": 30
}, {
"date": "2021-02-02 20:00:00", "length": 1800.0,
"newWords": -120, "novelWords": 690, "noteWords": 365
"newWords": -120, "novelWords": 690, "noteWords": 365, "idleTime": 40
}, {
"date": "2021-02-03 19:00:00", "length": 1800.0,
"newWords": -20, "novelWords": 680, "noteWords": 355
"newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04 19:00:00", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05 19:00:00", "length": 1800.0,
"newWords": -400, "novelWords": 500, "noteWords": 175
"newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06 19:00:00", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80
}, {
"date": "2021-02-07 19:00:00", "length": 1800.0,
"newWords": 0, "novelWords": 600, "noteWords": 275
"newWords": 0, "novelWords": 600, "noteWords": 275, "idleTime": 90
}
]
@@ -391,28 +391,28 @@ def testGuiWritingStats_Dialog(qtbot, monkeypatch, nwGUI, fncDir, fncProj):
assert jsonData == [
{
"date": "2021-01-31", "length": 1800.0,
"newWords": 1, "novelWords": 700, "noteWords": 375
"newWords": 1, "novelWords": 700, "noteWords": 375, "idleTime": 0
}, {
"date": "2021-02-01", "length": 3600.0,
"newWords": -200, "novelWords": 600, "noteWords": 275
"newWords": -200, "novelWords": 600, "noteWords": 275, "idleTime": 30
}, {
"date": "2021-02-02", "length": 3600.0,
"newWords": 180, "novelWords": 690, "noteWords": 365
"newWords": 180, "novelWords": 690, "noteWords": 365, "idleTime": 70
}, {
"date": "2021-02-03", "length": 1800.0,
"newWords": -20, "novelWords": 680, "noteWords": 355
"newWords": -20, "novelWords": 680, "noteWords": 355, "idleTime": 50
}, {
"date": "2021-02-04", "length": 1800.0,
"newWords": 40, "novelWords": 700, "noteWords": 375
"newWords": 40, "novelWords": 700, "noteWords": 375, "idleTime": 60
}, {
"date": "2021-02-05", "length": 1800.0,
"newWords": -400, "novelWords": 500, "noteWords": 175
"newWords": -400, "novelWords": 500, "noteWords": 175, "idleTime": 70
}, {
"date": "2021-02-06", "length": 1800.0,
"newWords": 200, "novelWords": 600, "noteWords": 275
"newWords": 200, "novelWords": 600, "noteWords": 275, "idleTime": 80
}, {
"date": "2021-02-07", "length": 1800.0,
"newWords": 0, "novelWords": 600, "noteWords": 275
"newWords": 0, "novelWords": 600, "noteWords": 275, "idleTime": 90
}
]