Example #1
0
    def _populateList(self):
        """Get the item selected in the tree, check that it is a folder,
        and try to find all files associated with it. The valid files
        are then added to the list view in order. The list itself can be
        reordered by the user.
        """
        if self.sourceItem is None:
            self.sourceItem = self.theParent.treeView.getSelectedHandle()

        if self.sourceItem is None:
            return

        nwItem = self.theProject.projTree[self.sourceItem]
        if nwItem is None:
            return
        if nwItem.itemType is not nwItemType.FILE:
            self.theParent.makeAlert(
                self.tr(
                    "Element selected in the project tree must be a file."),
                nwAlert.ERROR)
            return

        self.listBox.clear()
        theDoc = NWDoc(self.theProject, self.theParent)
        theText = theDoc.openDocument(self.sourceItem, False)

        spLevel = self.splitLevel.currentData()
        self.optState.setValue("GuiDocSplit", "spLevel", spLevel)
        logger.debug("Scanning document %s for headings level <= %d" %
                     (self.sourceItem, spLevel))

        lineNo = 0
        for aLine in theText.splitlines():

            lineNo += 1
            onLine = 0

            if aLine.startswith("# ") and spLevel >= 1:
                onLine = lineNo
            elif aLine.startswith("## ") and spLevel >= 2:
                onLine = lineNo
            elif aLine.startswith("### ") and spLevel >= 3:
                onLine = lineNo
            elif aLine.startswith("#### ") and spLevel >= 4:
                onLine = lineNo

            if onLine > 0:
                newItem = QListWidgetItem()
                newItem.setText(aLine.strip())
                newItem.setData(Qt.UserRole, onLine)
                self.listBox.addItem(newItem)

        return
Example #2
0
    def rebuildIndex(self, beQuiet=False):
        """Rebuild the entire index.
        """
        if not self.hasProject:
            return False

        logger.debug("Rebuilding index ...")
        qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
        tStart = time()

        self.treeView.saveTreeOrder()
        self.theIndex.clearIndex()

        theDoc = NWDoc(self.theProject, self)
        for nDone, tItem in enumerate(self.theProject.projTree):

            if tItem is not None:
                self.setStatus("Indexing: '%s'" % tItem.itemName)
            else:
                self.setStatus("Indexing: Unknown item")

            if tItem is not None and tItem.itemType == nwItemType.FILE:
                logger.verbose("Scanning: %s" % tItem.itemName)
                theText = theDoc.openDocument(tItem.itemHandle,
                                              showStatus=False)

                # Build tag index
                self.theIndex.scanText(tItem.itemHandle, theText)

                # Get Word Counts
                cC, wC, pC = self.theIndex.getCounts(tItem.itemHandle)
                tItem.setCharCount(cC)
                tItem.setWordCount(wC)
                tItem.setParaCount(pC)
                self.treeView.propagateCount(tItem.itemHandle, wC)
                self.treeView.projectWordCount()

        tEnd = time()
        self.setStatus("Indexing completed in %.1f ms" %
                       ((tEnd - tStart) * 1000.0))
        self.docEditor.updateTagHighLighting()
        qApp.restoreOverrideCursor()

        if not beQuiet:
            self.makeAlert("The project index has been successfully rebuilt.",
                           nwAlert.INFO)

        return True
Example #3
0
def testCoreDocument_Methods(monkeypatch, dummyGUI, nwMinimal):
    """Test other methods of the NWDoc class.
    """
    theProject = NWProject(dummyGUI)
    assert theProject.openProject(nwMinimal)
    assert theProject.projPath == nwMinimal

    theDoc = NWDoc(theProject, dummyGUI)
    sHandle = "8c659a11cd429"
    docPath = os.path.join(nwMinimal, "content", sHandle+".nwd")

    assert theDoc.openDocument(sHandle) == "### New Scene\n\n"

    # Check location
    assert theDoc.getFileLocation() == docPath

    # Check the item
    assert theDoc.getCurrentItem() is not None
    assert theDoc.getCurrentItem().itemHandle == sHandle

    # Check the meta
    theName, theParent, theClass, theLayout = theDoc.getMeta()
    assert theName == "New Scene"
    assert theParent == "a6d311a93600a"
    assert theClass == nwItemClass.NOVEL
    assert theLayout == nwItemLayout.SCENE

    # Add meta data garbage
    assert theDoc.saveDocument("%%~ stuff\n### Test File\n\nText ...\n\n")
    with open(docPath, mode="r", encoding="utf8") as inFile:
        assert inFile.read() == (
            "%%~name: New Scene\n"
            f"%%~path: a6d311a93600a/{sHandle}\n"
            "%%~kind: NOVEL/SCENE\n"
            "%%~ stuff\n"
            "### Test File\n\n"
            "Text ...\n\n"
        )

    assert theDoc.openDocument(sHandle) == "### Test File\n\nText ...\n\n"
