Merge pull request #55 from vkbo/project_export

Project Export
This commit is contained in:
Veronica K. Berglyd Olsen
2019-10-19 13:46:19 +02:00
committed by GitHub
18 changed files with 1245 additions and 56 deletions
+1
View File
@@ -19,6 +19,7 @@ class nwFiles():
PROJ_DICT = "wordlist.txt"
SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json"
EXPORT_OPT = "exportOptions.json"
# END Class nwFiles
+179
View File
@@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
"""novelWriter Text File
novelWriter Text File
=========================
Writes the project to a plain text file
File History:
Created: 2019-10-18 [0.2.3]
"""
import logging
import nw
from os import path
from PyQt5.QtWidgets import QMessageBox
from nw.convert.tokenizer import Tokenizer
from nw.enum import nwAlert, nwItemLayout
logger = logging.getLogger(__name__)
class TextFile():
def __init__(self, theProject, theParent):
self.mainConf = nw.CONFIG
self.theProject = theProject
self.theParent = theParent
self.fileExt = "txt"
self.outFile = None
self.fileName = ""
self.theText = ""
self.expNovel = True
self.expNotes = False
self.winEnding = False
self.theConv = Tokenizer(self.theProject, self.theParent)
self.makeAlert = self.theParent.makeAlert
self.setComments(False)
self.setMeta(False)
self.setWordWrap(80)
return
##
# Setters
##
def setExportNovel(self, doNovel):
self.expNovel = doNovel
return
def setExportNotes(self, doNotes):
self.expNotes = doNotes
return
def setComments(self, doComments):
self.theConv.setComments(doComments)
return
def setMeta(self, doMeta):
self.theConv.setCommands(doMeta)
return
def setWordWrap(self, wordWrap):
if wordWrap >= 0:
self.theConv.setWordWrap(wordWrap)
else:
self.theConv.setWordWrap(0)
return
def setTitleFormat(self, fmtTitle):
self.theConv.setTitleFormat(fmtTitle)
return
def setChapterFormat(self, fmtChapter):
self.theConv.setChapterFormat(fmtChapter)
return
def setUnNumberedFormat(self, fmtUnNum):
self.theConv.setUnNumberedFormat(fmtUnNum)
return
def setSceneFormat(self, fmtScene):
self.theConv.setSceneFormat(fmtScene)
return
def setSectionFormat(self, fmtSection):
self.theConv.setSectionFormat(fmtSection)
return
##
# Core Methods
##
def openFile(self, filePath):
self.fileName = path.basename(filePath)
if path.isfile(filePath) and self.mainConf.showGUI:
msgBox = QMessageBox()
msgRes = msgBox.question(
self.theParent, "Overwrite", ("File '%s' already exists.<br>Do you want to overwrite it?" % self.fileName)
)
if msgRes != QMessageBox.Yes:
return False
self._doOpenFile(filePath)
if self.outFile is None:
return False
return True
def closeFile(self):
self._doCloseFile()
return True
def addText(self, tHandle):
logger.verbose("Parsing content of item '%s'" % tHandle)
theItem = self.theProject.getItem(tHandle)
isNone = theItem.itemLayout == nwItemLayout.NO_LAYOUT
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
if isNone:
return False
if isNote and not self.expNotes:
return False
if isNovel and not self.expNovel:
return False
self.theConv.setText(tHandle)
self.theConv.doAutoReplace()
self.theConv.tokenizeText()
self.theConv.doHeaders()
self.theConv.doConvert()
if self.winEnding:
self.theConv.windowsEndings()
if self.theConv.theResult is not None and self.outFile is not None:
self.outFile.write(self.theConv.theResult)
return True
##
# Internal Functions
##
def _doOpenFile(self, filePath):
"""This function does the actual opening of the file, and can be overloaded by a subclass
that uses a different file format that requires a different approach.
"""
try:
self.outFile = open(filePath,mode="w+")
if self.winEnding:
self.outFile.write("\r\n\r\n")
else:
self.outFile.write("\n\n")
except Exception as e:
self.makeAlert(["Failed to open file.",str(e)], nwAlert.ERROR)
return False
return True
def _doCloseFile(self):
"""This function closes a file, and is meant to be overloaded by the subclass for other
file formats.
"""
if self.outFile is not None:
self.outFile.close()
return True
# END Class OutFile
+7 -7
View File
@@ -51,21 +51,21 @@ class ToHtml(Tokenizer):
self.theResult = ""
thisPar = []
for tType, tText, tFormat in self.theTokens:
for tType, tText, tFormat, tAlign in self.theTokens:
if tType == "empty":
if tType == self.T_EMPTY:
if len(thisPar) > 0:
self.theResult += "<p>%s</p>\n" % " ".join(thisPar)
thisPar = []
elif tType == "header1":
elif tType == self.T_HEAD1:
self.theResult += "<h1>%s</h1>\n" % tText
elif tType == "header2":
elif tType == self.T_HEAD2:
self.theResult += "<h2>%s</h2>\n" % tText
elif tType == "header3":
elif tType == self.T_HEAD3:
self.theResult += "<h3>%s</h3>\n" % tText
elif tType == "header4":
elif tType == self.T_HEAD4:
self.theResult += "<h4>%s</h4>\n" % tText
elif tType == "text":
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+htmlTags[xFmt]+tTemp[xPos+xLen:]
+290 -16
View File
@@ -10,24 +10,43 @@
"""
import textwrap
import logging
import re
import nw
from operator import itemgetter
from PyQt5.QtCore import QRegularExpression
from nw.project.document import NWDoc
from nw.tools.translate import numberToWord
from nw.enum import nwItemLayout
logger = logging.getLogger(__name__)
class Tokenizer():
FMT_B_B = 1 # Begin Bold
FMT_B_E = 2 # End Bold
FMT_I_B = 3 # Begin Italics
FMT_I_E = 4 # End Italics
FMT_U_B = 5 # Begin Underline
FMT_U_E = 6 # End Underline
FMT_B_B = "" # Begin bold
FMT_B_E = "" # End bold
FMT_I_B = "" # Begin italics
FMT_I_E = "" # End italics
FMT_U_B = "" # Begin underline
FMT_U_E = "" # End underline
T_EMPTY = 1 # Empty line (new paragraph)
T_COMMENT = 2 # Comment line
T_COMMAND = 3 # Command line
T_HEAD1 = 4 # Header 1 (title)
T_HEAD2 = 5 # Header 2 (chapter)
T_HEAD3 = 6 # Header 3 (scene)
T_HEAD4 = 7 # Header 4
T_TEXT = 8 # Text line
T_SEP = 9 # Scene separator
A_LEFT = 1 # Left aligned
A_RIGHT = 2 # Right aligned
A_CENTRE = 3 # Centred
A_JUSTIFY = 4 # Justified
def __init__(self, theProject, theParent):
@@ -41,8 +60,66 @@ class Tokenizer():
self.theTokens = None
self.theResult = None
self.wordWrap = 80
self.doComments = False
self.doCommands = False
self.fmtTitle = "%title%"
self.fmtChapter = "Chapter %numword%: %title%"
self.fmtUnNum = "%title%"
self.fmtScene = "* * *"
self.fmtSection = "%title%"
self.noSection = True
self.numChapter = 0
self.firstScene = False
return
##
# Setters
##
def setComments(self, doComments):
self.doComments = doComments
return
def setCommands(self, doCommands):
self.doCommands = doCommands
return
def setWordWrap(self, wordWrap):
if wordWrap >= 0:
self.wordWrap = wordWrap
else:
self.wordWrap = 0
return
def setTitleFormat(self, fmtTitle):
self.fmtTitle = fmtTitle
return
def setChapterFormat(self, fmtChapter):
self.fmtChapter = fmtChapter
return
def setUnNumberedFormat(self, fmtUnNum):
self.fmtUnNum = fmtUnNum
return
def setSceneFormat(self, fmtScene):
self.fmtScene = fmtScene
return
def setSectionFormat(self, fmtSection):
self.fmtSection = fmtSection
return
##
# Class Methods
##
def setText(self, theHandle, theText=None):
self.theHandle = theHandle
@@ -93,19 +170,19 @@ class Tokenizer():
# Tag lines starting with specific characters
if len(aLine) == 0:
self.theTokens.append(("empty","",None))
self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT))
elif aLine[0] == "%":
self.theTokens.append(("comment",aLine[1:].strip(),None))
self.theTokens.append((self.T_COMMENT,aLine[1:].strip(),None,self.A_LEFT))
elif aLine[0] == "@":
self.theTokens.append(("command",aLine[1:].strip(),None))
self.theTokens.append((self.T_COMMAND,aLine[1:].strip(),None,self.A_LEFT))
elif aLine[:2] == "# ":
self.theTokens.append(("header1",aLine[2:].strip(),None))
self.theTokens.append((self.T_HEAD1,aLine[2:].strip(),None,self.A_LEFT))
elif aLine[:3] == "## ":
self.theTokens.append(("header2",aLine[3:].strip(),None))
self.theTokens.append((self.T_HEAD2,aLine[3:].strip(),None,self.A_LEFT))
elif aLine[:4] == "### ":
self.theTokens.append(("header3",aLine[4:].strip(),None))
self.theTokens.append((self.T_HEAD3,aLine[4:].strip(),None,self.A_LEFT))
elif aLine[:5] == "#### ":
self.theTokens.append(("header4",aLine[5:].strip(),None))
self.theTokens.append((self.T_HEAD4,aLine[5:].strip(),None,self.A_LEFT))
else:
# Otherwise we use RegEx to find formatting tags within a line of text
fmtPos = []
@@ -121,12 +198,209 @@ class Tokenizer():
# Save the line as is, but append the array of formatting locations sorted by position
fmtPos = sorted(fmtPos,key=itemgetter(0))
self.theTokens.append(("text",aLine,fmtPos))
self.theTokens.append((self.T_TEXT,aLine,fmtPos,self.A_LEFT))
# Always add an empty line at the end
self.theTokens.append(("empty","",None))
# print(self.theTokens)
self.theTokens.append((self.T_EMPTY,"",None,self.A_LEFT))
return
def doHeaders(self):
isNone = self.theItem.itemLayout == nwItemLayout.NO_LAYOUT
isTitle = self.theItem.itemLayout == nwItemLayout.TITLE
isBook = self.theItem.itemLayout == nwItemLayout.BOOK
isPage = self.theItem.itemLayout == nwItemLayout.PAGE
isPart = self.theItem.itemLayout == nwItemLayout.PARTITION
isUnNum = self.theItem.itemLayout == nwItemLayout.UNNUMBERED
isChap = self.theItem.itemLayout == nwItemLayout.CHAPTER
isScene = self.theItem.itemLayout == nwItemLayout.SCENE
isNote = self.theItem.itemLayout == nwItemLayout.NOTE
# No special header formatting for notes and no layout files
if isNone: return
if isNote: return
# For novel files, we need to handle chapter numbering and scene breaks
if isBook or isUnNum or isChap or isScene:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
if tType == self.T_TEXT:
self.firstScene = False
elif tType == self.T_HEAD2:
if not isUnNum:
self.numChapter += 1
tText = self._doFormatChapter(tText,isUnNum)
self.theTokens[n] = (tType,tText,None,self.A_LEFT)
self.firstScene = True
elif tType == self.T_HEAD3:
tTemp = self._doFormatScene(tText)
if tTemp == "":
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
elif tTemp == self.fmtScene:
if self.firstScene:
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
else:
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_LEFT)
else:
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
self.firstScene = False
elif tType == self.T_HEAD4:
tTemp = self._doFormatSection(tText)
if tTemp == "":
self.theTokens[n] = (self.T_EMPTY,"",None,self.A_LEFT)
elif tTemp == self.fmtSection:
self.theTokens[n] = (self.T_SEP,tTemp,None,self.A_LEFT)
else:
self.theTokens[n] = (tType,tTemp,None,self.A_LEFT)
# For title page and partitions, we need to centre all text
if isTitle or isPart:
for n in range(len(self.theTokens)):
tToken = self.theTokens[n]
tType = tToken[0]
tText = tToken[1]
tFormat = tToken[2]
self.theTokens[n] = (tType,tText,tFormat,self.A_CENTRE)
return
def doConvert(self):
"""Converts the tokenized text into plain text.
"""
if self.wordWrap > 0:
tWrap = textwrap.TextWrapper(
width = self.wordWrap,
initial_indent = "",
subsequent_indent = "",
expand_tabs = True,
replace_whitespace = True,
fix_sentence_endings = False,
break_long_words = True,
drop_whitespace = True,
break_on_hyphens = True,
tabsize = 8,
max_lines = None
)
self.theResult = ""
thisPar = []
for tType, tText, tFormat, tAlign in self.theTokens:
# First check if we have a comment or plain text, as they need some
# extra replacing before we proceed to wrapping and final formatting.
if tType == self.T_COMMENT:
tText = "[%s]" % tText
elif tType == self.T_TEXT:
tTemp = tText
for xPos, xLen, xFmt in reversed(tFormat):
tTemp = tTemp[:xPos]+tTemp[xPos+xLen:]
tText = tTemp
tLen = len(tText)
# The text can now be word wrapped, if we have requested this and it's needed.
if tAlign == self.A_CENTRE:
if self.wordWrap > 0:
if tLen > self.wordWrap:
aText = tWrap.wrap(tText)
for n in range(len(aText)):
aText[n] = self._centreText(aText[n],self.wordWrap)
tText = "\n".join(aText)
else:
tText = self._centreText(tText,self.wordWrap)
else:
if self.wordWrap > 0 and tLen > self.wordWrap:
tText = tWrap.fill(tText)
# Then the text can receive final formatting before we append it to the results.
# We also store text lines in a buffer and merge them only when we find an empty line,
# indicating a new paragraph.
if tType == self.T_EMPTY:
if len(thisPar) > 0:
self.theResult += "%s\n\n" % " ".join(thisPar)
thisPar = []
elif tType == self.T_HEAD1:
uLine = "="*min(tLen,self.wordWrap)
if tAlign == self.A_CENTRE:
uLine = self._centreText(uLine,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD2:
uLine = "~"*min(tLen,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD3:
uLine = "-"*min(tLen,self.wordWrap)
self.theResult += "%s\n%s\n\n" % (tText,uLine)
elif tType == self.T_HEAD4:
self.theResult += "%s\n\n" % tText
elif tType == self.T_SEP:
if self.wordWrap > 0 and tLen < self.wordWrap:
tText = self._centreText(tText,self.wordWrap)
self.theResult += "%s\n\n" % tText
elif tType == self.T_TEXT:
thisPar.append(tText)
elif tType == self.T_COMMENT and self.doComments:
self.theResult += "%s\n\n" % tText
elif tType == self.T_COMMAND and self.doCommands:
self.theResult += "%s\n\n" % tText
return
def windowsEndings(self):
self.theResult = self.theResult.replace("\n","\r\n")
return
##
# Internal Functions
##
def _doFormatTitle(self, theText):
theTitle = self.fmtTitle
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _doFormatChapter(self, theText, noNum):
if noNum:
theTitle = self.fmtUnNum
theTitle = theTitle.replace("%title%", theText)
else:
theTitle = self.fmtChapter
theTitle = theTitle.replace("%title%", theText)
theTitle = theTitle.replace("%num%", str(self.numChapter))
theTitle = theTitle.replace("%numword%", numberToWord(self.numChapter,"en"))
return theTitle
def _doFormatScene(self, theText):
theTitle = self.fmtScene
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _doFormatSection(self, theText):
theTitle = self.fmtSection
theTitle = theTitle.replace("%title%", theText)
return theTitle
def _centreText(self, theText, theWidth):
tLen = len(theText)
if tLen < theWidth:
return " "*int((theWidth-tLen)/2) + theText
return theText
# END Class Tokenizer
+44
View File
@@ -0,0 +1,44 @@
<?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"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="18"
id="Layer_1"
version="1.2"
viewBox="0 0 19 18"
width="19"
xml:space="preserve"
sodipodi:docname="export.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata874"><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="defs872" /><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1080"
id="namedview870"
showgrid="false"
inkscape:zoom="33.875"
inkscape:cx="12.118081"
inkscape:cy="12"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" /><path
d="M 18.711,6.796 C 18.67,6.755 14.656,2.7 12.729,0.65 12.309,0.236 11.73,0 11.143,0 9.961,0 9,0.896 9,2 H 1 C 0.447,2 0,2.448 0,3 v 14 c 0,0.552 0.447,1 1,1 h 14 c 0.553,0 1,-0.448 1,-1 v -6.045 c 1.434,-1.461 2.688,-2.729 2.711,-2.751 0.387,-0.39 0.387,-1.018 0,-1.408 z m -7.432,6.145 C 11.257,12.968 11.214,13 11.143,13 11.069,13 11.022,12.977 10.999,12.96 V 10 9 h -1 C 8.228,9.034 6.663,9.68 5.246,10.958 5.676,8.743 6.846,6 9.999,6 h 1 V 2.042 C 11.027,2.021 11.07,2 11.143,2 c 0.09,0 0.152,0.05 0.154,0.05 1.436,1.525 4.051,4.187 5.297,5.45 -0.253,0.257 -4.342,4.422 -5.315,5.441 z M 2,16 V 4 h 8 c 0,0.348 0,0.695 0,1 C 5.34,5 4,9.871 4,13.5 V 14 c 1.691,-2.578 3.6,-3.953 6,-4 0,1.045 0,2.838 0,3 0,0.551 0.512,1 1.143,1 0.364,0 0.676,-0.158 0.883,-0.391 0.539,-0.565 1.242,-1.291 1.976,-2.043 V 16 Z"
id="path867"
style="fill:#000000;fill-opacity:0.35408559"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+3
View File
@@ -0,0 +1,3 @@
FROM ICON SET: Typicons
LICENSE: Creative Commons (Attribution-Share Alike 3.0 Unported)
https://creativecommons.org/licenses/by-sa/3.0/
+10 -5
View File
@@ -68,10 +68,14 @@ class GuiConfigEditor(QDialog):
self.show()
logger.debug("ProjectEditor ConfigEditor complete")
logger.debug("ConfigEditor initialisation complete")
return
##
# Buttons
##
def _doSave(self):
logger.verbose("ConfigEditor save button clicked")
@@ -111,10 +115,10 @@ class GuiConfigEditGeneral(QWidget):
def __init__(self, theParent):
QWidget.__init__(self, theParent)
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.outerBox = QGridLayout()
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theTheme = theParent.theTheme
self.outerBox = QGridLayout()
# User Interface
self.guiLook = QGroupBox("User Interface", self)
@@ -278,6 +282,7 @@ class GuiConfigEditGeneral(QWidget):
if newDir:
self.projBackupPath.setText(newDir)
return True
return False
# END Class GuiConfigEditGeneral
+1 -1
View File
@@ -54,7 +54,7 @@ class GuiDocDetails(QFrame):
QLabel("")
]
colOne = ["Name","Status","Class","Layout"]
colOne = ["Label","Status","Class","Layout"]
for nRow in range(4):
lblOne = QLabel(colOne[nRow])
lblOne.setFont(self.fntOne)
+507
View File
@@ -0,0 +1,507 @@
# -*- coding: utf-8 -*-
"""novelWriter GUI Export Tools
novelWriter GUI Export Tools
================================
Tool for exporting project files to other formats
File History:
Created: 2019-10-13 [0.2.3]
"""
import logging
import time
import json
import nw
from os import path
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import (
QDialog, QHBoxLayout, QVBoxLayout, QWidget, QTabWidget, QGridLayout, QGroupBox, QCheckBox,
QLabel, QComboBox, QLineEdit, QPushButton, QFileDialog, QProgressBar, QSpinBox
)
from nw.project.document import NWDoc
from nw.tools.translate import numberToWord
from nw.convert.textfile import TextFile
from nw.common import checkString, checkBool, checkInt
from nw.constants import nwFiles
from nw.enum import nwItemType
logger = logging.getLogger(__name__)
class GuiExport(QDialog):
def __init__(self, theParent, theProject):
QDialog.__init__(self, theParent)
logger.debug("Initialising GuiExport ...")
self.mainConf = nw.CONFIG
self.theParent = theParent
self.theProject = theProject
self.optState = ExportLastState(self.theProject)
self.outerBox = QHBoxLayout()
self.innerBox = QVBoxLayout()
self.setWindowTitle("Export Project")
self.setLayout(self.outerBox)
self.gradPath = path.abspath(path.join(self.mainConf.appPath,"graphics","export.svg"))
self.svgGradient = QSvgWidget(self.gradPath)
self.svgGradient.setFixedSize(QSize(64,64))
self.theProject.countStatus()
self.tabMain = GuiExportMain(self.theParent, self.theProject, self.optState)
self.tabWidget = QTabWidget()
self.tabWidget.addTab(self.tabMain, "Settings")
self.outerBox.addWidget(self.svgGradient, 0, Qt.AlignTop)
self.outerBox.addLayout(self.innerBox)
self.doExportForm = QGridLayout()
self.doExportForm.setContentsMargins(10,5,0,10)
self.exportButton = QPushButton("Export")
self.exportButton.clicked.connect(self._doExport)
self.closeButton = QPushButton("Close")
self.closeButton.clicked.connect(self._doClose)
self.exportStatus = QLabel("Ready ...")
self.exportProgress = QProgressBar(self)
self.doExportForm.addWidget(self.exportStatus, 0, 0, 1, 3)
self.doExportForm.addWidget(self.exportProgress, 1, 0)
self.doExportForm.addWidget(self.exportButton, 1, 1)
self.doExportForm.addWidget(self.closeButton, 1, 2)
self.innerBox.addWidget(self.tabWidget)
self.innerBox.addLayout(self.doExportForm)
self.rejected.connect(self._doClose)
self.show()
logger.debug("GuiExport initialisation complete")
return
##
# Buttons
##
def _doExport(self):
logger.verbose("GuiExport export button clicked")
wNovel = self.tabMain.expNovel.isChecked()
wNotes = self.tabMain.expNotes.isChecked()
eFormat = self.tabMain.outputFormat.currentData()
fixWidth = self.tabMain.fixedWidth.value()
wComments = self.tabMain.expComments.isChecked()
chFormat = self.tabMain.chapterFormat.text()
unFormat = self.tabMain.unnumFormat.text()
scFormat = self.tabMain.sceneFormat.text()
seFormat = self.tabMain.sectionFormat.text()
saveTo = self.tabMain.exportPath.text()
nItems = len(self.theProject.treeOrder)
self.exportProgress.setMinimum(0)
self.exportProgress.setMaximum(nItems)
self.exportProgress.setValue(0)
if not wNovel and not wNotes:
self.exportStatus.setText("Nothing to export ...")
return False
outFile = None
if eFormat == GuiExportMain.FMT_TXT:
outFile = TextFile(self.theProject, self.theParent)
if outFile is None:
return False
if outFile.openFile(saveTo):
outFile.setComments(wComments)
outFile.setExportNovel(wNovel)
outFile.setExportNotes(wNotes)
outFile.setWordWrap(fixWidth)
outFile.setChapterFormat(chFormat)
outFile.setUnNumberedFormat(unFormat)
outFile.setSceneFormat(scFormat)
outFile.setSectionFormat(seFormat)
else:
self.exportStatus.setText("Failed to open file for writing ...")
return False
nDone = 0
for tHandle in self.theProject.treeOrder:
time.sleep(0.1)
self.exportProgress.setValue(nDone)
tItem = self.theProject.getItem(tHandle)
self.exportStatus.setText("Exporting: %s" % tItem.itemName)
logger.verbose("Exporting: %s" % tItem.itemName)
if tItem is not None and tItem.itemType == nwItemType.FILE:
outFile.addText(tHandle)
nDone += 1
outFile.closeFile()
self.exportProgress.setValue(nDone)
self.exportStatus.setText("Export to %s complete" % outFile.fileName)
logger.verbose("Export to %s complete" % outFile.fileName)
return
def _doClose(self):
logger.verbose("GuiExport close button clicked")
wNovel = self.tabMain.expNovel.isChecked()
wNotes = self.tabMain.expNotes.isChecked()
eFormat = self.tabMain.outputFormat.currentData()
fixWidth = self.tabMain.fixedWidth.value()
wComments = self.tabMain.expComments.isChecked()
chFormat = self.tabMain.chapterFormat.text()
unFormat = self.tabMain.unnumFormat.text()
scFormat = self.tabMain.sceneFormat.text()
seFormat = self.tabMain.sectionFormat.text()
saveTo = self.tabMain.exportPath.text()
self.optState.setSetting("wNovel", wNovel)
self.optState.setSetting("wNotes", wNotes)
self.optState.setSetting("eFormat", eFormat)
self.optState.setSetting("fixWidth", fixWidth)
self.optState.setSetting("wComments",wComments)
self.optState.setSetting("chFormat", chFormat)
self.optState.setSetting("unFormat", unFormat)
self.optState.setSetting("scFormat", scFormat)
self.optState.setSetting("seFormat", seFormat)
self.optState.setSetting("saveTo", saveTo)
self.optState.saveSettings()
self.close()
return
# END Class GuiExport
class GuiExportMain(QWidget):
FMT_TXT = 1
FMT_MD = 2
FMT_HTML = 3
FMT_EBOOK = 4
FMT_ODT = 5
FMT_TEX = 6
FMT_EXT = {
FMT_TXT : ".txt",
FMT_MD : ".md",
FMT_HTML : ".htm",
FMT_EBOOK : ".htm",
FMT_ODT : ".odt",
FMT_TEX : ".tex",
}
FMT_HELP = {
FMT_TXT : (
"Exports a plain text file. "
"All formatting is stripped and comments are in square brackets."
),
FMT_MD : (
"Exports a standard markdown file. "
"Comments are converted to preformatted text blocks."
),
FMT_HTML : (
"Exports a plain html5 file. "
"Comments are converted to preformatted text blocks."
),
FMT_EBOOK : (
"Exports an html5 file that can be converted to eBook with Calibre. "
"Comments are not exported in this format."
),
FMT_ODT : (
"Exports an open document file that can be read by office applications. "
"Comments are exported as grey text."
),
FMT_TEX : (
"Exports a LaTeX file that can be compiled to PDF using for instance PDFLaTeX. "
"Comments are exported as LaTeX comments."
),
}
def __init__(self, theParent, theProject, optState):
QWidget.__init__(self, theParent)
self.theParent = theParent
self.theProject = theProject
self.theTheme = theParent.theTheme
self.outerBox = QGridLayout()
self.optState = optState
self.currFormat = self.FMT_TXT
# Select Files
self.guiFiles = QGroupBox("Selection", self)
self.guiFilesForm = QGridLayout(self)
self.guiFiles.setLayout(self.guiFilesForm)
self.expNovel = QCheckBox(self)
self.expNovel.setChecked(self.optState.getSetting("wNovel"))
self.expNovel.setToolTip("Include all novel files in the exported document")
self.expNotes = QCheckBox(self)
self.expNotes.setChecked(self.optState.getSetting("wNotes"))
self.expNotes.setToolTip("Include all note files in the exported document")
self.expComments = QCheckBox(self)
self.expComments.setChecked(self.optState.getSetting("wComments"))
self.expComments.setToolTip("Export comments from all files")
self.guiFilesForm.addWidget(QLabel("Novel files"), 0, 0)
self.guiFilesForm.addWidget(self.expNovel, 0, 1)
self.guiFilesForm.addWidget(QLabel("Note files"), 1, 0)
self.guiFilesForm.addWidget(self.expNotes, 1, 1)
self.guiFilesForm.addWidget(QLabel("Comments"), 2, 0)
self.guiFilesForm.addWidget(self.expComments, 2, 1)
# Chapter Settings
self.guiChapters = QGroupBox("Chapter Headings", self)
self.guiChaptersForm = QGridLayout(self)
self.guiChapters.setLayout(self.guiChaptersForm)
self.chapterFormat = QLineEdit()
self.chapterFormat.setText(self.optState.getSetting("chFormat"))
self.chapterFormat.setToolTip("Available formats: %num%, %numword%, %title%")
self.chapterFormat.setMinimumWidth(250)
self.unnumFormat = QLineEdit()
self.unnumFormat.setText(self.optState.getSetting("unFormat"))
self.unnumFormat.setToolTip("Available formats: %title%")
self.unnumFormat.setMinimumWidth(250)
self.guiChaptersForm.addWidget(QLabel("Numbered"), 0, 0)
self.guiChaptersForm.addWidget(self.chapterFormat, 0, 1)
self.guiChaptersForm.addWidget(QLabel("Unnumbered"), 1, 0)
self.guiChaptersForm.addWidget(self.unnumFormat, 1, 1)
# Scene and Section Settings
self.guiScenes = QGroupBox("Other Headings", self)
self.guiScenesForm = QGridLayout(self)
self.guiScenes.setLayout(self.guiScenesForm)
self.sceneFormat = QLineEdit()
self.sceneFormat.setText(self.optState.getSetting("scFormat"))
self.sceneFormat.setToolTip("Available formats: %title%")
self.sceneFormat.setMinimumWidth(100)
self.sectionFormat = QLineEdit()
self.sectionFormat.setText(self.optState.getSetting("seFormat"))
self.sectionFormat.setToolTip("Available formats: %title%")
self.sectionFormat.setMinimumWidth(100)
self.guiScenesForm.addWidget(QLabel("Scenes"), 0, 0)
self.guiScenesForm.addWidget(self.sceneFormat, 0, 1)
self.guiScenesForm.addWidget(QLabel("Sections"), 1, 0)
self.guiScenesForm.addWidget(self.sectionFormat, 1, 1)
# Output Path
self.exportTo = QGroupBox("Export Folder", self)
self.exportToForm = QGridLayout(self)
self.exportTo.setLayout(self.exportToForm)
self.exportPath = QLineEdit(self.optState.getSetting("saveTo"))
self.exportGetPath = QPushButton(self.theTheme.getIcon("folder"),"")
self.exportGetPath.clicked.connect(self._exportFolder)
self.exportToForm.addWidget(QLabel("Save to"), 0, 0)
self.exportToForm.addWidget(self.exportPath, 0, 1)
self.exportToForm.addWidget(self.exportGetPath, 0, 2)
# Output Format
self.guiOutput = QGroupBox("Export", self)
self.guiOutputForm = QGridLayout(self)
self.guiOutput.setLayout(self.guiOutputForm)
self.outputHelp = QLabel("")
self.outputHelp.setWordWrap(True)
self.outputHelp.setMinimumHeight(55)
self.outputHelp.setAlignment(Qt.AlignTop)
self.outputFormat = QComboBox(self)
self.outputFormat.addItem("Plain Text (.txt)", self.FMT_TXT)
# self.outputFormat.addItem("Markdown (.md)", self.FMT_MD)
# self.outputFormat.addItem("HTML5 (.htm)", self.FMT_HTML)
# self.outputFormat.addItem("HTML5 for eBook (.htm)", self.FMT_EBOOK)
# self.outputFormat.addItem("Open Document (.odt)", self.FMT_ODT)
# self.outputFormat.addItem("LaTeX for PDF (.tex)", self.FMT_TEX)
self.outputFormat.currentIndexChanged.connect(self._updateFormat)
optIdx = self.outputFormat.findData(self.optState.getSetting("eFormat"))
if optIdx != -1:
self.outputFormat.setCurrentIndex(optIdx)
self._updateFormat(optIdx)
self.guiOutputForm.addWidget(QLabel("Format"), 0, 0)
self.guiOutputForm.addWidget(self.outputFormat, 0, 1)
self.guiOutputForm.addWidget(self.outputHelp, 1, 0, 1, 3)
self.guiOutputForm.setColumnStretch(2, 1)
# Additional Settings
self.addSettings = QGroupBox("Additional Settings", self)
self.addSettingsForm = QGridLayout(self)
self.addSettings.setLayout(self.addSettingsForm)
self.fixedWidth = QSpinBox(self)
self.fixedWidth.setMinimum(0)
self.fixedWidth.setMaximum(999)
self.fixedWidth.setSingleStep(1)
self.fixedWidth.setValue(self.optState.getSetting("fixWidth"))
self.fixedWidth.setToolTip("0 disables the feature. Applies to .txt, .md and .tex files.")
self.addSettingsForm.addWidget(QLabel("Fixed width"), 0, 0)
self.addSettingsForm.addWidget(self.fixedWidth, 0, 1)
self.addSettingsForm.setColumnStretch(2, 1)
# Assemble
self.outerBox.addWidget(self.guiOutput, 0, 0, 1, 2)
self.outerBox.addWidget(self.guiFiles, 0, 2)
self.outerBox.addWidget(self.guiChapters, 1, 0, 1, 2)
self.outerBox.addWidget(self.guiScenes, 1, 2)
self.outerBox.addWidget(self.addSettings, 2, 0, 1, 3)
self.outerBox.addWidget(self.exportTo, 3, 0, 1, 3)
self.outerBox.setColumnStretch(0, 1)
self.outerBox.setColumnStretch(1, 1)
self.outerBox.setColumnStretch(2, 1)
self.setLayout(self.outerBox)
return
##
# Internal Functions
##
def _updateFormat(self, currIdx):
"""Update help text under output format selection and file extension in file box
"""
if currIdx == -1:
self.outputHelp.setText("")
else:
self.currFormat = self.outputFormat.itemData(currIdx)
self.outputHelp.setText("<i>%s</i>" % self.FMT_HELP[self.currFormat])
self._checkFileExtension()
return
def _exportFolder(self):
currDir = self.exportPath.text()
if not path.isdir(currDir):
currDir = ""
extFilter = [
"Text files (*.txt)",
"Markdown files (*.md)",
"HTML files (*.htm *.html)",
"Open document files (*.odt)",
"LaTeX files (*.tex)",
]
dlgOpt = QFileDialog.Options()
dlgOpt |= QFileDialog.ShowDirsOnly
dlgOpt |= QFileDialog.DontUseNativeDialog
saveTo = QFileDialog.getSaveFileName(
self,"Export File",self.exportPath.text(),options=dlgOpt,filter=";;".join(extFilter)
)
if saveTo:
self.exportPath.setText(saveTo[0])
self._checkFileExtension()
return True
return False
def _checkFileExtension(self):
saveTo = self.exportPath.text()
fileBits = path.splitext(saveTo)
if self.currFormat > 0:
saveTo = fileBits[0]+self.FMT_EXT[self.currFormat]
self.exportPath.setText(saveTo)
return
# END Class GuiExportMain
class ExportLastState():
def __init__(self, theProject):
self.theProject = theProject
self.theState = {
"wNovel" : True,
"wNotes" : False,
"eFormat" : 2,
"fixWidth" : 80,
"wComments" : False,
"chFormat" : "Chapter %numword%",
"unFormat" : "%title%",
"scFormat" : "* * *",
"seFormat" : "",
"saveTo" : "",
}
self.stringOpt = ("chFormat","unFormat","scFormat","seFormat","saveTo")
self.boolOpt = ("wNovel","wNotes","wComments")
self.intOpt = ("eFormat","fixWidth")
self.loadSettings()
return
def loadSettings(self):
stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT)
theState = {}
if path.isfile(stateFile):
logger.debug("Loading export options file")
try:
with open(stateFile,mode="r") as inFile:
theJson = inFile.read()
theState = json.loads(theJson)
except Exception as e:
logger.error("Failed to load export options file")
logger.error(str(e))
return False
for anOpt in theState:
self.theState[anOpt] = theState[anOpt]
return True
def saveSettings(self):
stateFile = path.join(self.theProject.projMeta, nwFiles.EXPORT_OPT)
logger.debug("Saving export options file")
try:
with open(stateFile,mode="w+") as outFile:
outFile.write(json.dumps(self.theState, indent=2))
except Exception as e:
logger.error("Failed to save export options file")
logger.error(str(e))
return False
return True
def setSetting(self, setName, setValue):
if setName in self.theState:
self.theState[setName] = setValue
else:
return False
return True
def getSetting(self, setName):
if setName in self.stringOpt:
return checkString(self.theState[setName],self.theState[setName],False)
elif setName in self.boolOpt:
return checkBool(self.theState[setName],self.theState[setName],False)
elif setName in self.intOpt:
return checkInt(self.theState[setName],self.theState[setName],False)
return None
# END Class ExportLastState
+7
View File
@@ -192,6 +192,13 @@ class GuiMainMenu(QMenuBar):
menuItem.triggered.connect(self.theParent.editProjectDialog)
self.projMenu.addAction(menuItem)
# Project > Export Project
menuItem = QAction("Export Project", self)
menuItem.setStatusTip("Export project")
menuItem.setShortcut("F5")
menuItem.triggered.connect(self.theParent.exportProjectDialog)
self.projMenu.addAction(menuItem)
# Project > Separator
self.projMenu.addSeparator()
+7
View File
@@ -30,6 +30,7 @@ from nw.gui.searchbar import GuiSearchBar
from nw.gui.mainmenu import GuiMainMenu
from nw.gui.configeditor import GuiConfigEditor
from nw.gui.projecteditor import GuiProjectEditor
from nw.gui.export import GuiExport
from nw.gui.itemeditor import GuiItemEditor
from nw.gui.statusbar import GuiMainStatus
from nw.gui.timelineview import GuiTimeLineView
@@ -511,6 +512,12 @@ class GuiMain(QMainWindow):
self._setWindowTitle(self.theProject.projName)
return True
def exportProjectDialog(self):
if self.hasProject:
dlgExport = GuiExport(self, self.theProject)
dlgExport.exec_()
return True
def showTimeLineDialog(self):
if self.hasProject:
dlgTLine = GuiTimeLineView(self, self.theProject, self.theIndex)
+93
View File
@@ -0,0 +1,93 @@
# -*- coding: utf-8 -*-
"""novelWriter Translate Tools
novelWriter Translate Tools
===============================
Various translate tools
File History:
Created: 2019-10-13 [0.2.3]
"""
import logging
import nw
from os import path
logger = logging.getLogger(__name__)
def numberToWord(numVal, theLanguage):
numWord = ""
if theLanguage == "en":
numWord = _numberToWordEN(numVal)
else:
numWord = _numberToWordEN(numVal)
# print("%4d : %s" % (numVal, numWord))
return numWord
def _numberToWordEN(numVal):
numWord = ""
oneWord = ""
tenWord = ""
hunWord = ""
if numVal == 0:
return "Zero"
oneVal = numVal % 10
tenVal = (numVal-oneVal) % 100
hunVal = (numVal-tenVal-oneVal) % 1000
if hunVal == 100: hunWord = "One Hundred"
if hunVal == 200: hunWord = "Two Hundred"
if hunVal == 300: hunWord = "Three Hundred"
if hunVal == 400: hunWord = "Four Hundred"
if hunVal == 500: hunWord = "Five Hundred"
if hunVal == 600: hunWord = "Six Hundred"
if hunVal == 700: hunWord = "Seven Hundred"
if hunVal == 800: hunWord = "Eight Hundred"
if hunVal == 900: hunWord = "Nine Hundred"
if tenVal == 20: tenWord = "Twenty"
if tenVal == 30: tenWord = "Thirty"
if tenVal == 40: tenWord = "Forty"
if tenVal == 50: tenWord = "Fifty"
if tenVal == 60: tenWord = "Sixty"
if tenVal == 70: tenWord = "Seventy"
if tenVal == 80: tenWord = "Eighty"
if tenVal == 90: tenWord = "Ninety"
if tenVal == 10:
if oneVal == 0: oneWord = "Ten"
if oneVal == 1: oneWord = "Eleven"
if oneVal == 2: oneWord = "Twelve"
if oneVal == 3: oneWord = "Thirteen"
if oneVal == 4: oneWord = "Fourteen"
if oneVal == 5: oneWord = "Fifteen"
if oneVal == 6: oneWord = "Sixteen"
if oneVal == 7: oneWord = "Seventeen"
if oneVal == 8: oneWord = "Eighteen"
if oneVal == 9: oneWord = "Nineteen"
numWord = ("%s %s" % (hunWord, oneWord)).strip()
else:
if oneVal == 0: oneWord = ""
if oneVal == 1: oneWord = "One"
if oneVal == 2: oneWord = "Two"
if oneVal == 3: oneWord = "Three"
if oneVal == 4: oneWord = "Four"
if oneVal == 5: oneWord = "Five"
if oneVal == 6: oneWord = "Six"
if oneVal == 7: oneWord = "Seven"
if oneVal == 8: oneWord = "Eight"
if oneVal == 9: oneWord = "Nine"
if tenVal == 0:
numWord = ("%s %s" % (hunWord, oneWord)).strip()
else:
if oneVal == 0:
numWord = ("%s %s" % (hunWord, tenWord)).strip()
else:
numWord = ("%s %s-%s" % (hunWord, tenWord, oneWord)).strip()
return numWord
@@ -1,6 +1,4 @@
# This is the Title
## This is the Subtitle
### Making a Scene
% Begin Meta
@pov: Jane
@@ -0,0 +1,5 @@
## So it Begins
@pov: Jane
% The first chapter with much action and such.
@@ -0,0 +1,5 @@
## Where has John Gone?
@pov: Jane
% We continue the saga of John and Jane
@@ -0,0 +1,23 @@
### We Found John!
@pov: John
Jane has been searching for a while, and she finally found John on Mars. He was indeed in space! What was he doing on Mars anyway? Well, it turns out, he was farming potatoes.
Farming potatoes you say? Why would you do that?
No one knows, but it seemed like a good idea at the time I suppose.
### A Note on Potato Farming on Mars
@pov: John
% Were adding a second scene to the same file here, which is perfectly fine I may add.
Potatoes cannot be farmed on Mars. That is simply a fact. There is no soil. Unless John brings the soil himself. Wait, did he?
I dont want to know.
#### Conclusion
Why would anyone write about such things? let alone make a film about it?
@@ -1,6 +1,8 @@
# This is a New File!
### Another Scene
@pov: John
@location: Space
Although, not so new now that it has text in it an everything …
This is the second scene in out story. We have no idea whats going on, so were just going to ramble on until we have a few lines of text so that the editor has something to work with.
In fact, this scene is supposed to be about John, but we dont really know anything about John, except that he is somewhere in space. Perhaps hes lost? Or has gone where no man has gone before? Where is Jane then?
+58 -22
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.2.0" fileVersion="1.0" timeStamp="2019-08-11 19:41:35">
<novelWriterXML appVersion="0.2.3" fileVersion="1.0" timeStamp="2019-10-19 13:37:25">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
@@ -11,7 +11,7 @@
<spellCheck>True</spellCheck>
<lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>636b6aa9b697b</lastViewed>
<lastWordCount>581</lastWordCount>
<lastWordCount>758</lastWordCount>
<autoReplace>
<A>B</A>
<B>E</B>
@@ -33,7 +33,7 @@
<entry blue="175" green="0" red="117">Main</entry>
</importance>
</settings>
<content count="15">
<content count="18">
<item handle="7031beac91f75" order="0" parent="None">
<name>Novel</name>
<type>ROOT</type>
@@ -51,7 +51,7 @@
<charCount>23</charCount>
<wordCount>5</wordCount>
<paraCount>1</paraCount>
<cursorPos>0</cursorPos>
<cursorPos>16</cursorPos>
</item>
<item handle="e7ded148d6e4a" order="1" parent="7031beac91f75">
<name>Some Chapter</name>
@@ -60,41 +60,77 @@
<status>Notes</status>
<expanded>True</expanded>
</item>
<item handle="96b68994dfa3d" order="0" parent="e7ded148d6e4a">
<name>New Scene</name>
<item handle="6a2d6d5f4f401" order="0" parent="e7ded148d6e4a">
<name>Chapter One</name>
<type>FILE</type>
<class>NOVEL</class>
<status>New</status>
<expanded>False</expanded>
<layout>CHAPTER</layout>
<charCount>12</charCount>
<wordCount>3</wordCount>
<paraCount>0</paraCount>
<cursorPos>75</cursorPos>
</item>
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
<name>Making a Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Notes</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>713</charCount>
<wordCount>132</wordCount>
<paraCount>6</paraCount>
<cursorPos>85</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Notes</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>412</charCount>
<wordCount>82</wordCount>
<paraCount>2</paraCount>
<cursorPos>448</cursorPos>
</item>
<item handle="96b68994dfa3d" order="3" parent="e7ded148d6e4a">
<name>A Note on Ipsums</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Started</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<layout>NOTE</layout>
<charCount>2571</charCount>
<wordCount>377</wordCount>
<paraCount>5</paraCount>
<cursorPos>0</cursorPos>
</item>
<item handle="636b6aa9b697b" order="1" parent="e7ded148d6e4a">
<name>File With Stuff</name>
<item handle="88706ddc78b1b" order="4" parent="e7ded148d6e4a">
<name>Chapter Two</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Notes</status>
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>736</charCount>
<wordCount>137</wordCount>
<paraCount>6</paraCount>
<cursorPos>82</cursorPos>
<layout>CHAPTER</layout>
<charCount>20</charCount>
<wordCount>4</wordCount>
<paraCount>0</paraCount>
<cursorPos>76</cursorPos>
</item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>New File</name>
<item handle="ae7339df26ded" order="5" parent="e7ded148d6e4a">
<name>We Found John!</name>
<type>FILE</type>
<class>NOVEL</class>
<status>Notes</status>
<status>New</status>
<expanded>False</expanded>
<layout>SCENE</layout>
<charCount>82</charCount>
<wordCount>19</wordCount>
<paraCount>1</paraCount>
<cursorPos>69</cursorPos>
<charCount>567</charCount>
<wordCount>112</wordCount>
<paraCount>6</paraCount>
<cursorPos>634</cursorPos>
</item>
<item handle="f6622b4617424" order="1" parent="None">
<name>Characters</name>