Cleane dup project open and handling of legacy project content

This commit is contained in:
Veronica K. B. Olsen
2020-05-29 23:01:25 +02:00
parent 2b36ed3617
commit 36ca2ee309
9 changed files with 172 additions and 127 deletions
+2
View File
@@ -39,6 +39,8 @@ class nwFiles():
PROJ_FILE = "nwProject.nwx" PROJ_FILE = "nwProject.nwx"
PROJ_DICT = "wordlist.txt" PROJ_DICT = "wordlist.txt"
PROJ_LOCK = "nwProject.lock" PROJ_LOCK = "nwProject.lock"
TOC_TXT = "ToC.txt"
TOC_JSON = "ToC.json"
SESS_INFO = "sessionInfo.log" SESS_INFO = "sessionInfo.log"
INDEX_FILE = "tagsIndex.json" INDEX_FILE = "tagsIndex.json"
OPTS_FILE = "guiOptions.json" OPTS_FILE = "guiOptions.json"
-2
View File
@@ -9,7 +9,6 @@ from nw.core.spellcheck import NWSpellSimple
from nw.core.tokenizer import Tokenizer from nw.core.tokenizer import Tokenizer
from nw.core.tohtml import ToHtml from nw.core.tohtml import ToHtml
from nw.core.tools import countWords from nw.core.tools import countWords
from nw.core.tools import projectMaintenance
from nw.core.tools import numberToWord from nw.core.tools import numberToWord
__all__ = [ __all__ = [
@@ -22,6 +21,5 @@ __all__ = [
"Tokenizer", "Tokenizer",
"ToHtml", "ToHtml",
"countWords", "countWords",
"projectMaintenance",
"numberToWord", "numberToWord",
] ]
+150 -59
View File
@@ -42,7 +42,6 @@ from shutil import make_archive
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from nw.gui.tools import OptionState from nw.gui.tools import OptionState
from nw.core.tools import projectMaintenance
from nw.core.document import NWDoc from nw.core.document import NWDoc
from nw.common import checkString, checkBool, checkInt, formatTimeStamp from nw.common import checkString, checkBool, checkInt, formatTimeStamp
from nw.constants import ( from nw.constants import (
@@ -72,11 +71,12 @@ class NWProject():
self.autoCount = 0 # Meta data: number of automatic saves self.autoCount = 0 # Meta data: number of automatic saves
# Class Settings # Class Settings
self.projPath = None # The full path to where the currently open project is saved self.projPath = None # The full path to where the currently open project is saved
self.projMeta = None # The full path to the project's meta data folder self.projMeta = None # The full path to the project's meta data folder
self.projData = None # The full path to the project's data folder self.projCache = None # The full path to the project's cache folder
self.projDict = None # The spell check dictionary self.projContent = None # The full path to the project's content folder
self.projFile = None # The file name of the project main XML file self.projDict = None # The spell check dictionary
self.projFile = None # The file name of the project main XML file
# Project Meta # Project Meta
self.projName = "" # Project name (working title) self.projName = "" # Project name (working title)
@@ -196,7 +196,8 @@ class NWProject():
# Project Settings # Project Settings
self.projPath = None self.projPath = None
self.projMeta = None self.projMeta = None
self.projData = None self.projCache = None
self.projContent = None
self.projDict = None self.projDict = None
self.projFile = nwFiles.PROJ_FILE self.projFile = nwFiles.PROJ_FILE
self.projName = "" self.projName = ""
@@ -248,14 +249,37 @@ class NWProject():
self.projPath = path.abspath(path.dirname(fileName)) self.projPath = path.abspath(path.dirname(fileName))
logger.debug("Opening project: %s" % self.projPath) logger.debug("Opening project: %s" % self.projPath)
self.projMeta = path.join(self.projPath,"meta") # Standard Folders and Files
self.projData = path.join(self.projPath,"content") # ==========================
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
self.projMeta = path.join(self.projPath, "meta")
self.projCache = path.join(self.projPath, "cache")
self.projContent = path.join(self.projPath, "content")
self.projDict = path.join(self.projMeta, nwFiles.PROJ_DICT)
if not self._checkFolder(self.projMeta): if not self._checkFolder(self.projMeta):
return False return False
if not self._checkFolder(self.projData): if not self._checkFolder(self.projCache):
return False return False
if not self._checkFolder(self.projContent):
return False
# Check for Old Legacy Data
# =========================
errList = []
for projItem in listdir(self.projPath):
logger.verbose("Project contains: %s" % projItem)
if projItem.startswith("data_"):
self._legacyDataFolder(projItem)
if errList:
self.makeAlert(errList, nwAlert.ERROR)
self._deprecatedFiles()
# Project Lock
# ============
if overrideLock: if overrideLock:
self._clearLockFile() self._clearLockFile()
@@ -272,10 +296,8 @@ class NWProject():
else: else:
logger.verbose("Project is not locked") logger.verbose("Project is not locked")
try: # Open The Project XML File
projectMaintenance(self) # =========================
except Exception as E:
logger.error(str(E))
try: try:
nwXML = etree.parse(fileName) nwXML = etree.parse(fileName)
@@ -321,6 +343,7 @@ class NWProject():
# Check File Type # Check File Type
# =============== # ===============
if not nwxRoot == "novelWriterXML": if not nwxRoot == "novelWriterXML":
self.makeAlert( self.makeAlert(
"Project file does not appear to be a novelWriterXML file.", "Project file does not appear to be a novelWriterXML file.",
@@ -330,14 +353,17 @@ class NWProject():
# Check Project Storage Version # Check Project Storage Version
# ============================= # =============================
if fileVersion == "1.0": if fileVersion == "1.0":
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Old Project Version", ( msgRes = msgBox.question(self.theParent, "Old Project Version", (
"The project file and data is created by a %s version lower than 0.7. " "The project file and data is created by a %s version lower than 0.7. "
"Do you want to upgrade the project to the most recent format?<br><br>" "Do you want to upgrade the project to the most recent format?<br><br>"
"Note that after the upgrade, you cannot open the project with an older " "Note that after the upgrade, you cannot open the project with an older "
"version of novelWriter any more, so make sure you have a recent backup." "version of %s any more, so make sure you have a recent backup."
) % nw.__package__) ) % (
nw.__package__, nw.__package__
))
if msgRes == QMessageBox.Yes: if msgRes == QMessageBox.Yes:
self._updateStorage() self._updateStorage()
else: else:
@@ -353,6 +379,7 @@ class NWProject():
# Check novelWriter Version # Check novelWriter Version
# ========================= # =========================
if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI: if int(hexVersion, 16) > int(nw.__hexversion__, 16) and self.mainConf.showGUI:
msgBox = QMessageBox() msgBox = QMessageBox()
msgRes = msgBox.question(self.theParent, "Version Conflict", ( msgRes = msgBox.question(self.theParent, "Version Conflict", (
@@ -367,6 +394,7 @@ class NWProject():
# Start Parsing XML # Start Parsing XML
# ================= # =================
for xChild in xRoot: for xChild in xRoot:
if xChild.tag == "project": if xChild.tag == "project":
logger.debug("Found project meta") logger.debug("Found project meta")
@@ -384,6 +412,7 @@ class NWProject():
self.bookAuthors.append(xItem.text) self.bookAuthors.append(xItem.text)
elif xItem.tag == "backup": elif xItem.tag == "backup":
self.doBackup = checkBool(xItem.text, False) self.doBackup = checkBool(xItem.text, False)
elif xChild.tag == "settings": elif xChild.tag == "settings":
logger.debug("Found project settings") logger.debug("Found project settings")
for xItem in xChild: for xItem in xChild:
@@ -411,6 +440,7 @@ class NWProject():
for xEntry in xItem: for xEntry in xItem:
titleFormat[xEntry.tag] = checkString(xEntry.text, "", False) titleFormat[xEntry.tag] = checkString(xEntry.text, "", False)
self.setTitleFormat(titleFormat) self.setTitleFormat(titleFormat)
elif xChild.tag == "content": elif xChild.tag == "content":
logger.debug("Found project content") logger.debug("Found project content")
self.projTree.unpackXML(xChild) self.projTree.unpackXML(xChild)
@@ -444,14 +474,14 @@ class NWProject():
return False return False
self.projMeta = path.join(self.projPath, "meta") self.projMeta = path.join(self.projPath, "meta")
self.projData = path.join(self.projPath, "content") self.projContent = path.join(self.projPath, "content")
saveTime = time() saveTime = time()
if not self._checkFolder(self.projPath): if not self._checkFolder(self.projPath):
return False return False
if not self._checkFolder(self.projMeta): if not self._checkFolder(self.projMeta):
return False return False
if not self._checkFolder(self.projData): if not self._checkFolder(self.projContent):
return False return False
logger.debug("Saving project: %s" % self.projPath) logger.debug("Saving project: %s" % self.projPath)
@@ -978,7 +1008,7 @@ class NWProject():
# Then check the files in the data folder # Then check the files in the data folder
orphanFiles = [] orphanFiles = []
for fileItem in listdir(self.projData): for fileItem in listdir(self.projContent):
if not fileItem.endswith(".nwd"): if not fileItem.endswith(".nwd"):
logger.warning("Skipping file %s" % fileItem) logger.warning("Skipping file %s" % fileItem)
continue continue
@@ -1046,52 +1076,113 @@ class NWProject():
return True return True
def _updateStorage(self): ##
"""Updates the project storage folder from 1.0 to 1.1. # Legacy Data Structure Handlers
##
def _legacyDataFolder(self, theFolder):
"""Clean up legacy data folders.
""" """
contDir = path.join(self.projPath, "content")
self._checkFolder(contDir)
errList = [] errList = []
theData = path.join(self.projPath, theFolder)
if not path.isdir(theData):
errList.append("Not a folder: %s" % theData)
return errList
for projItem in listdir(self.projPath): logger.info("Old data folder %s found" % theFolder)
itemPath = path.join(self.projPath, projItem)
if not path.isdir(itemPath) or not projItem.startswith("data_"): # Move Documents to Content
# =========================
for dataItem in listdir(theData):
theFile = path.join(theData, dataItem)
if not path.isfile(theFile):
theErr = self._moveUnknownItem(theData, dataItem)
if theErr:
errList.append(theErr)
continue continue
for dataFile in listdir(itemPath):
dataPath = path.join(itemPath, dataFile)
if dataFile.endswith(".bak"):
try:
unlink(dataPath)
logger.info("Deleted file: %s" % dataPath)
except:
errList.append("Failed to delete: %s" % dataPath)
elif dataFile.endswith(".nwd") and len(dataFile) == 21: if len(dataItem) == 21 and dataItem.endswith("_main.nwd"):
tHandle = projItem[-1]+dataFile[:12] tHandle = theFolder[-1]+dataItem[:12]
newPath = path.join(contDir, tHandle+".nwd") newPath = path.join(self.projContent, tHandle+".nwd")
try: try:
rename(dataPath, newPath) rename(theFile, newPath)
logger.info("Moved file: %s" % dataPath) logger.info("Moved file: %s" % theFile)
logger.info("New location: %s" % newPath) logger.info("New location: %s" % newPath)
except: except Exception as e:
errList.append("Failed to move: %s" % dataPath) logger.error(str(e))
errList.append("Could not move: %s" % theFile)
else: elif len(dataItem) == 21 and dataItem.endswith("_main.bak"):
newPath = path.join(self.projPath, "unknown_"+dataFile) try:
try: unlink(theFile)
rename(dataPath, newPath) logger.info("Deleted file: %s" % theFile)
logger.info("Moved file: %s" % dataPath) except Exception as e:
logger.info("New location: %s" % newPath) logger.error(str(e))
except: errList.append("Could not delete: %s" % theFile)
errList.append("Failed to move: %s" % dataPath)
try:
rmdir(itemPath)
logger.info("Removed folder: %s" % itemPath)
except:
errList.append("Failed to delete: %s" % itemPath)
if errList: else:
self.makeAlert(errList, nwAlert.ERROR) theErr = self._moveUnknownItem(theData, dataItem)
if theErr:
errList.append(theErr)
# Remove Data Folder
# ==================
try:
rmdir(theData)
logger.info("Removed folder: %s" % theFolder)
except:
errList.append("Failed to remove: %s" % theFolder)
return errList
def _moveUnknownItem(self, theDir, theItem):
"""Move an item that doesn't belong in the project folder to
a junk folder.
"""
theJunk = path.join(self.projPath, "junk")
if not self._checkFolder(theJunk):
return "Could not make folder: %s" % theJunk
theSrc = path.join(theDir, theItem)
theDst = path.join(theJunk, theItem)
try:
rename(theSrc, theDst)
logger.info("Moved to junk: %s" % theSrc)
except Exception as e:
logger.error(str(e))
return "Could not move item %s to junk." % theSrc
return ""
def _deprecatedFiles(self):
"""Delete files that are no longer used by novelWriter.
"""
rmList = []
rmList.append(path.join(self.projCache, "nwProject.nwx.0"))
rmList.append(path.join(self.projCache, "nwProject.nwx.1"))
rmList.append(path.join(self.projCache, "nwProject.nwx.2"))
rmList.append(path.join(self.projCache, "nwProject.nwx.3"))
rmList.append(path.join(self.projCache, "nwProject.nwx.4"))
rmList.append(path.join(self.projCache, "nwProject.nwx.5"))
rmList.append(path.join(self.projCache, "nwProject.nwx.6"))
rmList.append(path.join(self.projCache, "nwProject.nwx.7"))
rmList.append(path.join(self.projCache, "nwProject.nwx.8"))
rmList.append(path.join(self.projCache, "nwProject.nwx.9"))
rmList.append(path.join(self.projMeta, "mainOptions.json"))
rmList.append(path.join(self.projMeta, "exportOptions.json"))
rmList.append(path.join(self.projMeta, "outlineOptions.json"))
rmList.append(path.join(self.projMeta, "timelineOptions.json"))
rmList.append(path.join(self.projMeta, "docMergeOptions.json"))
rmList.append(path.join(self.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return return
-40
View File
@@ -8,7 +8,6 @@
File History: File History:
Created: 2019-04-22 [0.0.1] countWords Created: 2019-04-22 [0.0.1] countWords
Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN Created: 2019-10-13 [0.2.3] numberToWord, _numberToWordEN
Created: 2020-02-13 [0.4.3] projectMaintenance
Merged: 2020-05-08 [0.4.5] All of the above into this file Merged: 2020-05-08 [0.4.5] All of the above into this file
This file is a part of novelWriter This file is a part of novelWriter
@@ -81,45 +80,6 @@ def countWords(theText):
return charCount, wordCount, paraCount return charCount, wordCount, paraCount
def projectMaintenance(theProject):
"""Wrapper class for handling various tasks related to managing old
projects with content from older versions of novelWriter.
"""
# Remove no longer used project cache folder
if path.isdir(theProject.projPath):
cacheDir = path.join(theProject.projPath, "cache")
if path.isdir(cacheDir):
logger.info("Deprecated cache folder content found")
rmList = []
for i in range(10):
rmList.append(path.join(cacheDir, "nwProject.nwx.%d" % i))
rmList.append(path.join(cacheDir, "projCount.txt"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
# Remove no longer used meta files
rmList = []
rmList.append(path.join(theProject.projMeta, "mainOptions.json"))
rmList.append(path.join(theProject.projMeta, "exportOptions.json"))
rmList.append(path.join(theProject.projMeta, "outlineOptions.json"))
rmList.append(path.join(theProject.projMeta, "timelineOptions.json"))
rmList.append(path.join(theProject.projMeta, "docMergeOptions.json"))
rmList.append(path.join(theProject.projMeta, "sessionLogOptions.json"))
for rmFile in rmList:
if path.isfile(rmFile):
logger.info("Deleting: %s" % rmFile)
try:
unlink(rmFile)
except Exception as e:
logger.error(str(e))
return
def numberToWord(numVal, theLanguage): def numberToWord(numVal, theLanguage):
"""Wrapper for converting numbers to words for chapter headings. """Wrapper for converting numbers to words for chapter headings.
""" """
+1 -1
View File
@@ -2,6 +2,6 @@
### We Found John! ### We Found John!
@pov: John @pov: John
@location: Mars, OuterSpace @location: Mars
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. 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.
+1 -1
View File
@@ -1,4 +1,4 @@
%%~ b8136a5a774a0:7031beac91f75:Delete Me! %%~ b8136a5a774a0:98acd8c76c93a:Delete Me!
### Delete Me! ### Delete Me!
This scene is trash. This scene is trash.
+1 -1
View File
@@ -1,4 +1,4 @@
%%~ edca4be2fcaf8:7031beac91f75:Part One %%~ edca4be2fcaf8:7031beac91f75:Part 1
# Part One # Part One
The first part. The first part.
-6
View File
@@ -5,10 +5,4 @@
Space … its an awful lot of nothing, with bits in it here and there. Some of which, people like to call home. Space … its an awful lot of nothing, with bits in it here and there. Some of which, people like to call home.
## Outer Space
@tag: OuterSpace
Now even further into space!
You can have more than one tag in a file, as long as there is only one tag per heading.
+17 -17
View File
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="0.6.2" hexVersion="0x000602f0" fileVersion="1.1" saveCount="189" autoCount="30" timeStamp="2020-05-28 20:19:28"> <novelWriterXML appVersion="0.7.0rc1" hexVersion="0x000700c1" fileVersion="1.1" saveCount="193" autoCount="29" timeStamp="2020-05-29 22:52:06">
<project> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
@@ -11,8 +11,8 @@
<spellCheck>True</spellCheck> <spellCheck>True</spellCheck>
<autoOutline>True</autoOutline> <autoOutline>True</autoOutline>
<lastEdited>636b6aa9b697b</lastEdited> <lastEdited>636b6aa9b697b</lastEdited>
<lastViewed>bc0cbd2a407f3</lastViewed> <lastViewed>ba8a28a246524</lastViewed>
<lastWordCount>941</lastWordCount> <lastWordCount>914</lastWordCount>
<autoReplace> <autoReplace>
<A>B</A> <A>B</A>
<B>E</B> <B>E</B>
@@ -73,7 +73,7 @@
<expanded>False</expanded> <expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>PAGE</layout> <layout>PAGE</layout>
<charCount>210</charCount> <charCount>208</charCount>
<wordCount>40</wordCount> <wordCount>40</wordCount>
<paraCount>2</paraCount> <paraCount>2</paraCount>
<cursorPos>213</cursorPos> <cursorPos>213</cursorPos>
@@ -89,7 +89,7 @@
<charCount>23</charCount> <charCount>23</charCount>
<wordCount>5</wordCount> <wordCount>5</wordCount>
<paraCount>1</paraCount> <paraCount>1</paraCount>
<cursorPos>27</cursorPos> <cursorPos>0</cursorPos>
</item> </item>
<item handle="e7ded148d6e4a" order="3" parent="7031beac91f75"> <item handle="e7ded148d6e4a" order="3" parent="7031beac91f75">
<name>A Folder</name> <name>A Folder</name>
@@ -122,7 +122,7 @@
<charCount>1199</charCount> <charCount>1199</charCount>
<wordCount>216</wordCount> <wordCount>216</wordCount>
<paraCount>7</paraCount> <paraCount>7</paraCount>
<cursorPos>1066</cursorPos> <cursorPos>1266</cursorPos>
</item> </item>
<item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a"> <item handle="bc0cbd2a407f3" order="2" parent="e7ded148d6e4a">
<name>Another Scene</name> <name>Another Scene</name>
@@ -135,7 +135,7 @@
<charCount>476</charCount> <charCount>476</charCount>
<wordCount>93</wordCount> <wordCount>93</wordCount>
<paraCount>3</paraCount> <paraCount>3</paraCount>
<cursorPos>428</cursorPos> <cursorPos>551</cursorPos>
</item> </item>
<item handle="ba8a28a246524" order="3" parent="e7ded148d6e4a"> <item handle="ba8a28a246524" order="3" parent="e7ded148d6e4a">
<name>Interlude</name> <name>Interlude</name>
@@ -148,7 +148,7 @@
<charCount>633</charCount> <charCount>633</charCount>
<wordCount>101</wordCount> <wordCount>101</wordCount>
<paraCount>3</paraCount> <paraCount>3</paraCount>
<cursorPos>752</cursorPos> <cursorPos>1238</cursorPos>
</item> </item>
<item handle="96b68994dfa3d" order="4" parent="e7ded148d6e4a"> <item handle="96b68994dfa3d" order="4" parent="e7ded148d6e4a">
<name>A Note on Structure</name> <name>A Note on Structure</name>
@@ -161,7 +161,7 @@
<charCount>1692</charCount> <charCount>1692</charCount>
<wordCount>313</wordCount> <wordCount>313</wordCount>
<paraCount>6</paraCount> <paraCount>6</paraCount>
<cursorPos>551</cursorPos> <cursorPos>1721</cursorPos>
</item> </item>
<item handle="88706ddc78b1b" order="5" parent="e7ded148d6e4a"> <item handle="88706ddc78b1b" order="5" parent="e7ded148d6e4a">
<name>Chapter Two</name> <name>Chapter Two</name>
@@ -174,7 +174,7 @@
<charCount>139</charCount> <charCount>139</charCount>
<wordCount>28</wordCount> <wordCount>28</wordCount>
<paraCount>1</paraCount> <paraCount>1</paraCount>
<cursorPos>242</cursorPos> <cursorPos>343</cursorPos>
</item> </item>
<item handle="ae7339df26ded" order="6" parent="e7ded148d6e4a"> <item handle="ae7339df26ded" order="6" parent="e7ded148d6e4a">
<name>We Found John!</name> <name>We Found John!</name>
@@ -214,7 +214,7 @@
<charCount>49</charCount> <charCount>49</charCount>
<wordCount>9</wordCount> <wordCount>9</wordCount>
<paraCount>1</paraCount> <paraCount>1</paraCount>
<cursorPos>65</cursorPos> <cursorPos>24</cursorPos>
</item> </item>
<item handle="bb2c23b3c42cc" order="1" parent="f7e2d9f330615"> <item handle="bb2c23b3c42cc" order="1" parent="f7e2d9f330615">
<name>Jane Smith</name> <name>Jane Smith</name>
@@ -227,7 +227,7 @@
<charCount>55</charCount> <charCount>55</charCount>
<wordCount>9</wordCount> <wordCount>9</wordCount>
<paraCount>1</paraCount> <paraCount>1</paraCount>
<cursorPos>71</cursorPos> <cursorPos>25</cursorPos>
</item> </item>
<item handle="15c4492bd5107" order="2" parent="None"> <item handle="15c4492bd5107" order="2" parent="None">
<name>Locations</name> <name>Locations</name>
@@ -247,7 +247,7 @@
<charCount>76</charCount> <charCount>76</charCount>
<wordCount>15</wordCount> <wordCount>15</wordCount>
<paraCount>1</paraCount> <paraCount>1</paraCount>
<cursorPos>93</cursorPos> <cursorPos>20</cursorPos>
</item> </item>
<item handle="f1471bef9f2ae" order="1" parent="15c4492bd5107"> <item handle="f1471bef9f2ae" order="1" parent="15c4492bd5107">
<name>Space</name> <name>Space</name>
@@ -257,10 +257,10 @@
<expanded>False</expanded> <expanded>False</expanded>
<exported>True</exported> <exported>True</exported>
<layout>NOTE</layout> <layout>NOTE</layout>
<charCount>241</charCount> <charCount>115</charCount>
<wordCount>51</wordCount> <wordCount>24</wordCount>
<paraCount>3</paraCount> <paraCount>1</paraCount>
<cursorPos>135</cursorPos> <cursorPos>133</cursorPos>
</item> </item>
<item handle="5eaea4e8cdee8" order="2" parent="15c4492bd5107"> <item handle="5eaea4e8cdee8" order="2" parent="15c4492bd5107">
<name>Mars</name> <name>Mars</name>