Example #4
0
    def _doMerge(self):
        """Perform the merge of the files in the selected folder, and
        create a new file in the same parent folder. The old files are
        not removed in the merge process, and must be deleted manually.
        """
        logger.verbose("GuiDocMerge merge button clicked")

        finalOrder = []
        for i in range(self.listBox.count()):
            finalOrder.append(self.listBox.item(i).data(Qt.UserRole))

        if len(finalOrder) == 0:
            self.theParent.makeAlert((
                "No source documents found. Nothing to do."
            ), nwAlert.ERROR)
            return

        theDoc = NWDoc(self.theProject, self.theParent)
        theText = ""
        for tHandle in finalOrder:
            theText += theDoc.openDocument(tHandle, False).rstrip("\n")
            theText += "\n\n"

        if self.sourceItem is None:
            self.theParent.makeAlert((
                "No source document selected. Nothing to do."
            ), nwAlert.ERROR)
            return

        srcItem = self.theProject.projTree[self.sourceItem]
        if srcItem is None:
            self.theParent.makeAlert((
                "Could not parse source document."
            ), nwAlert.ERROR)
            return

        nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
        newItem = self.theProject.projTree[nHandle]
        newItem.setStatus(srcItem.itemStatus)

        theDoc.openDocument(nHandle, False)
        theDoc.saveDocument(theText)
        self.theParent.treeView.revealNewTreeItem(nHandle)
        self.theParent.openDocument(nHandle, doScroll=True)

        self._doClose()

        return
Example #5
0
    def _doSplit(self):
        """Perform the split of the file, create a new folder in the
        same parent folder, and multiple files depending on split level
        settings. The old file is not removed in the split process, and
        must be deleted manually.
        """
        logger.verbose("GuiDocSplit split button clicked")

        if self.sourceItem is None:
            self.theParent.makeAlert(
                ("No source document selected. Nothing to do."), nwAlert.ERROR)
            return

        srcItem = self.theProject.projTree[self.sourceItem]
        if srcItem is None:
            self.theParent.makeAlert(("Could not parse source document."),
                                     nwAlert.ERROR)
            return

        theDoc = NWDoc(self.theProject, self.theParent)
        theText = theDoc.openDocument(self.sourceItem, False)
        theLines = theText.splitlines()
        nLines = len(theLines)
        theLines.insert(0, "%Split Doc")
        logger.debug("Splitting document %s with %d lines" %
                     (self.sourceItem, nLines))

        finalOrder = []
        for i in range(self.listBox.count()):
            listItem = self.listBox.item(i)
            wTitle = listItem.text()
            lineNo = listItem.data(Qt.UserRole)
            finalOrder.append([wTitle, lineNo, nLines])
            if i > 0:
                finalOrder[i - 1][2] = lineNo

        nFiles = len(finalOrder)
        if nFiles == 0:
            self.theParent.makeAlert(("No headers found. Nothing to do."),
                                     nwAlert.ERROR)
            return

        # Check that another folder can be created
        parTree = self.theProject.projTree.getItemPath(srcItem.itemParent)
        if len(parTree) >= nwConst.MAX_DEPTH - 1:
            self.theParent.makeAlert(
                ("Cannot add new folder for the document split. "
                 "Maximum folder depth has been reached. "
                 "Please move the file to another level in the project tree."),
                nwAlert.ERROR)
            return

        msgYes = self.theParent.askQuestion(
            "Split Document",
            ("The document will be split into %d file(s) in a new folder. "
             "The original document will remain intact.<br><br>"
             "Continue with the splitting process?") % nFiles)
        if not msgYes:
            return

        # Create the folder
        fHandle = self.theProject.newFolder(srcItem.itemName,
                                            srcItem.itemClass,
                                            srcItem.itemParent)
        self.theParent.treeView.revealNewTreeItem(fHandle)
        logger.verbose("Creating folder %s" % fHandle)

        # Loop through, and create the files
        for wTitle, iStart, iEnd in finalOrder:

            itemLayout = nwItemLayout.NOTE
            if srcItem.itemClass == nwItemClass.NOVEL:
                if wTitle.startswith("# "):
                    itemLayout = nwItemLayout.PARTITION
                elif wTitle.startswith("## "):
                    itemLayout = nwItemLayout.CHAPTER
                elif wTitle.startswith("### "):
                    itemLayout = nwItemLayout.SCENE
                elif wTitle.startswith("#### "):
                    itemLayout = nwItemLayout.SCENE

            wTitle = wTitle.lstrip("#")
            wTitle = wTitle.strip()

            nHandle = self.theProject.newFile(wTitle, srcItem.itemClass,
                                              fHandle)
            newItem = self.theProject.projTree[nHandle]
            newItem.setLayout(itemLayout)
            newItem.setStatus(srcItem.itemStatus)
            logger.verbose(
                "Creating new document %s with text from line %d to %d" %
                (nHandle, iStart, iEnd - 1))

            theText = "\n".join(theLines[iStart:iEnd])
            theText = theText.rstrip("\n") + "\n\n"
            theDoc.openDocument(nHandle, False)
            theDoc.saveDocument(theText)
            theDoc.clearDocument()
            self.theParent.treeView.revealNewTreeItem(nHandle)

        self._doClose()

        return
