Add a custom text document in the editor

This commit is contained in:
Veronica Berglyd Olsen
2023-09-07 19:20:00 +02:00
parent e08790e889
commit 16e2e0cc83
6 changed files with 154 additions and 95 deletions
+48 -51
View File
@@ -56,6 +56,7 @@ from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.core.index import countWords
from novelwriter.gui.dochighlight import GuiDocHighlighter
from novelwriter.gui.editordocument import GuiTextDocument
from novelwriter.extensions.wheeleventfilter import WheelEventFilter
if TYPE_CHECKING: # pragma: no cover
@@ -120,9 +121,12 @@ class GuiDocEditor(QPlainTextEdit):
self._typPadBefore = ""
self._typPadAfter = ""
# Core Elements and Signals
qDoc = self.document()
qDoc.contentsChange.connect(self._docChange)
# Create Custom Document
self._qDocument = GuiTextDocument(self)
self.setDocument(self._qDocument)
# Connect Signals
self._qDocument.contentsChange.connect(self._docChange)
self.selectionChanged.connect(self._updateSelectedStatus)
# Document Title
@@ -130,9 +134,6 @@ class GuiDocEditor(QPlainTextEdit):
self.docFooter = GuiDocEditFooter(self)
self.docSearch = GuiDocEditSearch(self)
# Syntax
self.highLight = GuiDocHighlighter(qDoc)
# Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
@@ -212,7 +213,7 @@ class GuiDocEditor(QPlainTextEdit):
@property
def isEmpty(self) -> bool:
"""Check if the current document is empty."""
return self.document().isEmpty()
return self._qDocument.isEmpty()
##
# Methods
@@ -266,7 +267,7 @@ class GuiDocEditor(QPlainTextEdit):
self.docHeader.matchColours()
self.docFooter.matchColours()
self.highLight.initHighlighter()
self._qDocument.syntaxHighlighter.initHighlighter()
return
@@ -312,8 +313,7 @@ class GuiDocEditor(QPlainTextEdit):
# Due to cursor visibility, a part of the margin must be
# allocated to the document itself. See issue #1112.
cW = 2*self.cursorWidth()
qDoc = self.document()
qDoc.setDocumentMargin(cW)
self._qDocument.setDocumentMargin(cW)
self._vpMargin = max(CONFIG.getTextMargin() - cW, 0)
self.setViewportMargins(self._vpMargin, self._vpMargin, self._vpMargin, self._vpMargin)
@@ -327,7 +327,7 @@ class GuiDocEditor(QPlainTextEdit):
if CONFIG.showLineEndings:
theOpt.setFlags(theOpt.flags() | QTextOption.ShowLineAndParagraphSeparators)
qDoc.setDefaultTextOption(theOpt)
self._qDocument.setDefaultTextOption(theOpt)
# Scroll bars
if CONFIG.hideVScroll:
@@ -377,13 +377,12 @@ class GuiDocEditor(QPlainTextEdit):
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self._docHandle = tHandle
self.highLight.setHandle(tHandle)
tStart = time()
self._allowAutoReplace(False)
self.setPlainText(docText)
self._qDocument.setTextContent(docText, tHandle)
self._allowAutoReplace(True)
logger.debug("Document text loaded in %.3f ms", 1000*(time() - tStart))
logger.debug("Document text set in %.3f ms", 1000*(time() - tStart))
qApp.processEvents()
self._lastEdit = time()
@@ -404,14 +403,15 @@ class GuiDocEditor(QPlainTextEdit):
self.docFooter.updateLineCount()
# This is a hack to fix invisible cursor on an empty document
if self.document().characterCount() <= 1:
if self._qDocument.characterCount() <= 1:
self.setPlainText("\n")
self.setPlainText("")
self.setCursorPosition(0)
qApp.processEvents()
self.document().clearUndoRedoStacks()
self.setDocumentChanged(False)
self._qDocument.clearUndoRedoStacks()
qApp.restoreOverrideCursor()
# Update the status bar
@@ -422,12 +422,12 @@ class GuiDocEditor(QPlainTextEdit):
def updateTagHighLighting(self) -> None:
"""Rerun the syntax highlighter on all meta data lines."""
self.highLight.rehighlightByType(GuiDocHighlighter.BLOCK_META)
self._qDocument.syntaxHighlighter.rehighlightByType(GuiDocHighlighter.BLOCK_META)
return
def redrawText(self) -> None:
"""Redraw the text by marking the document content as dirty."""
self.document().markContentsDirty(0, self.document().characterCount())
self._qDocument.markContentsDirty(0, self._qDocument.characterCount())
self.updateDocMargins()
return
@@ -561,7 +561,7 @@ class GuiDocEditor(QPlainTextEdit):
paragraph and line separators though.
See: https://doc.qt.io/qt-5/qtextdocument.html#toPlainText
"""
text = self.document().toRawText()
text = self._qDocument.toRawText()
text = text.replace(nwUnicode.U_LSEP, "\n") # Line separators
text = text.replace(nwUnicode.U_PSEP, "\n") # Paragraph separators
return text
@@ -586,7 +586,7 @@ class GuiDocEditor(QPlainTextEdit):
def setCursorPosition(self, position: int) -> None:
"""Move the cursor to a given position in the document."""
nChars = self.document().characterCount()
nChars = self._qDocument.characterCount()
if nChars > 1 and isinstance(position, int):
cursor = self.textCursor()
cursor.setPosition(minmax(position, 0, nChars-1))
@@ -605,7 +605,7 @@ class GuiDocEditor(QPlainTextEdit):
def setCursorLine(self, line: int | None) -> None:
"""Move the cursor to a given line in the document."""
if isinstance(line, int) and line > 0:
block = self.document().findBlockByNumber(line - 1)
block = self._qDocument.findBlockByNumber(line - 1)
if block:
self.setCursorPosition(block.position())
logger.debug("Cursor moved to line %d", line)
@@ -638,7 +638,7 @@ class GuiDocEditor(QPlainTextEdit):
self._spellCheck = state
self.mainGui.mainMenu.setSpellCheck(state)
SHARED.project.data.setSpellCheck(state)
self.highLight.setSpellCheck(state)
self._qDocument.syntaxHighlighter.setSpellCheck(state)
if state is False:
self.spellCheckDocument()
@@ -655,7 +655,7 @@ class GuiDocEditor(QPlainTextEdit):
logger.debug("Running spell checker")
start = time()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
self.highLight.rehighlight()
self._qDocument.syntaxHighlighter.rehighlight()
qApp.restoreOverrideCursor()
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
self.statusMessage.emit(self.tr("Spell check complete"))
@@ -994,7 +994,7 @@ class GuiDocEditor(QPlainTextEdit):
self.wcTimerDoc.start()
if self._doReplace and added == 1:
self._docAutoReplace(self.document().findBlock(pos))
self._docAutoReplace(self._qDocument.findBlock(pos))
return
@@ -1122,7 +1122,7 @@ class GuiDocEditor(QPlainTextEdit):
theWord = cursor.selectedText().strip().strip(self._nonWord)
logger.debug("Added '%s' to project dictionary", theWord)
SHARED.spelling.addWord(theWord)
self.highLight.rehighlightBlock(cursor.block())
self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block())
return
@pyqtSlot()
@@ -1418,8 +1418,8 @@ class GuiDocEditor(QPlainTextEdit):
posS = theCursor.selectionStart()
posE = theCursor.selectionEnd()
blockS = self.document().findBlock(posS)
blockE = self.document().findBlock(posE)
blockS = self._qDocument.findBlock(posS)
blockE = self._qDocument.findBlock(posE)
if blockS != blockE:
posE = blockS.position() + blockS.length() - 1
@@ -1430,14 +1430,14 @@ class GuiDocEditor(QPlainTextEdit):
numB = 0
for n in range(fLen):
if self.document().characterAt(posS-n-1) == fChar:
if self._qDocument.characterAt(posS-n-1) == fChar:
numB += 1
else:
break
numA = 0
for n in range(fLen):
if self.document().characterAt(posE+n) == fChar:
if self._qDocument.characterAt(posE+n) == fChar:
numA += 1
else:
break
@@ -1487,9 +1487,8 @@ class GuiDocEditor(QPlainTextEdit):
posS = theCursor.selectionStart()
posE = theCursor.selectionEnd()
qDoc = self.document()
blockS = qDoc.findBlock(posS)
blockE = qDoc.findBlock(posE)
blockS = self._qDocument.findBlock(posS)
blockE = self._qDocument.findBlock(posE)
if blockS != blockE:
posE = blockS.position() + blockS.length() - 1
@@ -1690,16 +1689,15 @@ class GuiDocEditor(QPlainTextEdit):
def _removeInParLineBreaks(self) -> None:
"""Strip line breaks within paragraphs in the selected text."""
theCursor = self.textCursor()
theDoc = self.document()
cursor = self.textCursor()
iS = 0
iE = theDoc.blockCount() - 1
iE = self._qDocument.blockCount() - 1
rS = 0
rE = theDoc.characterCount()
if theCursor.hasSelection():
sBlock = theDoc.findBlock(theCursor.selectionStart())
eBlock = theDoc.findBlock(theCursor.selectionEnd())
rE = self._qDocument.characterCount()
if cursor.hasSelection():
sBlock = self._qDocument.findBlock(cursor.selectionStart())
eBlock = self._qDocument.findBlock(cursor.selectionEnd())
iS = sBlock.blockNumber()
iE = eBlock.blockNumber()
rS = sBlock.position()
@@ -1709,7 +1707,7 @@ class GuiDocEditor(QPlainTextEdit):
currPar = []
cleanText = ""
for i in range(iS, iE+1):
cBlock = theDoc.findBlockByNumber(i)
cBlock = self._qDocument.findBlockByNumber(i)
cText = cBlock.text()
if cText.strip() == "":
if currPar:
@@ -1726,12 +1724,12 @@ class GuiDocEditor(QPlainTextEdit):
cleanText += " ".join(currPar) + "\n\n"
# Replace the text with the cleaned up text
theCursor.beginEditBlock()
theCursor.clearSelection()
theCursor.setPosition(rS)
theCursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS)
theCursor.insertText(cleanText.rstrip() + "\n")
theCursor.endEditBlock()
cursor.beginEditBlock()
cursor.clearSelection()
cursor.setPosition(rS)
cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, rE-rS)
cursor.insertText(cleanText.rstrip() + "\n")
cursor.endEditBlock()
return
@@ -1913,11 +1911,10 @@ class GuiDocEditor(QPlainTextEdit):
# Underscore counts as a part of the word, so check that the
# selection isn't wrapped in italics markers.
reSelect = False
qDoc = self.document()
if qDoc.characterAt(posS) == "_":
if self._qDocument.characterAt(posS) == "_":
posS += 1
reSelect = True
if qDoc.characterAt(posE) == "_":
if self._qDocument.characterAt(posE) == "_":
posE -= 1
reSelect = True
if reSelect:
@@ -2838,7 +2835,7 @@ class GuiDocEditFooter(QWidget):
else:
theCursor = self.docEditor.textCursor()
iLine = theCursor.blockNumber() + 1
iDist = 100*iLine/self.docEditor.document().blockCount()
iDist = 100*iLine/self.docEditor._qDocument.blockCount()
self.linesText.setText(
self.tr("Line: {0} ({1})").format(f"{iLine:n}", f"{iDist:.0f} %")
)
@@ -2869,7 +2866,7 @@ class GuiDocEditFooter(QWidget):
self.tr("Words: {0} ({1})").format(f"{wCount:n}", f"{wDiff:+n}")
)
byteSize = self.docEditor.document().characterCount()
byteSize = self.docEditor._qDocument.characterCount()
self.wordsText.setToolTip(
self.tr("Document size is {0} bytes").format(f"{byteSize:n}")
)
+23 -25
View File
@@ -51,6 +51,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Create: GuiDocHighlighter")
self._tItem = None
self._tHandle = None
self._spellCheck = False
self._spellRx = QRegularExpression()
@@ -79,11 +80,6 @@ class GuiDocHighlighter(QSyntaxHighlighter):
return
@property
def spellCheck(self) -> bool:
"""Check if spell checking is enabled."""
return self._spellCheck
def initHighlighter(self) -> None:
"""Initialise the syntax highlighter, setting all the colour
rules and building the RegExes.
@@ -248,6 +244,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
def setHandle(self, tHandle: str) -> None:
"""Set the handle of the currently highlighted document."""
self._tHandle = tHandle
self._tItem = SHARED.project.tree[tHandle]
logger.debug(
"Syntax highlighter %s for item '%s'",
"enabled" if self._tItem else "disabled", tHandle
)
return
##
@@ -284,27 +285,24 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if text.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META)
pIndex = SHARED.project.index
tItem = SHARED.project.tree[self._tHandle]
if tItem is None:
return
isValid, theBits, thePos = pIndex.scanThis(text)
isGood = pIndex.checkThese(theBits, tItem)
if isValid:
for n, theBit in enumerate(theBits):
xPos = thePos[n]
xLen = len(theBit)
if isGood[n]:
if n == 0:
self.setFormat(xPos, xLen, self._hStyles["keyword"])
if self._tItem:
pIndex = SHARED.project.index
isValid, theBits, thePos = pIndex.scanThis(text)
isGood = pIndex.checkThese(theBits, self._tItem)
if isValid:
for n, theBit in enumerate(theBits):
xPos = thePos[n]
xLen = len(theBit)
if isGood[n]:
if n == 0:
self.setFormat(xPos, xLen, self._hStyles["keyword"])
else:
self.setFormat(xPos, xLen, self._hStyles["value"])
else:
self.setFormat(xPos, xLen, self._hStyles["value"])
else:
kwFmt = self.format(xPos)
kwFmt.setUnderlineColor(self._colError)
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
self.setFormat(xPos, xLen, kwFmt)
kwFmt = self.format(xPos)
kwFmt.setUnderlineColor(self._colError)
kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline)
self.setFormat(xPos, xLen, kwFmt)
# We never want to run the spell checker on keyword/values,
# so we force a return here
+64
View File
@@ -0,0 +1,64 @@
"""
novelWriter GUI Text Document
===============================
File History:
Created: 2023-09-07 [2.2b1]
This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import logging
from PyQt5.QtCore import QObject
from PyQt5.QtGui import QTextDocument
from PyQt5.QtWidgets import QPlainTextDocumentLayout
from novelwriter.gui.dochighlight import GuiDocHighlighter
logger = logging.getLogger(__name__)
class GuiTextDocument(QTextDocument):
def __init__(self, parent: QObject) -> None:
super().__init__(parent=parent)
self._handle = None
self._syntax = GuiDocHighlighter(self)
self.setDocumentLayout(QPlainTextDocumentLayout(self))
logger.debug("Ready: GuiTextDocument")
return
def __del__(self): # pragma: no cover
logger.debug("Delete: GuiTextDocument")
return
@property
def syntaxHighlighter(self) -> GuiDocHighlighter:
return self._syntax
def setTextContent(self, text: str, tHandle: str) -> None:
"""Set the text content of the document."""
self._syntax.setHandle(tHandle)
self.setPlainText(text)
return
# END Class GuiTextDocument
+2 -2
View File
@@ -621,10 +621,10 @@ class GuiMain(QMainWindow):
break
if nHandle is not None:
self.openDocument(nHandle, tLine=0, doScroll=True)
self.openDocument(nHandle, tLine=1, doScroll=True)
return True
elif wrapAround:
self.openDocument(fHandle, tLine=0, doScroll=True)
self.openDocument(fHandle, tLine=1, doScroll=True)
return False
return False
+3 -3
View File
@@ -1,8 +1,8 @@
%%~name: Making a Scene
%%~path: 6a2d6d5f4f401/636b6aa9b697b
%%~kind: NOVEL/DOCUMENT
%%~hash: 053cc65631403c15ddc112849dc7fcae44eb9d63
%%~date: Unknown/2023-08-25 16:56:11
%%~hash: 7aae771de46c3cab06d8be0e860dc0bd383860a5
%%~date: Unknown/2023-09-07 19:00:10
### Making a Scene
@pov: Jane
@@ -15,7 +15,7 @@ Each paragraph in the scene is separated by a blank line. The text supports mini
In addition, the editor supports automatic formatting of “quotes”, both double and single. Depending on the syntax highlighter settings and colour theme, these can be in different colours. “You can of course use **bold** and _italic_ text inside of quotes too.”
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is sett in Project Settings.
If you have the need for it, you can also add text that can be automatically replaced by other text when you generate a preview or export the project. Now, lets auto-replace this A with <A>, and this C with <C>. While <E> is just <E>. Press Ctrl+R to see what this looks like in the view pane. The list of auto-replaced text is set in Project Settings.
The editor also supports non breaking spaces, and the spell checker accepts long dashes—like this—as valid word separators. Regular dashes are also supported and can be automatically inserted when typing two hyphens.
+14 -14
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-01 20:48:55">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1522" autoCount="237" editTime="75353">
<novelWriterXML appVersion="2.2-alpha1" hexVersion="0x020200a1" fileVersion="1.5" fileRevision="1" timeStamp="2023-09-07 19:14:50">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1531" autoCount="238" editTime="75619">
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
@@ -58,27 +58,27 @@
<name status="sf24ce6" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="636b6aa9b697b" parent="6a2d6d5f4f401" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="2687" wordCount="479" paraCount="14" cursorPos="66" />
<meta expanded="no" heading="H3" charCount="2686" wordCount="479" paraCount="14" cursorPos="19" />
<name status="s90e6c9" import="ia857f0" active="yes">Making a Scene</name>
</item>
<item handle="bc0cbd2a407f3" parent="6a2d6d5f4f401" root="7031beac91f75" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="465" />
<meta expanded="no" heading="H3" charCount="548" wordCount="108" paraCount="3" cursorPos="531" />
<name status="s90e6c9" import="ia857f0" active="yes">Another Scene</name>
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="310" />
<meta expanded="no" heading="H2" charCount="617" wordCount="101" paraCount="3" cursorPos="0" />
<name status="s78ea90" import="ia857f0" active="yes">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="0" />
<meta expanded="no" heading="H1" charCount="1909" wordCount="346" paraCount="7" cursorPos="1940" />
<name status="sf24ce6" import="ia857f0" active="no">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="188" />
<meta expanded="yes" heading="H2" charCount="139" wordCount="28" paraCount="1" cursorPos="356" />
<name status="s90e6c9" import="ia857f0" active="yes">Chapter Two</name>
</item>
<item handle="ae7339df26ded" parent="88706ddc78b1b" root="7031beac91f75" order="0" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="0" />
<meta expanded="no" heading="H3" charCount="189" wordCount="37" paraCount="1" cursorPos="237" />
<name status="s90e6c9" import="ia857f0" active="yes">We Found John!</name>
</item>
<item handle="e5e47ebf63b1c" parent="None" root="e5e47ebf63b1c" order="1" type="ROOT" class="NOVEL">
@@ -90,7 +90,7 @@
<name status="sc24b8f" import="ia857f0" active="yes">Title Page</name>
</item>
<item handle="a520879ca0b45" parent="e5e47ebf63b1c" root="e5e47ebf63b1c" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="104" />
<meta expanded="no" heading="H2" charCount="299" wordCount="55" paraCount="2" cursorPos="387" />
<name status="s90e6c9" import="ia857f0" active="yes">Chapter One</name>
</item>
<item handle="f6622b4617424" parent="None" root="f6622b4617424" order="2" type="ROOT" class="CHARACTER">
@@ -102,11 +102,11 @@
<name status="sf12341" import="ia857f0">Main Characters</name>
</item>
<item handle="14298de4d9524" parent="f7e2d9f330615" root="f6622b4617424" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="24" />
<meta expanded="no" heading="H1" charCount="49" wordCount="9" paraCount="1" cursorPos="65" />
<name status="sf12341" import="icfb3a5" active="yes">John Smith</name>
</item>
<item handle="bb2c23b3c42cc" parent="f7e2d9f330615" root="f6622b4617424" order="1" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="25" />
<meta expanded="no" heading="H1" charCount="55" wordCount="9" paraCount="1" cursorPos="71" />
<name status="sf12341" import="i2d7a54" active="yes">Jane Smith</name>
</item>
<item handle="15c4492bd5107" parent="None" root="15c4492bd5107" order="3" type="ROOT" class="WORLD">
@@ -114,15 +114,15 @@
<name status="sf12341" import="ia857f0">Locations</name>
</item>
<item handle="b3e74dbc1f584" parent="15c4492bd5107" root="15c4492bd5107" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="20" />
<meta expanded="no" heading="H1" charCount="76" wordCount="15" paraCount="1" cursorPos="111" />
<name status="sf12341" import="i56be10" active="yes">Earth</name>
</item>
<item handle="f1471bef9f2ae" parent="15c4492bd5107" root="15c4492bd5107" order="1" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="133" />
<meta expanded="no" heading="H1" charCount="115" wordCount="24" paraCount="1" cursorPos="135" />
<name status="sf12341" import="icfb3a5" active="yes">Space</name>
</item>
<item handle="5eaea4e8cdee8" parent="15c4492bd5107" root="15c4492bd5107" order="2" type="FILE" class="WORLD" layout="NOTE">
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="45" />
<meta expanded="no" heading="H1" charCount="28" wordCount="6" paraCount="1" cursorPos="62" />
<name status="sf12341" import="i2d7a54" active="yes">Mars</name>
</item>
<item handle="6827118336ac1" parent="None" root="6827118336ac1" order="4" type="ROOT" class="ARCHIVE">