diff --git a/bin/test_io.py b/bin/test_io.py new file mode 100644 index 00000000..808c83c9 --- /dev/null +++ b/bin/test_io.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os +import sys + +realpath = os.path.realpath(__file__) + +sys.path.insert(1, os.path.join(os.path.dirname(realpath), '..')) + +import manuskript.data as data +import manuskript.load_save.version_1 as v1 + +path = os.path.join(sys.path[1], "sample-projects/book-of-acts") + +import time + +start = time.time() + +project = data.Project(path + ".msk") +project.load() + +end = time.time() +duration = end - start + +print(duration) + +project.save() diff --git a/manuskript/data/__init__.py b/manuskript/data/__init__.py new file mode 100644 index 00000000..0ce564b9 --- /dev/null +++ b/manuskript/data/__init__.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from manuskript.data.characters import Characters, Character +from manuskript.data.color import Color +from manuskript.data.goal import GoalKind, Goal +from manuskript.data.info import Info +from manuskript.data.labels import LabelHost, Label +from manuskript.data.outline import Outline, OutlineFolder, OutlineText +from manuskript.data.plots import Plots, PlotLine, PlotStep +from manuskript.data.project import Project +from manuskript.data.revisions import Revisions +from manuskript.data.settings import Settings +from manuskript.data.status import StatusHost, Status +from manuskript.data.unique_id import UniqueIDHost, UniqueID diff --git a/manuskript/data/characters.py b/manuskript/data/characters.py new file mode 100644 index 00000000..a78ac18b --- /dev/null +++ b/manuskript/data/characters.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from collections import OrderedDict +from manuskript.data.color import Color +from manuskript.data.unique_id import UniqueIDHost +from manuskript.io.mmdFile import MmdFile + + +class Character: + + def __init__(self, path, characters): + self.file = MmdFile(path, 21) + self.characters = characters + + self.UID = None + self.name = None + self.importance = None + self.POV = None + self.motivation = None + self.goal = None + self.conflict = None + self.epiphany = None + self.summarySentence = None + self.summaryParagraph = None + self.summaryFull = None + self.notes = None + self.color = None + self.details = dict() + + def allowPOV(self) -> bool: + return True if self.POV is None else self.POV + + @classmethod + def loadAttribute(cls, metadata: dict, name: str, defaultValue=None): + if name in metadata: + return metadata.pop(name) + else: + return defaultValue + + def load(self): + metadata, _ = self.file.loadMMD(True) + + ID = Character.loadAttribute(metadata, "ID") + + if ID is None: + raise IOError("Character is missing ID!") + + self.UID = self.characters.host.loadID(int(ID)) + self.name = Character.loadAttribute(metadata, "Name", None) + self.importance = Character.loadAttribute(metadata, "Importance", None) + self.POV = Character.loadAttribute(metadata, "POV", None) + self.motivation = Character.loadAttribute(metadata, "Motivation", None) + self.goal = Character.loadAttribute(metadata, "Goal", None) + self.conflict = Character.loadAttribute(metadata, "Conflict", None) + self.epiphany = Character.loadAttribute(metadata, "Epiphany", None) + self.summarySentence = Character.loadAttribute(metadata, "Phrase Summary", None) + self.summaryParagraph = Character.loadAttribute(metadata, "Paragraph Summary", None) + self.summaryFull = Character.loadAttribute(metadata, "Full Summary", None) + self.notes = Character.loadAttribute(metadata, "Notes", None) + self.color = Color.parse(Character.loadAttribute(metadata, "Color", None)) + + self.details.clear() + + for (key, value) in metadata.items(): + self.details[key] = value + + def save(self): + metadata = OrderedDict() + + metadata["Name"] = self.name + metadata["ID"] = str(self.UID.value) + metadata["Importance"] = self.importance + metadata["POV"] = self.POV + metadata["Motivation"] = self.motivation + metadata["Goal"] = self.goal + metadata["Conflict"] = self.conflict + metadata["Epiphany"] = self.epiphany + metadata["Phrase Summary"] = self.summarySentence + metadata["Paragraph Summary"] = self.summaryParagraph + metadata["Full Summary"] = self.summaryFull + metadata["Notes"] = self.notes + metadata["Color"] = self.color + + for (key, value) in self.details.items(): + if not (key in metadata): + metadata[key] = value + + self.file.save((metadata, None)) + + +class Characters: + + def __init__(self, path): + self.dir_path = os.path.join(path, "characters") + self.host = UniqueIDHost() + self.characters = list() + + def load(self): + self.characters.clear() + + for name in os.listdir(self.dir_path): + path = os.path.join(self.dir_path, name) + + if not os.path.isfile(path): + continue + + character = Character(path, self) + + try: + character.load() + except FileNotFoundError: + continue + + self.characters.append(character) + + def save(self): + for character in self.characters: + character.save() diff --git a/manuskript/data/color.py b/manuskript/data/color.py new file mode 100644 index 00000000..12c5f2af --- /dev/null +++ b/manuskript/data/color.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import re + + +class Color: + + def __init__(self, red: int, green: int, blue: int): + self.red = red + self.green = green + self.blue = blue + + def __str__(self): + return "#%02x%02x%02x" % (self.red, self.green, self.blue) + + @classmethod + def parse(cls, string: str): + colorPattern = re.compile(r"\#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})") + + m = colorPattern.match(string) + + if m is None: + return None + + return Color(int(m.group(1), 16), int(m.group(2), 16), int(m.group(3), 16)) diff --git a/manuskript/data/goal.py b/manuskript/data/goal.py new file mode 100644 index 00000000..8a128136 --- /dev/null +++ b/manuskript/data/goal.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from enum import Enum, unique + + +@unique +class GoalKind(Enum): + WORDS = 0 + CHARACTERS = 1 + + +class Goal: + + def __init__(self, value: int = 0, kind: GoalKind = GoalKind.WORDS): + self.value = value + self.kind = kind + + def __str__(self): + if self.kind != GoalKind.WORDS: + return str(self.value) + " " + self.kind.name.lower() + else: + return str(self.value) + + @classmethod + def parse(cls, string: str): + if string is None: + return None + + parts = string.split(" ") + + try: + value = int(parts[0]) + kind = GoalKind[parts[1].upper()] if len(parts) > 1 else GoalKind.WORDS + except ValueError: + return None + + return Goal(value, kind) diff --git a/manuskript/data/info.py b/manuskript/data/info.py new file mode 100644 index 00000000..3758733e --- /dev/null +++ b/manuskript/data/info.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from manuskript.io.mmdFile import MmdFile + + +class Info: + + def __init__(self, path): + self.file = MmdFile(os.path.join(path, "infos.txt"), 16) + + self.title = None + self.subtitle = None + self.serie = None + self.volume = None + self.genre = None + self.license = None + self.author = None + self.email = None + + def load(self): + try: + metadata, _ = self.file.loadMMD(True) + except FileNotFoundError: + metadata = dict() + + self.title = metadata.get("Title", None) + self.subtitle = metadata.get("Subtitle", None) + self.serie = metadata.get("Serie", None) + self.volume = metadata.get("Volume", None) + self.genre = metadata.get("Genre", None) + self.license = metadata.get("License", None) + self.author = metadata.get("Author", None) + self.email = metadata.get("Email", None) + + def save(self): + metadata = dict() + + metadata["Title"] = self.title + metadata["Subtitle"] = self.subtitle + metadata["Serie"] = self.serie + metadata["Volume"] = self.volume + metadata["Genre"] = self.genre + metadata["License"] = self.license + metadata["Author"] = self.author + metadata["Email"] = self.email + + self.file.save((metadata, None)) diff --git a/manuskript/data/labels.py b/manuskript/data/labels.py new file mode 100644 index 00000000..556f6041 --- /dev/null +++ b/manuskript/data/labels.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from manuskript.data.color import Color +from manuskript.io.mmdFile import MmdFile + + +class Label: + + def __init__(self, host, name: str, color: Color): + self.host = host + + self.name = name + self.color = color + + def __str__(self): + return self.name + + def load(self): + self.host.load() + + def save(self): + self.host.save() + + +class LabelHost: + + def __init__(self, path): + self.file = MmdFile(os.path.join(path, "labels.txt"), 21) + self.labels = dict() + + def addLabel(self, name: str, color: Color): + self.labels[name] = Label(self, name, color) + + def removeLabel(self, name: str): + self.labels.pop(name) + + def renameLabel(self, oldName: str, newName: str): + label = self.labels.get(oldName) + label.name = newName + self.labels[newName] = label + self.labels.pop(oldName) + + def getLabel(self, name: str): + return self.labels.get(name) + + def __iter__(self): + return self.labels.values().__iter__() + + def load(self): + try: + metadata, _ = self.file.loadMMD(True) + self.labels.clear() + except FileNotFoundError: + self.labels.clear() + return + + for (name, value) in metadata.items(): + if value is None: + continue + + self.addLabel(name, Color.parse(value)) + + def save(self): + metadata = dict() + + for (name, label) in self.labels.items(): + metadata[name] = label.color + + self.file.save((metadata, None)) diff --git a/manuskript/data/outline.py b/manuskript/data/outline.py new file mode 100644 index 00000000..e3df7c57 --- /dev/null +++ b/manuskript/data/outline.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from collections import OrderedDict +from enum import Enum, unique +from manuskript.data.goal import Goal +from manuskript.data.unique_id import UniqueIDHost +from manuskript.io.mmdFile import MmdFile + + +@unique +class OutlineState(Enum): + UNDEFINED = 0, + OPTIMIZED = 1, + COMPLETE = 2 + + +class OutlineItem: + + def __init__(self, path, outline): + self.file = MmdFile(path) + self.outline = outline + self.state = OutlineState.UNDEFINED + + self.UID = None + self.title = "" + self.type = "" + self.summarySentence = None + self.summaryFull = None + self.POV = None + self.notes = None + self.label = None + self.status = None + self.compile = True + self.goal = None + + @classmethod + def loadMetadata(cls, item, metadata: dict): + ID = metadata.get("ID") + + if ID is None: + return + + item.UID = item.outline.host.loadID(int(ID)) + item.title = metadata.get("title", None) + item.type = metadata.get("type", "md") + item.summarySentence = metadata.get("summarySentence", None) + item.summaryFull = metadata.get("summaryFull", None) + item.POV = metadata.get("POV", None) + item.notes = metadata.get("notes", None) + item.label = metadata.get("label", None) + item.status = metadata.get("status", None) + item.compile = metadata.get("compile") + item.goal = Goal.parse(metadata.get("setGoal", None)) + + @classmethod + def saveMetadata(cls, item): + metadata = OrderedDict() + + if item.UID is None: + return metadata + + metadata["title"] = item.title + metadata["ID"] = str(item.UID.value) + metadata["type"] = item.type + metadata["summarySentence"] = item.summarySentence + metadata["summaryFull"] = item.summaryFull + metadata["POV"] = item.POV + metadata["notes"] = item.notes + metadata["label"] = item.label + metadata["status"] = item.status + metadata["compile"] = item.compile + metadata["setGoal"] = item.goal + + return metadata + + def load(self, optimized: bool = True): + raise IOError('Loading undefined!') + + def save(self): + raise IOError('Saving undefined!') + + +class OutlineText(OutlineItem): + + def __init__(self, path, outline): + OutlineItem.__init__(self, path, outline) + + self.text = "" + + def load(self, optimized: bool = True): + metadata, body = self.file.loadMMD(optimized) + OutlineItem.loadMetadata(self, metadata) + + if not optimized: + self.text = body + self.state = OutlineState.COMPLETE + elif self.state == OutlineState.UNDEFINED: + self.state = OutlineState.OPTIMIZED + + def save(self): + if self.state == OutlineState.OPTIMIZED: + self.outline.host.removeID(self.UID) + self.load(False) + + metadata = OutlineItem.saveMetadata(self) + self.file.save((metadata, self.text)) + + +class OutlineFolder(OutlineItem): + + def __init__(self, path, outline): + self.dir_path = path + self.items = list() + + OutlineItem.__init__(self, os.path.join(self.dir_path, "folder.txt"), outline) + + def __iter__(self): + return self.items.__iter__() + + @classmethod + def loadItems(cls, outline, folder, recursive: bool = True): + folder.items.clear() + + names = os.listdir(folder.dir_path) + names.remove("folder.txt") + + for name in names: + path = os.path.join(folder.dir_path, name) + + if os.path.isdir(path): + item = OutlineFolder(path, outline) + else: + item = OutlineText(path, outline) + + try: + item.load() + except FileNotFoundError: + continue + + folder.items.append(item) + + if recursive: + for item in folder.items: + if type(item) is OutlineFolder: + cls.loadItems(outline, item, recursive) + + def load(self, _: bool = True): + metadata, _ = self.file.loadMMD(True) + OutlineItem.loadMetadata(self, metadata) + self.state = OutlineState.COMPLETE + + @classmethod + def saveItems(cls, folder, recursive: bool = True): + for item in folder.items: + item.save() + + if recursive: + for item in folder.items: + if type(item) is OutlineFolder: + cls.saveItems(item, recursive) + + def save(self): + self.type = "folder" + metadata = OutlineItem.saveMetadata(self) + self.file.save((metadata, "\n")) + + +class Outline: + + def __init__(self, path): + self.dir_path = os.path.join(path, "outline") + self.host = UniqueIDHost() + self.items = list() + + def __iter__(self): + return self.items.__iter__() + + def all(self): + result = list() + queue = list(self.items) + + while len(queue) > 0: + item = queue.pop() + + if type(item) is OutlineFolder: + for child in item: + queue.append(child) + + result.append(item) + + return result + + def load(self): + self.items.clear() + + for name in os.listdir(self.dir_path): + path = os.path.join(self.dir_path, name) + + if os.path.isdir(path): + item = OutlineFolder(path, self) + else: + item = OutlineText(path, self) + + try: + item.load() + except FileNotFoundError: + continue + + self.items.append(item) + + for item in self.items: + if type(item) is OutlineFolder: + OutlineFolder.loadItems(self, item, True) + + def save(self): + for item in self.items: + item.save() + + for item in self.items: + if type(item) is OutlineFolder: + OutlineFolder.saveItems(item, True) diff --git a/manuskript/data/plots.py b/manuskript/data/plots.py new file mode 100644 index 00000000..ae439b37 --- /dev/null +++ b/manuskript/data/plots.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from lxml import etree +from enum import Enum, unique +from manuskript.data.unique_id import UniqueIDHost, UniqueID +from manuskript.io.xmlFile import XmlFile + + +@unique +class PlotImportance(Enum): + MINOR = 0 + SECONDARY = 1 + MAIN = 2 + + +class PlotStep: + + def __init__(self, plot, UID: UniqueID, name: str, meta: str = "", summary: str = ""): + self.plot = plot + + self.UID = UID + self.name = name + self.meta = meta + self.summary = summary + + def save(self): + self.plot.save() + + +class PlotLine: + + def __init__(self, plots, UID: UniqueID, name: str, importance: PlotImportance = PlotImportance.MINOR): + self.plots = plots + self.host = UniqueIDHost() + + self.UID = UID + self.name = name + self.importance = importance + self.characters = list() + self.description = "" + self.result = "" + self.steps = list() + + def addStep(self, name: str, meta: str = "", summary: str = ""): + step = PlotStep(self, self.host.newID(), name, meta, summary) + self.steps.append(step) + return step + + def loadStep(self, ID: int, name: str, meta: str = "", summary: str = ""): + step = PlotStep(self, self.host.loadID(ID), name, meta, summary) + self.steps.append(step) + return step + + def removeStep(self, step: PlotStep): + self.host.removeID(step.UID) + self.steps.remove(step) + + def __iter__(self): + return self.steps.__iter__() + + def save(self): + self.plots.save() + + +class Plots: + + def __init__(self, path): + self.file = XmlFile(os.path.join(path, "plots.xml")) + self.host = UniqueIDHost() + self.lines = dict() + + def addLine(self, name: str, importance: PlotImportance = PlotImportance.MINOR): + line = PlotLine(self, self.host.newID(), name, importance) + self.lines[line.UID.value] = line + return line + + def loadLine(self, ID: int, name: str, importance: PlotImportance = PlotImportance.MINOR): + line = PlotLine(self, self.host.loadID(ID), name, importance) + self.lines[line.UID.value] = line + return line + + def removeLine(self, line: PlotLine): + self.host.removeID(line.UID) + self.lines.pop(line.UID.value) + + def __iter__(self): + return self.lines.values().__iter__() + + @classmethod + def loadPlotStep(cls, line: PlotLine, element: etree.Element): + ID = element.get("ID") + + if ID is None: + return + + line.loadStep( + int(ID), + element.get("name"), + element.get("meta"), + element.get("summary") + ) + + @classmethod + def loadPlotLine(cls, plots, element: etree.Element): + ID = element.get("ID") + + if ID is None: + return + + importance = PlotImportance(int(element.get("importance", 0))) + line = plots.loadLine(int(ID), element.get("name"), importance) + line.description = element.get("description") + line.result = element.get("result") + + for characterID in element.get("characters", "").split(','): + #TODO: Character loadings/adding should link to models! + + try: + line.characters.append(int(characterID)) + except ValueError: + continue + + for child in element.findall("step"): + cls.loadPlotStep(line, child) + + @classmethod + def loadPlots(cls, plots, root: etree.Element): + plots.host.reset() + plots.lines.clear() + + for element in root.findall("plot"): + cls.loadPlotLine(plots, element) + + def load(self): + try: + tree = self.file.load() + Plots.loadPlots(self, tree.getroot()) + except FileNotFoundError: + self.host.reset() + self.lines.clear() + + @classmethod + def saveElementAttribute(cls, element: etree.Element, name: str, value): + if value is None: + return + + str_value = str(value) + + if len(str_value) > 0: + element.set(name, str_value) + + @classmethod + def savePlotStep(cls, step: PlotStep, parent: etree.Element): + element = etree.SubElement(parent, "step") + + cls.saveElementAttribute(element, "name", step.name) + cls.saveElementAttribute(element, "ID", step.UID.value) + cls.saveElementAttribute(element, "meta", step.meta) + cls.saveElementAttribute(element, "summary", step.summary) + + @classmethod + def savePlotLine(cls, line: PlotLine, root: etree.Element): + element = etree.SubElement(root, "plot") + + characters = ",".join([str(characterID) for characterID in line.characters]) + + cls.saveElementAttribute(element, "name", line.name) + cls.saveElementAttribute(element, "ID", line.UID.value) + cls.saveElementAttribute(element, "importance", line.importance.value) + cls.saveElementAttribute(element, "characters", characters) + cls.saveElementAttribute(element, "description", line.description) + cls.saveElementAttribute(element, "result", line.result) + + for step in line: + cls.savePlotStep(step, element) + + @classmethod + def savePlots(cls, plots): + root = etree.Element("root") + + for line in plots.lines.values(): + cls.savePlotLine(line, root) + + return root + + def save(self): + tree = etree.ElementTree(Plots.savePlots(self)) + self.file.save(tree) diff --git a/manuskript/data/project.py b/manuskript/data/project.py new file mode 100644 index 00000000..a9a80ec2 --- /dev/null +++ b/manuskript/data/project.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from zipfile import BadZipFile +from manuskript.data.info import Info +from manuskript.data.summary import Summary +from manuskript.data.labels import LabelHost +from manuskript.data.status import StatusHost +from manuskript.data.settings import Settings +from manuskript.data.characters import Characters +from manuskript.data.plots import Plots +from manuskript.data.world import World +from manuskript.data.outline import Outline +from manuskript.data.revisions import Revisions +from manuskript.io.mskFile import MskFile + + +class Project: + + def __init__(self, path): + self.file = MskFile(path) + + self.info = Info(self.file.dir_path) + self.summary = Summary(self.file.dir_path) + self.labels = LabelHost(self.file.dir_path) + self.statuses = StatusHost(self.file.dir_path) + self.settings = Settings(self.file.dir_path) + self.characters = Characters(self.file.dir_path) + self.plots = Plots(self.file.dir_path) + self.world = World(self.file.dir_path) + self.outline = Outline(self.file.dir_path) + self.revisions = Revisions(self.file.dir_path) + + def __del__(self): + del self.file + + def getName(self): + parts = os.path.split(self.file.path) + name = parts[-1] + + if name.endswith('.msk'): + name = name[:-4] + + return name + + def load(self): + try: + self.file.load() + except BadZipFile: + return + except FileNotFoundError: + return + + self.info.load() + self.summary.load() + self.labels.load() + self.statuses.load() + self.settings.load() + self.characters.load() + self.plots.load() + self.world.load() + self.outline.load() + self.revisions.load() + + self.file.setZipFile(self.settings.isEnabled("saveToZip")) + + def save(self): + saveToZip = self.settings.isEnabled("saveToZip") + self.file.setZipFile(saveToZip) + + self.info.save() + self.summary.save() + self.labels.save() + self.statuses.save() + self.settings.save() + self.characters.save() + self.plots.save() + self.world.save() + self.outline.save() + #self.revisions.save() + + self.file.save(saveToZip) diff --git a/manuskript/data/revisions.py b/manuskript/data/revisions.py new file mode 100644 index 00000000..c02db7ac --- /dev/null +++ b/manuskript/data/revisions.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from lxml import etree +from manuskript.io.xmlFile import XmlFile + + +class RevisionEntry: + + def __init__(self, outline, timestamp: int, text: str): + self.outline = outline + + self.timestamp = timestamp + self.text = text + + def load(self): + self.outline.load() + + +class RevisionOutline: + + def __init__(self, revisions, ID: int): + self.revisions = revisions + + self.ID = ID + self.entries = list() + + def __iter__(self): + return self.entries.__iter__() + + def load(self): + self.revisions.load() + + +class Revisions: + + def __init__(self, path): + self.file = XmlFile(os.path.join(path, "revisions.xml")) + self.outline = dict() + + def __iter__(self): + return self.outline.values().__iter__() + + @classmethod + def loadRevisionEntry(cls, revisions, ID: int, element: etree.Element): + timestamp = element.get("timestamp") + text = element.get("text") + + if (timestamp is None) or (text is None): + return + + revOutline = revisions.outline.get(ID, None) + + if revOutline is None: + revOutline = RevisionOutline(revisions, ID) + revisions.outline[ID] = revOutline + + revOutline.entries.append(RevisionEntry(revOutline, timestamp, text)) + + @classmethod + def loadRevisionOutline(cls, revisions, element: etree.Element, parent: etree.Element): + if element.tag == "revision": + if parent is None: + return + + ID = int(parent.get("ID")) + + cls.loadRevisionEntry(revisions, ID, element) + elif element.tag == "outlineItem": + ID = element.get("ID") + + if ID is None: + return + + for child in element: + cls.loadRevisionOutline(revisions, child, element) + + def load(self): + try: + tree = self.file.load() + self.outline.clear() + + Revisions.loadRevisionOutline(self, tree.getroot(), None) + except FileNotFoundError: + self.outline.clear() diff --git a/manuskript/data/settings.py b/manuskript/data/settings.py new file mode 100644 index 00000000..7e14b8a1 --- /dev/null +++ b/manuskript/data/settings.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from manuskript.io.jsonFile import JsonFile + + +class Settings: + + def __init__(self, path, initDefault: bool = True): + self.file = JsonFile(os.path.join(path, "settings.txt")) + self.properties = dict() + + if initDefault: + Settings.loadDefaultSettings(self) + + def get(self, key: str): + return self.properties.get(key) + + def isEnabled(self, key: str) -> bool: + return self.properties.get(key, False) is True + + def set(self, key: str, value): + self.properties[key] = value + + def __iter__(self): + return self.properties.__iter__() + + @classmethod + def loadDefaultSettings(cls, settings): + settings.properties = { + 'autoSave': False, + 'autoSaveDelay': 5, + 'autoSaveNoChanges': True, + 'autoSaveNoChangesDelay': 5, + + 'corkBackground': { + 'color': '#926239', + 'image': 'writingdesk.jpg' + }, + + 'corkSizeFactor': 84, + 'corkStyle': 'new', + + 'defaultTextType': 'md', + 'dict': 'en_US', + 'dontShowDeleteWarning': False, + 'folderView': 'cork', + 'frequencyAnalyzer': { + 'phraseMax': 5, + 'phraseMin': 2, + 'wordExclude': 'a, and, or', + 'wordMin': 1 + }, + + 'fullScreenTheme': 'gentleblues', + 'lastTab': 6, + 'openIndexes': [None], + 'outlineViewColumns': [0, 8, 9, 11, 12, 13, 7], + + 'revisions': { + 'keep': True, + 'rules': { + '2592000': 86400, + '3600': 600, + '600': 60, + '86400': 3600, + 'null': 604800}, + 'smartremove': True + }, + + 'saveOnQuit': True, + 'saveToZip': False, + + 'spellcheck': False, + + 'textEditor': { + 'background': '#fff', + 'backgroundTransparent': False, + 'cursorNotBlinking': False, + 'cursorWidth': 1, + 'font': 'DejaVu Sans,10,-1,5,50,0,0,0,0,0', + 'fontColor': '#000', + 'indent': True, + 'lineSpacing': 100, + 'marginsLR': 0, + 'marginsTB': 0, + 'maxWidth': 0, + 'misspelled': '#F00', + 'spacingAbove': 5, + 'spacingBelow': 5, + 'tabWidth': 20, + 'textAlignment': 0 + }, + + 'viewMode': 'fiction', + 'viewSettings': { + 'Cork': { + 'Background': 'Nothing', + 'Border': 'Nothing', + 'Corner': 'Label', + 'Icon': 'Nothing', + 'Text': 'Nothing' + }, + + 'Outline': { + 'Background': 'Nothing', + 'Icon': 'Nothing', + 'Text': 'Compile' + }, + + 'Tree': { + 'Background': 'Nothing', + 'Icon': 'Nothing', + 'InfoFolder': 'Summary', + 'InfoText': 'Nothing', + 'Text': 'Compile', + 'iconSize': 24 + } + } + } + + def load(self): + try: + self.properties = self.file.load() + except FileNotFoundError: + Settings.loadDefaultSettings(self) + + def save(self): + self.file.save(self.properties) diff --git a/manuskript/data/status.py b/manuskript/data/status.py new file mode 100644 index 00000000..74def9ae --- /dev/null +++ b/manuskript/data/status.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from manuskript.io.textFile import TextFile + + +class Status: + + def __init__(self, host, name: str): + self.host = host + + self.name = name + + def __str__(self): + return self.name + + def load(self): + self.host.load() + + def save(self): + self.host.save() + + +class StatusHost: + + def __init__(self, path): + self.file = TextFile(os.path.join(path, "status.txt")) + self.statuses = dict() + + def addStatus(self, name: str): + self.statuses[name] = Status(self, name) + + def removeStatus(self, name: str): + self.statuses.pop(name) + + def renameStatus(self, oldName: str, newName: str): + status = self.statuses.get(oldName) + status.name = newName + self.statuses[newName] = status + self.statuses.pop(oldName) + + def getStatus(self, name: str): + return self.statuses.get(name) + + def __iter__(self): + return self.statuses.values().__iter__() + + def load(self): + try: + text = self.file.load() + self.statuses.clear() + except FileNotFoundError: + self.statuses.clear() + return + + if len(text) <= 1: + return + + text = text[:-1] + + if len(text) <= 0: + return + + for name in text.split("\n"): + self.addStatus(name) + + def save(self): + self.file.save("\n".join(self.statuses.keys()) + "\n") diff --git a/manuskript/data/summary.py b/manuskript/data/summary.py new file mode 100644 index 00000000..88aabda6 --- /dev/null +++ b/manuskript/data/summary.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from manuskript.io.mmdFile import MmdFile + + +class Summary: + + def __init__(self, path): + self.file = MmdFile(os.path.join(path, "summary.txt"), 13) + + self.sentence = None + self.paragraph = None + self.page = None + self.full = None + + def load(self): + try: + metadata, _ = self.file.loadMMD(True) + except FileNotFoundError: + metadata = dict() + + self.sentence = metadata.get("Sentence", None) + self.paragraph = metadata.get("Paragraph", None) + self.page = metadata.get("Page", None) + self.full = metadata.get("Full", None) + + def save(self): + metadata = dict() + + metadata["Sentence"] = self.sentence + metadata["Paragraph"] = self.paragraph + metadata["Page"] = self.page + metadata["Full"] = self.full + + self.file.save((metadata, None)) diff --git a/manuskript/data/unique_id.py b/manuskript/data/unique_id.py new file mode 100644 index 00000000..ddeb5f04 --- /dev/null +++ b/manuskript/data/unique_id.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +class UniqueID: + + def __init__(self, host, value: int): + self.host = host + self.value = value + + def __str__(self): + return str(self.value) + + +class UniqueIDHost: + + def __init__(self): + self.counter = 0 + self.uids = dict() + + def reset(self): + self.counter = 0 + self.uids.clear() + + def newID(self): + uid = UniqueID(self, self.counter) + self.counter = self.counter + 1 + self.uids[uid.value] = uid + return uid + + def loadID(self, value: int): + if value in self.uids: + raise ValueError("ID not unique: " + str(value)) + + uid = UniqueID(self, value) + self.counter = max(self.counter, uid.value + 1) + self.uids[uid.value] = uid + return uid + + def removeID(self, uid: UniqueID): + if uid.host != self: + raise ValueError("ID not bound to host!") + + self.uids.pop(uid.value) diff --git a/manuskript/data/world.py b/manuskript/data/world.py new file mode 100644 index 00000000..49d6e28b --- /dev/null +++ b/manuskript/data/world.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from manuskript.data.unique_id import UniqueIDHost, UniqueID +from manuskript.io.opmlFile import OpmlFile, OpmlOutlineItem + + +class WorldItem: + + def __init__(self, world, UID: UniqueID, name: str): + self.world = world + + self.UID = UID + self.name = name + self.description = None + self.passion = None + self.conflict = None + self.children = list() + + def __iter__(self): + return self.children.__iter__() + + def load(self): + self.world.load() + + +class World: + + def __init__(self, path): + self.file = OpmlFile(os.path.join(path, "world.opml")) + self.host = UniqueIDHost() + self.items = dict() + self.top = list() + + def addItem(self, name: str) -> WorldItem: + item = WorldItem(self, self.host.newID(), name) + self.items[item.UID.value] = item + return item + + def loadItem(self, ID: int, name: str) -> WorldItem: + item = WorldItem(self, self.host.loadID(ID), name) + self.items[item.UID.value] = item + return item + + def removeItem(self, item: WorldItem): + self.host.removeID(item.UID) + self.items.pop(item.UID.value) + + def __iter__(self): + return self.items.values().__iter__() + + @classmethod + def loadWorldItem(cls, world, outline: OpmlOutlineItem): + ID = outline.attributes.get("ID", None) + + if ID is None: + return None + + item = world.loadItem(int(ID), outline.attributes.get("name", None)) + + item.description = outline.attributes.get("description", None) + item.passion = outline.attributes.get("passion", None) + item.conflict = outline.attributes.get("conflict", None) + + for child in outline.children: + childItem = cls.loadWorldItem(world, child) + + if childItem is None: + continue + + item.children.append(childItem) + + return item + + def load(self): + try: + outlines = self.file.load() + + self.items.clear() + self.top.clear() + + for outline in outlines: + item = World.loadWorldItem(self, outline) + + if item is None: + continue + + self.top.append(item) + except FileNotFoundError: + self.items.clear() + self.top.clear() + + @classmethod + def saveWorldItem(cls, item: WorldItem): + outline = OpmlOutlineItem() + + outline.attributes["name"] = item.name + outline.attributes["ID"] = str(item.UID.value) + outline.attributes["description"] = item.description + outline.attributes["passion"] = item.passion + outline.attributes["conflict"] = item.conflict + + for childItem in item.children: + outline.children.append(cls.saveWorldItem(childItem)) + + return outline + + def save(self): + outlines = list() + + for item in self.top: + outlines.append(World.saveWorldItem(item)) + + self.file.save(outlines) diff --git a/manuskript/io/__init__.py b/manuskript/io/__init__.py new file mode 100644 index 00000000..5a66973d --- /dev/null +++ b/manuskript/io/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from manuskript.io.textFile import TextFile +from manuskript.io.jsonFile import JsonFile +from manuskript.io.xmlFile import XmlFile +from manuskript.io.opmlFile import OpmlFile +from manuskript.io.mmdFile import MmdFile +from manuskript.io.zipFile import ZipFile +from manuskript.io.mskFile import MskFile diff --git a/manuskript/io/abstractFile.py b/manuskript/io/abstractFile.py new file mode 100644 index 00000000..574cf73a --- /dev/null +++ b/manuskript/io/abstractFile.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + + +class AbstractFile: + + def __init__(self, path): + self.path = path + + def load(self): + raise IOError('Loading undefined!') + + def save(self, content): + raise IOError('Saving undefined!') + + def remove(self): + raise IOError('Removing undefined!') diff --git a/manuskript/io/jsonFile.py b/manuskript/io/jsonFile.py new file mode 100644 index 00000000..95213e09 --- /dev/null +++ b/manuskript/io/jsonFile.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import json +from manuskript.io.textFile import TextFile + + +class JsonFile(TextFile): + + def load(self): + return json.loads(TextFile.load(self)) + + def save(self, content): + TextFile.save(self, json.dumps(content, indent=4, sort_keys=True)) diff --git a/manuskript/io/mmdFile.py b/manuskript/io/mmdFile.py new file mode 100644 index 00000000..e881dcb6 --- /dev/null +++ b/manuskript/io/mmdFile.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- +import os +import re + +from manuskript.io.abstractFile import AbstractFile + + +class MmdFile(AbstractFile): + + def __init__(self, path, metaSpacing=16): + AbstractFile.__init__(self, path) + + self.metaSpacing = metaSpacing + + def loadMMD(self, ignoreBody: bool = True): + metadata = dict() + body = None + + metaPattern = re.compile(r"^([^\s].*?):\s*(.*)\n$") + metaValuePattern = re.compile(r"^(\s+)(.*)\n$") + metaKey = None + metaValue = None + + with open(self.path, 'rt', encoding='utf-8') as file: + for line in file: + m = metaPattern.match(line) + + if not (m is None): + if not (metaKey is None): + metadata[metaKey] = metaValue + + metaKey = m.group(1) + metaValue = m.group(2) + continue + + m = metaValuePattern.match(line) + + if not (m is None): + metaValue += "\n" + m.group(2) + elif line == "\n": + break + + if not (metaKey is None): + metadata[metaKey] = metaValue + + if not ignoreBody: + body = file.read() + + return metadata, body + + def load(self): + return self.loadMMD(False) + + def save(self, content): + metadata, body = content + metaSpacing = self.metaSpacing + + for (key, value) in metadata.items(): + if value is None: + continue + + metaSpacing = max(metaSpacing, len(key) + 2) + + with open(self.path, 'wt', encoding='utf-8') as file: + for (key, value) in metadata.items(): + if value is None: + continue + + spacing = metaSpacing - (len(key) + 2) + lines = str(value).split("\n") + + file.write(key + ": " + spacing * " " + lines[0] + "\n") + + for line in lines[1:]: + file.write(metaSpacing * " " + line + "\n") + + if not (body is None): + file.write("\n" + body) + + def remove(self): + if os.path.exists(self.path): + os.remove(self.path) diff --git a/manuskript/io/mskFile.py b/manuskript/io/mskFile.py new file mode 100644 index 00000000..5cb6e81e --- /dev/null +++ b/manuskript/io/mskFile.py @@ -0,0 +1,69 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os +import shutil + +from manuskript.io.textFile import TextFile +from manuskript.io.zipFile import ZipFile + + +class MskFile(TextFile, ZipFile): + + def __init__(self, path): + dir_path = os.path.splitext(path)[0] + + if (not os.path.isdir(dir_path)) or (os.path.getsize(path) > 1): + dir_path = None + + self.zipFile = dir_path is None + ZipFile.__init__(self, path, dir_path) + + def __del__(self): + ZipFile.__del__(self) + + if self.isZipFile() and (self.tmp is None) and not (self.dir_path is None): + shutil.rmtree(self.dir_path) + + def isZipFile(self) -> bool: + return self.zipFile + + def setZipFile(self, zipFile: bool): + if zipFile is self.zipFile: + return + + if not zipFile: + self.dir_path = os.path.splitext(self.path)[0] + + if not os.path.isdir(self.dir_path): + os.mkdir(self.dir_path) + + ZipFile.load(self) + + self.zipFile = zipFile + + def load(self): + if self.zipFile: + ZipFile.load(self) + else: + value = TextFile.load(self) + + if value == "1": + self.setZipFile(False) + + return self.zipFile + + def save(self, content=None): + if not (content is None): + self.setZipFile(content) + + if self.zipFile: + ZipFile.save(self) + else: + TextFile.save(self, "1") + + def remove(self): + if os.path.isdir(self.dir_path): + shutil.rmtree(self.dir_path) + + ZipFile.remove(self) diff --git a/manuskript/io/opmlFile.py b/manuskript/io/opmlFile.py new file mode 100644 index 00000000..3a86095a --- /dev/null +++ b/manuskript/io/opmlFile.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +from lxml import etree +from manuskript.io.xmlFile import XmlFile + + +class OpmlOutlineItem: + + def __init__(self): + self.attributes = dict() + self.children = list() + + def __iter__(self): + return self.children.__iter__() + + def keys(self): + return self.attributes.keys() + + def values(self): + return self.attributes.values() + + +class OpmlFile(XmlFile): + + @classmethod + def loadOutline(cls, element): + outline = OpmlOutlineItem() + + for key in element.keys(): + outline.attributes[key] = element.get(key) + + for child in element.getchildren(): + outline.children.append(cls.loadOutline(child)) + + return outline + + def load(self): + tree = XmlFile.load(self) + root = tree.getroot() + + if root.tag != "opml": + raise IOError("No valid OPML!") + + body = root.find("body") + + if body is None: + return [] + + return [OpmlFile.loadOutline(element) for element in body.getchildren()] + + @classmethod + def saveOutline(cls, outline, parent): + element = etree.SubElement(parent, "outline") + + for (key, value) in outline.attributes.items(): + if value is None: + continue + + element.attrib[key] = value + + for child in outline.children: + cls.saveOutline(child, element) + + def save(self, content): + root = etree.Element("opml") + root.set("version", "1.0") + + body = etree.SubElement(root, "body") + + for outline in content: + OpmlFile.saveOutline(outline, body) + + tree = etree.ElementTree(root) + XmlFile.save(self, tree) diff --git a/manuskript/io/textFile.py b/manuskript/io/textFile.py new file mode 100644 index 00000000..a51d255d --- /dev/null +++ b/manuskript/io/textFile.py @@ -0,0 +1,21 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from manuskript.io.abstractFile import AbstractFile + + +class TextFile(AbstractFile): + + def load(self): + with open(self.path, 'rt', encoding='utf-8') as file: + return file.read() + + def save(self, content): + with open(self.path, 'wt', encoding='utf-8') as file: + file.write(content) + + def remove(self): + if os.path.exists(self.path): + os.remove(self.path) diff --git a/manuskript/io/xmlFile.py b/manuskript/io/xmlFile.py new file mode 100644 index 00000000..d4a11503 --- /dev/null +++ b/manuskript/io/xmlFile.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os + +from lxml import etree +from manuskript.io.abstractFile import AbstractFile + + +class XmlFile(AbstractFile): + + def load(self): + with open(self.path, 'rb') as file: + return etree.parse(file) + + def save(self, content): + with open(self.path, 'wb') as file: + content.write(file, encoding="utf-8", xml_declaration=True, pretty_print=True) + + def remove(self): + if os.path.exists(self.path): + os.remove(self.path) diff --git a/manuskript/io/zipFile.py b/manuskript/io/zipFile.py new file mode 100644 index 00000000..1457e636 --- /dev/null +++ b/manuskript/io/zipFile.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python +# --!-- coding: utf8 --!-- + +import os +import shutil +import tempfile + +from zipfile import ZipFile as _ZipFile +from manuskript.io.abstractFile import AbstractFile + + +class ZipFile(AbstractFile): + + def __init__(self, path, dir_path=None): + AbstractFile.__init__(self, path) + + if dir_path is None: + self.tmp = tempfile.TemporaryDirectory() + dir_path = self.tmp.name + else: + self.tmp = None + + self.dir_path = dir_path + + def __del__(self): + if not (self.tmp is None): + self.tmp.cleanup() + + def load(self): + if self.dir_path is None: + self.tmp = tempfile.TemporaryDirectory() + self.dir_path = self.tmp.name + + archive = _ZipFile(self.path) + archive.extractall(self.dir_path) + return self.dir_path + + def save(self, content=None): + if not (content is None): + if not (self.tmp is None): + self.tmp.cleanup() + + self.tmp = None + self.dir_path = content + elif self.dir_path is None: + if self.tmp is None: + self.tmp = tempfile.TemporaryDirectory() + + self.dir_path = self.tmp.name + + shutil.make_archive(self.path, 'zip', self.dir_path) + shutil.move(self.path + ".zip", self.path) + + def remove(self): + if os.path.exists(self.path): + os.remove(self.path)