def testCoreToken_TextOps(monkeypatch, nwMinimal, dummyGUI):
    """Test handling files and text in the Tokenizer class.
    """
    theProject = NWProject(dummyGUI)
    theProject.projTree.setSeed(42)
    theProject.projLang = "en"
    theProject._loadProjectLocalisation()

    theToken = Tokenizer(theProject, dummyGUI)
    theToken.setKeepMarkdown(True)

    assert theProject.openProject(nwMinimal)
    sHandle = "8c659a11cd429"

    # Set some content to work with

    docText = (
        "### Scene Six\n\n"
        "This is text with _italic text_, some **bold text**, some ~~deleted text~~, "
        "and some **_mixed text_** and **some _nested_ text**.\n\n"
        "#### Replace\n\n"
        "Also, replace <A> and <B>.\n\n"
    )
    docTextR = docText.replace("<A>", "this").replace("<B>", "that")

    nDoc = NWDoc(theProject, dummyGUI)
    nDoc.openDocument(sHandle)
    nDoc.saveDocument(docText)
    nDoc.clearDocument()

    theProject.setAutoReplace({"A": "this", "B": "that"})

    assert theProject.saveProject()

    # Root heading
    assert theToken.addRootHeading("dummy") is False
    assert theToken.addRootHeading(sHandle) is False
    assert theToken.addRootHeading("7695ce551d265") is True
    assert theToken.theMarkdown[-1] == "# Notes: Plot\n\n"

    # Set text
    assert theToken.setText("dummy") is False
    assert theToken.setText(sHandle) is True
    assert theToken.theText == docText

    with monkeypatch.context() as mp:
        mp.setattr("nw.constants.nwConst.MAX_DOCSIZE", 100)
        assert theToken.setText(sHandle, docText) is True
        assert theToken.theText == (
            "# ERROR\n\n"
            "Document 'New Scene' is too big (0.00 MB). Skipping.\n\n"
        )

    assert theToken.setText(sHandle, docText) is True
    assert theToken.theText == docText

    assert theToken.isNone is False
    assert theToken.isTitle is False
    assert theToken.isBook is False
    assert theToken.isPage is False
    assert theToken.isPart is False
    assert theToken.isUnNum is False
    assert theToken.isChap is False
    assert theToken.isScene is True
    assert theToken.isNote is False
    assert theToken.isNovel is True

    # Pre Processing
    theToken.doPreProcessing()
    assert theToken.theText == docTextR

    # 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"
Example #7
0
    def deleteItem(self, tHandle=None, alreadyAsked=False, askForTrash=False):
        """Delete an item from the project tree. As a first step, files are
        moved to the Trash folder. Permanent deletion is a second step. This
        second step also deletes the item from the project object as well as
        delete the files on disk. Folders are deleted if they're empty only,
        and the deletion is always permanent.
        """
        if not self.theParent.hasProject:
            logger.error("No project open")
            return False

        if tHandle is None:
            tHandle = self.getSelectedHandle()

        if tHandle is None:
            return False

        trItemS = self._getTreeItem(tHandle)
        nwItemS = self.theProject.projTree[tHandle]

        if trItemS is None or nwItemS is None:
            return False

        wCount = int(trItemS.data(self.C_COUNT, Qt.UserRole))
        if nwItemS.itemType == nwItemType.FILE:
            logger.debug("User requested file %s moved to trash" % tHandle)
            trItemP = trItemS.parent()
            trItemT = self._addTrashRoot()
            if trItemP is None or trItemT is None:
                logger.error("Could not delete item")
                return False

            pHandle = nwItemS.itemParent
            if self.theProject.projTree.isTrashRoot(pHandle):
                # If the file is in the trash folder already, as the
                # user if they want to permanently delete the file.
                doPermanent = False
                if not alreadyAsked:
                    msgYes = self.theParent.askQuestion(
                        "Delete File", "Permanently delete file '%s'?" % nwItemS.itemName
                    )
                    if msgYes:
                        doPermanent = True
                else:
                    doPermanent = True

                if doPermanent:
                    logger.debug("Permanently deleting file with handle %s" % tHandle)

                    self.propagateCount(tHandle, 0)
                    tIndex = trItemP.indexOfChild(trItemS)
                    trItemC = trItemP.takeChild(tIndex)

                    if self.theParent.docEditor.theHandle == tHandle:
                        self.theParent.closeDocument()

                    theDoc = NWDoc(self.theProject, self.theParent)
                    theDoc.deleteDocument(tHandle)
                    del self.theProject.projTree[tHandle]
                    self.theIndex.deleteHandle(tHandle)

            else:
                # The file is not already in the trash folder, so we
                # move it there.
                doTrash = False
                if askForTrash:
                    msgYes = self.theParent.askQuestion(
                        "Delete File", "Move file '%s' to Trash?" % nwItemS.itemName
                    )
                    if msgYes:
                        doTrash = True
                else:
                    doTrash = True

                if doTrash:
                    if pHandle is None:
                        logger.warning("File has no parent item")

                    self.propagateCount(tHandle, 0)
                    tIndex  = trItemP.indexOfChild(trItemS)
                    trItemC = trItemP.takeChild(tIndex)
                    trItemT.addChild(trItemC)
                    nwItemS.setParent(self.theProject.projTree.trashRoot())
                    self.propagateCount(tHandle, wCount)

                    self._setTreeChanged(True)
                    self.theIndex.deleteHandle(tHandle)

        elif nwItemS.itemType == nwItemType.FOLDER:
            logger.debug("User requested folder %s deleted" % tHandle)
            trItemP = trItemS.parent()
            if trItemP is None:
                logger.error("Could not delete folder")
                return False
            tIndex = trItemP.indexOfChild(trItemS)
            if trItemS.childCount() == 0:
                trItemP.takeChild(tIndex)
                del self.theProject.projTree[tHandle]
            else:
                self.makeAlert((
                    "Cannot delete folder. It is not empty. "
                    "Recursive deletion is not supported. "
                    "Please delete the content first."
                ), nwAlert.ERROR)
                return False

        elif nwItemS.itemType == nwItemType.ROOT:
            logger.debug("User requested root folder %s deleted" % tHandle)
            tIndex = self.indexOfTopLevelItem(trItemS)
            if trItemS.childCount() == 0:
                self.takeTopLevelItem(tIndex)
                del self.theProject.projTree[tHandle]
                self.theParent.mainMenu.setAvailableRoot()
                self._setTreeChanged(True)
            else:
                self.makeAlert((
                    "Cannot delete root folder. It is not empty. "
                    "Recursive deletion is not supported. "
                    "Please delete the content first."
                ), nwAlert.ERROR)
                return False

        return True
Example #8
0
def testCoreDocument_LoadSave(monkeypatch, dummyGUI, nwMinimal):
    """Test loading and saving a document with the NWDoc class.
    """
    theProject = NWProject(dummyGUI)
    assert theProject.openProject(nwMinimal)
    assert theProject.projPath == nwMinimal

    theDoc = NWDoc(theProject, dummyGUI)
    sHandle = "8c659a11cd429"

    # Not a valid handle
    assert theDoc.openDocument("dummy") is None

    # Non-existent handle
    assert theDoc.openDocument("0000000000000") is None

    # Cause open() to fail while loading
    def dummyOpen(*args, **kwargs):
        raise OSError

    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", dummyOpen)
        assert theDoc.openDocument(sHandle) is None

    # Load the text
    assert theDoc.openDocument(sHandle) == "### New Scene\n\n"

    # Try to open a new (non-existent) file
    nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
    assert nHandle is not None
    xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
    assert theDoc.openDocument(xHandle) == ""

    # Check cached item
    assert isinstance(theDoc._theItem, NWItem)
    assert theDoc.openDocument(xHandle, isOrphan=True) == ""
    assert theDoc._theItem is None

    # Set handle and save again
    theText = "### Test File\n\nText ...\n\n"
    assert theDoc.openDocument(xHandle) == ""
    assert theDoc.saveDocument(theText)

    # Save again to ensure temp file and previous file is handled
    assert theDoc.saveDocument(theText)

    # Check file content
    docPath = os.path.join(nwMinimal, "content", xHandle + ".nwd")
    with open(docPath, mode="r", encoding="utf8") as inFile:
        assert inFile.read() == ("%%~name: New File\n"
                                 f"%%~path: a508bb932959c/{xHandle}\n"
                                 "%%~kind: NOVEL/SCENE\n"
                                 "### Test File\n\n"
                                 "Text ...\n\n")

    # Force no meta data
    theDoc._theItem = None
    assert theDoc.saveDocument(theText)

    with open(docPath, mode="r", encoding="utf8") as inFile:
        assert inFile.read() == theText

    # Cause open() to fail while saving
    with monkeypatch.context() as mp:
        mp.setattr("builtins.open", causeOSError)
        assert not theDoc.saveDocument(theText)

    # Saving with no handle
    theDoc.clearDocument()
    assert not theDoc.saveDocument(theText)

    # Delete the last document
    assert not theDoc.deleteDocument("dummy")
    assert os.path.isfile(docPath)

    # Cause the delete to fail
    with monkeypatch.context() as mp:
        mp.setattr("os.unlink", causeOSError)
        assert not theDoc.deleteDocument(xHandle)

    # Make the delete pass
    assert theDoc.deleteDocument(xHandle)
    assert not os.path.isfile(docPath)
Example #9
0
    def newTreeItem(self, itemType, itemClass):
        """Add new item to the tree, with a given itemType and
        itemClass, and attach it to the selected handle. Also make sure
        the item is added in a place it can be added, and that other
        meta data is set correctly to ensure a valid project tree.
        """
        pHandle = self.getSelectedHandle()
        nHandle = None

        if not self.theParent.hasProject:
            logger.error("No project open")
            return False

        if not isinstance(itemType, nwItemType):
            # This would indicate an internal bug
            logger.error("No itemType provided")
            return False

        # The item needs to be assigned an item class, so one must be
        # provided, or it must be possible to extract it from the parent
        # item of the new item.
        if itemClass is None and pHandle is not None:
            pItem = self.theProject.projTree[pHandle]
            if pItem is not None:
                itemClass = pItem.itemClass

        # If class is still not set, alert the user and exit
        if itemClass is None:
            if itemType == nwItemType.FILE:
                self.makeAlert(
                    self.
                    tr("Please select a valid location in the tree to add the document."
                       ), nwAlert.ERROR)
            else:
                self.makeAlert(
                    self.
                    tr("Please select a valid location in the tree to add the folder."
                       ), nwAlert.ERROR)
            return False

        # Everything is fine, we have what we need, so we proceed
        logger.verbose("Adding new item of type %s and class %s to handle %s" %
                       (itemType.name, itemClass.name, str(pHandle)))

        if itemType == nwItemType.ROOT:
            tHandle = self.theProject.newRoot(
                trConst(nwLabels.CLASS_NAME[itemClass]), itemClass)
            if tHandle is None:
                logger.error("No root item added")
                return False

        else:
            # If no parent has been selected, make the new file under
            # the root NOVEL item.
            if pHandle is None:
                pHandle = self.theProject.projTree.findRoot(nwItemClass.NOVEL)

            # If still nothing, give up
            if pHandle is None:
                self.makeAlert(
                    self.tr(
                        "Did not find anywhere to add the file or folder!"),
                    nwAlert.ERROR)
                return False

            # Now check if the selected item is a file, in which case
            # the new file will be a sibling
            pItem = self.theProject.projTree[pHandle]
            if pItem.itemType == nwItemType.FILE:
                nHandle = pHandle
                pHandle = pItem.itemParent

            # If we again have no home, give up
            if pHandle is None:
                self.makeAlert(
                    self.tr(
                        "Did not find anywhere to add the file or folder!"),
                    nwAlert.ERROR)
                return False

            if self.theProject.projTree.isTrashRoot(pHandle):
                self.makeAlert(
                    self.
                    tr("Cannot add new files or folders to the Trash folder."),
                    nwAlert.ERROR)
                return False

            parTree = self.theProject.projTree.getItemPath(pHandle)

            # If we're still here, add the file or folder
            if itemType == nwItemType.FILE:
                tHandle = self.theProject.newFile(self.tr("New File"),
                                                  itemClass, pHandle)

            elif itemType == nwItemType.FOLDER:
                if len(parTree) >= nwConst.MAX_DEPTH - 1:
                    # Folders cannot be deeper than MAX_DEPTH - 1, leaving room
                    # for one more level of files.
                    self.makeAlert(
                        (self.tr("Cannot add new folder to this item."),
                         self.tr("Maximum folder depth has been reached.")),
                        nwAlert.ERROR)
                    return False
                tHandle = self.theProject.newFolder(self.tr("New Folder"),
                                                    itemClass, pHandle)

            else:
                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
        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