2016-03-05 09:57:38 +13:00
|
|
|
#!/usr/bin/env python
|
|
|
|
# --!-- coding: utf8 --!--
|
|
|
|
|
|
|
|
# Version 1 of file saving format.
|
|
|
|
# Aims at providing a plain-text way of saving a project
|
|
|
|
# (except for some elements), allowing collaborative work
|
|
|
|
# versioning and third-partty editing.
|
2016-03-10 01:20:52 +13:00
|
|
|
import os, shutil
|
2016-03-06 05:46:02 +13:00
|
|
|
import string
|
2016-03-05 09:57:38 +13:00
|
|
|
import zipfile
|
|
|
|
|
2016-03-06 05:46:02 +13:00
|
|
|
from PyQt5.QtCore import Qt, QModelIndex
|
2016-03-05 12:35:14 +13:00
|
|
|
from PyQt5.QtGui import QColor
|
|
|
|
|
2016-03-05 09:57:38 +13:00
|
|
|
from manuskript import settings
|
2016-03-08 21:21:44 +13:00
|
|
|
from manuskript.enums import Character, World, Plot, PlotStep, Outline
|
2016-03-05 12:35:14 +13:00
|
|
|
from manuskript.functions import mainWindow, iconColor
|
2016-03-06 05:46:02 +13:00
|
|
|
from lxml import etree as ET
|
|
|
|
|
2016-03-05 09:57:38 +13:00
|
|
|
|
|
|
|
try:
|
|
|
|
import zlib # Used with zipfile for compression
|
|
|
|
|
|
|
|
compression = zipfile.ZIP_DEFLATED
|
|
|
|
except:
|
|
|
|
compression = zipfile.ZIP_STORED
|
|
|
|
|
2016-03-05 12:35:14 +13:00
|
|
|
cache = {}
|
|
|
|
|
2016-03-06 00:55:56 +13:00
|
|
|
|
|
|
|
def formatMetaData(name, value, tabLength=10):
|
|
|
|
|
2016-03-06 05:46:02 +13:00
|
|
|
# Multiline formatting
|
|
|
|
if len(value.split("\n")) > 1:
|
|
|
|
value = "\n".join([" " * (tabLength + 1) + l for l in value.split("\n")])[tabLength + 1:]
|
|
|
|
|
|
|
|
# Avoid empty description (don't know how much MMD loves that)
|
|
|
|
if name == "":
|
|
|
|
name = "None"
|
|
|
|
|
|
|
|
# Escapes ":" in name
|
|
|
|
name = name.replace(":", "_.._")
|
|
|
|
|
2016-03-06 00:55:56 +13:00
|
|
|
return "{name}:{spaces}{value}\n".format(
|
|
|
|
name=name,
|
|
|
|
spaces=" " * (tabLength - len(name)),
|
|
|
|
value=value
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2016-03-06 05:46:02 +13:00
|
|
|
def slugify(name):
|
|
|
|
"""
|
|
|
|
A basic slug function, that escapes all spaces to "_" and all non letters/digits to "-".
|
|
|
|
@param name: name to slugify (str)
|
|
|
|
@return: str
|
|
|
|
"""
|
|
|
|
valid = string.ascii_letters + string.digits
|
|
|
|
newName = ""
|
|
|
|
for c in name:
|
|
|
|
if c in valid:
|
|
|
|
newName += c
|
|
|
|
elif c in string.whitespace:
|
|
|
|
newName += "_"
|
|
|
|
else:
|
|
|
|
newName += "-"
|
|
|
|
return newName
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
def log(*args):
|
|
|
|
print(" ".join(str(a) for a in args))
|
|
|
|
|
|
|
|
|
2016-03-05 09:57:38 +13:00
|
|
|
def saveProject(zip=None):
|
|
|
|
"""
|
|
|
|
Saves the project. If zip is False, the project is saved as a multitude of plain-text files for the most parts
|
|
|
|
and some XML or zip? for settings and stuff.
|
|
|
|
If zip is True, everything is saved as a single zipped file. Easier to carry around, but does not allow
|
|
|
|
collaborative work, versionning, or third-party editing.
|
|
|
|
@param zip: if True, saves as a single file. If False, saves as plain-text. If None, tries to determine based on
|
|
|
|
settings.
|
|
|
|
@return: Nothing
|
|
|
|
"""
|
|
|
|
if zip is None:
|
2016-03-06 00:55:56 +13:00
|
|
|
zip = False
|
2016-03-05 09:57:38 +13:00
|
|
|
# Fixme
|
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
log("\n\nSaving to:", "zip" if zip else "folder")
|
2016-03-10 01:20:52 +13:00
|
|
|
|
2016-03-06 05:46:02 +13:00
|
|
|
# List of files to be written
|
2016-03-05 09:57:38 +13:00
|
|
|
files = []
|
2016-03-06 05:46:02 +13:00
|
|
|
# List of files to be removed
|
|
|
|
removes = []
|
2016-03-10 02:10:22 +13:00
|
|
|
# List of files to be moved
|
|
|
|
moves = []
|
2016-03-06 05:46:02 +13:00
|
|
|
|
2016-03-05 09:57:38 +13:00
|
|
|
mw = mainWindow()
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
if zip:
|
|
|
|
# File format version
|
|
|
|
files.append(("VERSION", "1"))
|
|
|
|
|
2016-03-05 12:35:14 +13:00
|
|
|
# General infos (book and author)
|
|
|
|
# Saved in plain text, in infos.txt
|
|
|
|
|
|
|
|
path = "infos.txt"
|
|
|
|
content = ""
|
|
|
|
for name, col in [
|
|
|
|
("Title", 0),
|
|
|
|
("Subtitle", 1),
|
|
|
|
("Serie", 2),
|
|
|
|
("Volume", 3),
|
|
|
|
("Genre", 4),
|
|
|
|
("License", 5),
|
|
|
|
("Author", 6),
|
|
|
|
("Email", 7),
|
|
|
|
]:
|
|
|
|
val = mw.mdlFlatData.item(0, col).text().strip()
|
|
|
|
if val:
|
|
|
|
content += "{name}:{spaces}{value}\n".format(
|
|
|
|
name=name,
|
|
|
|
spaces=" " * (15 - len(name)),
|
|
|
|
value=val
|
|
|
|
)
|
|
|
|
files.append((path, content))
|
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
2016-03-05 12:35:14 +13:00
|
|
|
# Summary
|
|
|
|
# In plain text, in summary.txt
|
|
|
|
|
|
|
|
path = "summary.txt"
|
|
|
|
content = ""
|
|
|
|
for name, col in [
|
|
|
|
("Situation", 0),
|
|
|
|
("Sentence", 1),
|
|
|
|
("Paragraph", 2),
|
|
|
|
("Page", 3),
|
|
|
|
("Full", 4),
|
|
|
|
]:
|
|
|
|
val = mw.mdlFlatData.item(1, col).text().strip()
|
|
|
|
if val:
|
2016-03-06 05:46:02 +13:00
|
|
|
content += formatMetaData(name, val, 12)
|
|
|
|
|
2016-03-05 12:35:14 +13:00
|
|
|
files.append((path, content))
|
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
2016-03-05 12:35:14 +13:00
|
|
|
# Label & Status
|
|
|
|
# In plain text
|
|
|
|
|
|
|
|
for mdl, path in [
|
|
|
|
(mw.mdlStatus, "status.txt"),
|
|
|
|
(mw.mdlLabels, "labels.txt")
|
|
|
|
]:
|
|
|
|
|
|
|
|
content = ""
|
|
|
|
|
|
|
|
# We skip the first row, which is empty and transparent
|
|
|
|
for i in range(1, mdl.rowCount()):
|
|
|
|
color = ""
|
|
|
|
if mdl.data(mdl.index(i, 0), Qt.DecorationRole) is not None:
|
|
|
|
color = iconColor(mdl.data(mdl.index(i, 0), Qt.DecorationRole)).name(QColor.HexRgb)
|
|
|
|
color = color if color != "#ff000000" else "#00000000"
|
|
|
|
|
|
|
|
text = mdl.data(mdl.index(i, 0))
|
|
|
|
|
|
|
|
if text:
|
|
|
|
content += "{name}{color}\n".format(
|
|
|
|
name=text,
|
|
|
|
color= "" if color == "" else ":" + " " * (20 - len(text)) + color
|
|
|
|
)
|
|
|
|
|
|
|
|
files.append((path, content))
|
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
|
|
|
# Characters
|
2016-03-05 12:35:14 +13:00
|
|
|
# In a character folder
|
|
|
|
|
2016-03-08 21:21:44 +13:00
|
|
|
path = os.path.join("characters", "{name}.txt")
|
2016-03-06 00:55:56 +13:00
|
|
|
_map = [
|
|
|
|
(Character.name, "Name"),
|
|
|
|
(Character.ID, "ID"),
|
|
|
|
(Character.importance, "Importance"),
|
|
|
|
(Character.motivation, "Motivation"),
|
|
|
|
(Character.goal, "Goal"),
|
|
|
|
(Character.conflict, "Conflict"),
|
|
|
|
(Character.summarySentence, "Phrase Summary"),
|
|
|
|
(Character.summaryPara, "Paragraph Summary"),
|
|
|
|
(Character.summaryFull, "Full Summary"),
|
|
|
|
(Character.notes, "Notes"),
|
|
|
|
]
|
|
|
|
mdl = mw.mdlCharacter
|
2016-03-10 02:10:22 +13:00
|
|
|
|
|
|
|
# Review characters
|
2016-03-06 00:55:56 +13:00
|
|
|
for c in mdl.characters:
|
2016-03-10 02:10:22 +13:00
|
|
|
|
|
|
|
# Generates file's content
|
2016-03-06 00:55:56 +13:00
|
|
|
content = ""
|
|
|
|
for m, name in _map:
|
|
|
|
val = mdl.data(c.index(m.value)).strip()
|
|
|
|
if val:
|
2016-03-06 05:46:02 +13:00
|
|
|
content += formatMetaData(name, val, 20)
|
2016-03-06 00:55:56 +13:00
|
|
|
|
|
|
|
for info in c.infos:
|
2016-03-06 05:46:02 +13:00
|
|
|
content += formatMetaData(info.description, info.value, 20)
|
2016-03-06 00:55:56 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
# generate file's path
|
2016-03-06 05:46:02 +13:00
|
|
|
cpath = path.format(name="{ID}-{slugName}".format(
|
2016-03-06 00:55:56 +13:00
|
|
|
ID=c.ID(),
|
2016-03-06 05:46:02 +13:00
|
|
|
slugName=slugify(c.name())
|
|
|
|
))
|
2016-03-10 02:10:22 +13:00
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
# Has the character been renamed?
|
2016-03-06 05:46:02 +13:00
|
|
|
if c.lastPath and cpath != c.lastPath:
|
2016-03-10 02:10:22 +13:00
|
|
|
moves.append((c.lastPath, cpath))
|
|
|
|
|
|
|
|
# Update character's path
|
2016-03-06 05:46:02 +13:00
|
|
|
c.lastPath = cpath
|
2016-03-05 12:35:14 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
files.append((cpath, content))
|
|
|
|
|
2016-03-10 03:48:59 +13:00
|
|
|
# # List removed characters
|
|
|
|
# for c in mdl.removed:
|
|
|
|
# # generate file's path
|
|
|
|
# cpath = path.format(name="{ID}-{slugName}".format(
|
|
|
|
# ID=c.ID(),
|
|
|
|
# slugName=slugify(c.name())
|
|
|
|
# ))
|
|
|
|
#
|
|
|
|
# # Mark for removal
|
|
|
|
# removes.append(cpath)
|
2016-03-10 02:10:22 +13:00
|
|
|
|
|
|
|
mdl.removed.clear()
|
|
|
|
|
|
|
|
####################################################################################################################
|
2016-03-05 12:35:14 +13:00
|
|
|
# Texts
|
|
|
|
# In an outline folder
|
|
|
|
|
2016-03-08 21:21:44 +13:00
|
|
|
mdl = mw.mdlOutline
|
2016-03-10 03:48:59 +13:00
|
|
|
|
|
|
|
# Go through the tree
|
2016-03-10 02:10:22 +13:00
|
|
|
f, m, r = exportOutlineItem(mdl.rootItem)
|
2016-03-10 01:20:52 +13:00
|
|
|
files += f
|
2016-03-10 02:10:22 +13:00
|
|
|
moves += m
|
2016-03-10 01:20:52 +13:00
|
|
|
removes += r
|
2016-03-05 12:35:14 +13:00
|
|
|
|
2016-03-10 03:48:59 +13:00
|
|
|
# List removed items
|
|
|
|
# for item in mdl.removed:
|
|
|
|
# path = outlineItemPath(item)
|
|
|
|
# log("* Marking for removal:", path)
|
|
|
|
|
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
|
|
|
# World
|
2016-03-05 12:35:14 +13:00
|
|
|
# Either in an XML file, or in lots of plain texts?
|
|
|
|
# More probably text, since there might be writing done in third-party.
|
|
|
|
|
2016-03-06 05:46:02 +13:00
|
|
|
path = "world.opml"
|
|
|
|
mdl = mw.mdlWorld
|
|
|
|
|
|
|
|
root = ET.Element("opml")
|
|
|
|
root.attrib["version"] = "1.0"
|
|
|
|
body = ET.SubElement(root, "body")
|
|
|
|
addWorldItem(body, mdl)
|
|
|
|
content = ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True)
|
|
|
|
files.append((path, content))
|
2016-03-05 12:35:14 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
2016-03-05 12:35:14 +13:00
|
|
|
# Plots (mw.mdlPlots)
|
|
|
|
# Either in XML or lots of plain texts?
|
|
|
|
# More probably XML since there is not really a lot if writing to do (third-party)
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
path = "plots.xml"
|
2016-03-06 21:26:59 +13:00
|
|
|
mdl = mw.mdlPlots
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
root = ET.Element("root")
|
|
|
|
addPlotItem(root, mdl)
|
2016-03-06 21:26:59 +13:00
|
|
|
content = ET.tostring(root, encoding="UTF-8", xml_declaration=True, pretty_print=True)
|
|
|
|
files.append((path, content))
|
2016-03-05 12:35:14 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
2016-03-05 12:35:14 +13:00
|
|
|
# Settings
|
|
|
|
# Saved in readable text (json) for easier versionning. But they mustn't be shared, it seems.
|
|
|
|
# Maybe include them only if zipped?
|
|
|
|
# Well, for now, we keep them here...
|
2016-03-10 02:10:22 +13:00
|
|
|
|
2016-03-05 12:35:14 +13:00
|
|
|
files.append(("settings.txt", settings.save(protocol=0)))
|
2016-03-05 09:57:38 +13:00
|
|
|
|
|
|
|
project = mw.currentProject
|
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
2016-03-05 12:35:14 +13:00
|
|
|
# Save to zip
|
2016-03-10 02:10:22 +13:00
|
|
|
|
2016-03-05 12:35:14 +13:00
|
|
|
if zip:
|
|
|
|
project = os.path.join(
|
|
|
|
os.path.dirname(project),
|
|
|
|
"_" + os.path.basename(project)
|
|
|
|
)
|
|
|
|
|
|
|
|
zf = zipfile.ZipFile(project, mode="w")
|
|
|
|
|
|
|
|
for filename, content in files:
|
|
|
|
zf.writestr(filename, content, compress_type=compression)
|
|
|
|
|
|
|
|
zf.close()
|
2016-03-05 09:57:38 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
####################################################################################################################
|
2016-03-05 12:35:14 +13:00
|
|
|
# Save to plain text
|
2016-03-10 02:10:22 +13:00
|
|
|
|
2016-03-05 12:35:14 +13:00
|
|
|
else:
|
2016-03-10 02:10:22 +13:00
|
|
|
|
2016-03-10 03:48:59 +13:00
|
|
|
global cache
|
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
# Project path
|
2016-03-05 12:35:14 +13:00
|
|
|
dir = os.path.dirname(project)
|
2016-03-10 02:10:22 +13:00
|
|
|
|
|
|
|
# Folder containing file: name of the project file (without .msk extension)
|
2016-03-05 12:35:14 +13:00
|
|
|
folder = os.path.splitext(os.path.basename(project))[0]
|
2016-03-10 02:10:22 +13:00
|
|
|
|
|
|
|
# Debug
|
|
|
|
log("\nSaving to folder", folder)
|
|
|
|
|
|
|
|
# Moving files that have been renamed
|
|
|
|
for old, new in moves:
|
|
|
|
|
|
|
|
# Get full path
|
|
|
|
oldPath = os.path.join(dir, folder, old)
|
|
|
|
newPath = os.path.join(dir, folder, new)
|
|
|
|
|
|
|
|
# Move the old file to the new place
|
2016-03-10 03:48:59 +13:00
|
|
|
try:
|
|
|
|
os.replace(oldPath, newPath)
|
|
|
|
log("* Renaming/moving {} to {}".format(old, new))
|
|
|
|
except FileNotFoundError:
|
|
|
|
# Maybe parent folder has been renamed
|
|
|
|
pass
|
2016-03-10 02:10:22 +13:00
|
|
|
|
|
|
|
# Update cache
|
2016-03-10 03:48:59 +13:00
|
|
|
cache2 = {}
|
|
|
|
for f in cache:
|
|
|
|
f2 = f.replace(old, new)
|
|
|
|
if f2 != f:
|
|
|
|
log(" * Updating cache:", f, f2)
|
|
|
|
cache2[f2] = cache[f]
|
|
|
|
cache = cache2
|
|
|
|
|
|
|
|
# Writing files
|
2016-03-05 12:35:14 +13:00
|
|
|
for path, content in files:
|
|
|
|
filename = os.path.join(dir, folder, path)
|
|
|
|
os.makedirs(os.path.dirname(filename), exist_ok=True)
|
2016-03-05 09:57:38 +13:00
|
|
|
|
2016-03-05 12:35:14 +13:00
|
|
|
# TODO: the first time it saves, it will overwrite everything, since it's not yet in cache.
|
|
|
|
# Or we have to cache while loading.
|
|
|
|
|
|
|
|
if not path in cache or cache[path] != content:
|
2016-03-10 02:10:22 +13:00
|
|
|
log("* Writing file", path)
|
2016-03-05 12:35:14 +13:00
|
|
|
mode = "w"+ ("b" if type(content) == bytes else "")
|
|
|
|
with open(filename, mode) as f:
|
|
|
|
f.write(content)
|
|
|
|
cache[path] = content
|
|
|
|
|
|
|
|
else:
|
2016-03-10 01:20:52 +13:00
|
|
|
pass
|
2016-03-10 02:10:22 +13:00
|
|
|
# log(" In cache, and identical. Do nothing.")
|
2016-03-05 09:57:38 +13:00
|
|
|
|
2016-03-10 03:48:59 +13:00
|
|
|
# Removing phantoms
|
|
|
|
for path in [p for p in cache if p not in [p for p,c in files]]:
|
|
|
|
filename = os.path.join(dir, folder, path)
|
|
|
|
log("* Removing", path)
|
|
|
|
|
|
|
|
if os.path.isdir(filename):
|
|
|
|
shutil.rmtree(filename)
|
|
|
|
|
|
|
|
else: # elif os.path.exists(filename)
|
|
|
|
os.remove(filename)
|
|
|
|
|
|
|
|
# Clear cache
|
|
|
|
cache.pop(path, 0)
|
|
|
|
|
|
|
|
# Removing empty directories
|
|
|
|
for root, dirs, files in os.walk(os.path.join(dir, folder, "outline")):
|
|
|
|
for dir in dirs:
|
|
|
|
newDir = os.path.join(root, dir)
|
|
|
|
try:
|
|
|
|
os.removedirs(newDir)
|
|
|
|
log("* Removing empty directory:", newDir)
|
|
|
|
except:
|
|
|
|
# Directory not empty, we don't remove.
|
|
|
|
pass
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
|
2016-03-06 05:46:02 +13:00
|
|
|
def addWorldItem(root, mdl, parent=QModelIndex()):
|
|
|
|
"""
|
|
|
|
Lists elements in a world model and create an OPML xml file.
|
|
|
|
@param root: an Etree element
|
|
|
|
@param mdl: a worldModel
|
|
|
|
@param parent: the parent index in the world model
|
|
|
|
@return: root, to which sub element have been added
|
|
|
|
"""
|
|
|
|
# List every row (every world item)
|
|
|
|
for x in range(mdl.rowCount(parent)):
|
|
|
|
|
|
|
|
# For each row, create an outline item.
|
|
|
|
outline = ET.SubElement(root, "outline")
|
|
|
|
for y in range(mdl.columnCount(parent)):
|
|
|
|
|
|
|
|
val = mdl.data(mdl.index(x, y, parent))
|
|
|
|
|
|
|
|
if not val:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for w in World:
|
|
|
|
if y == w.value:
|
|
|
|
outline.attrib[w.name] = val
|
|
|
|
|
|
|
|
if mdl.hasChildren(mdl.index(x, y, parent)):
|
|
|
|
addWorldItem(outline, mdl, mdl.index(x, y, parent))
|
|
|
|
|
|
|
|
return root
|
2016-03-05 09:57:38 +13:00
|
|
|
|
2016-03-06 21:26:59 +13:00
|
|
|
|
|
|
|
def addPlotItem(root, mdl, parent=QModelIndex()):
|
|
|
|
"""
|
2016-03-07 04:27:03 +13:00
|
|
|
Lists elements in a plot model and create an xml file.
|
2016-03-06 21:26:59 +13:00
|
|
|
@param root: an Etree element
|
|
|
|
@param mdl: a plotModel
|
|
|
|
@param parent: the parent index in the plot model
|
|
|
|
@return: root, to which sub element have been added
|
|
|
|
"""
|
|
|
|
|
|
|
|
# List every row (every plot item)
|
|
|
|
for x in range(mdl.rowCount(parent)):
|
|
|
|
|
|
|
|
# For each row, create an outline item.
|
2016-03-07 04:27:03 +13:00
|
|
|
outline = ET.SubElement(root, "plot")
|
2016-03-06 21:26:59 +13:00
|
|
|
for y in range(mdl.columnCount(parent)):
|
|
|
|
|
|
|
|
index = mdl.index(x, y, parent)
|
|
|
|
val = mdl.data(index)
|
|
|
|
|
|
|
|
if not val:
|
|
|
|
continue
|
|
|
|
|
|
|
|
for w in Plot:
|
|
|
|
if y == w.value:
|
|
|
|
outline.attrib[w.name] = val
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
# List characters as attrib
|
2016-03-06 21:26:59 +13:00
|
|
|
if y == Plot.characters.value:
|
|
|
|
if mdl.hasChildren(index):
|
|
|
|
characters = []
|
|
|
|
for cX in range(mdl.rowCount(index)):
|
|
|
|
for cY in range(mdl.columnCount(index)):
|
|
|
|
cIndex = mdl.index(cX, cY, index)
|
|
|
|
characters.append(mdl.data(cIndex))
|
|
|
|
outline.attrib[Plot.characters.name] = ",".join(characters)
|
|
|
|
else:
|
|
|
|
outline.attrib.pop(Plot.characters.name)
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
# List resolution steps as sub items
|
|
|
|
elif y == Plot.steps.value:
|
|
|
|
if mdl.hasChildren(index):
|
|
|
|
for cX in range(mdl.rowCount(index)):
|
|
|
|
step = ET.SubElement(outline, "step")
|
|
|
|
for cY in range(mdl.columnCount(index)):
|
|
|
|
cIndex = mdl.index(cX, cY, index)
|
|
|
|
val = mdl.data(cIndex)
|
2016-03-06 21:26:59 +13:00
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
for w in PlotStep:
|
|
|
|
if cY == w.value:
|
|
|
|
step.attrib[w.name] = val
|
2016-03-06 21:26:59 +13:00
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
outline.attrib.pop(Plot.steps.name)
|
2016-03-06 21:26:59 +13:00
|
|
|
|
|
|
|
return root
|
|
|
|
|
2016-03-07 04:27:03 +13:00
|
|
|
|
2016-03-08 21:21:44 +13:00
|
|
|
def exportOutlineItem(root):
|
|
|
|
"""
|
2016-03-10 01:20:52 +13:00
|
|
|
Takes an outline item, and returns two lists:
|
|
|
|
1. of (`filename`, `content`), representing the whole tree of files to be written, in multimarkdown.
|
2016-03-10 02:10:22 +13:00
|
|
|
3. of (`filename`, `filename`) listing files to be moved
|
2016-03-10 01:20:52 +13:00
|
|
|
2. of `filename`, representing files to be removed.
|
2016-03-08 21:21:44 +13:00
|
|
|
|
|
|
|
@param root: OutlineItem
|
2016-03-10 01:20:52 +13:00
|
|
|
@return: [(str, str)], [str]
|
2016-03-08 21:21:44 +13:00
|
|
|
"""
|
2016-03-10 01:20:52 +13:00
|
|
|
|
|
|
|
files = []
|
2016-03-10 02:10:22 +13:00
|
|
|
moves = []
|
2016-03-10 01:20:52 +13:00
|
|
|
removes = []
|
|
|
|
|
2016-03-08 21:21:44 +13:00
|
|
|
k=0
|
|
|
|
for child in root.children():
|
2016-03-10 03:48:59 +13:00
|
|
|
itemPath = outlineItemPath(child)
|
|
|
|
spath = os.path.join(*itemPath)
|
|
|
|
|
2016-03-08 21:21:44 +13:00
|
|
|
k += 1
|
|
|
|
|
2016-03-10 01:20:52 +13:00
|
|
|
# Has the item been renamed?
|
2016-03-10 03:48:59 +13:00
|
|
|
lp = child._lastPath
|
|
|
|
if lp and spath != lp:
|
|
|
|
moves.append((lp, spath))
|
|
|
|
log(child.title(), "has been renamed (", lp, " → ", spath, ")")
|
|
|
|
log(" → We mark for moving:", lp)
|
2016-03-10 01:20:52 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
# Updates item last's path
|
2016-03-10 03:48:59 +13:00
|
|
|
child._lastPath = spath # itemPath[-1]
|
2016-03-10 01:20:52 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
# Generating content
|
2016-03-08 21:21:44 +13:00
|
|
|
if child.type() == "folder":
|
|
|
|
fpath = os.path.join(spath, "folder.txt")
|
|
|
|
content = outlineToMMD(child)
|
2016-03-10 01:20:52 +13:00
|
|
|
files.append((fpath, content))
|
2016-03-08 21:21:44 +13:00
|
|
|
|
|
|
|
elif child.type() in ["txt", "t2t"]:
|
|
|
|
content = outlineToMMD(child)
|
2016-03-10 01:20:52 +13:00
|
|
|
files.append((spath, content))
|
2016-03-08 21:21:44 +13:00
|
|
|
|
|
|
|
elif child.type() in ["html"]:
|
2016-03-10 03:48:59 +13:00
|
|
|
# Save as html. Not the most beautiful, but hey.
|
2016-03-10 02:10:22 +13:00
|
|
|
content = outlineToMMD(child)
|
|
|
|
files.append((spath, content))
|
2016-03-08 21:21:44 +13:00
|
|
|
|
|
|
|
else:
|
2016-03-10 02:10:22 +13:00
|
|
|
log("Unknown type")
|
2016-03-08 21:21:44 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
f, m, r = exportOutlineItem(child)
|
2016-03-10 01:20:52 +13:00
|
|
|
files += f
|
2016-03-10 02:10:22 +13:00
|
|
|
moves += m
|
2016-03-10 01:20:52 +13:00
|
|
|
removes += r
|
2016-03-08 21:21:44 +13:00
|
|
|
|
2016-03-10 02:10:22 +13:00
|
|
|
return files, moves, removes
|
2016-03-08 21:21:44 +13:00
|
|
|
|
|
|
|
def outlineItemPath(item):
|
2016-03-10 03:48:59 +13:00
|
|
|
"""
|
|
|
|
Returns the outlineItem file path (like the path where it will be written on the disk). As a list of folder's
|
|
|
|
name. To be joined by os.path.join.
|
|
|
|
@param item: outlineItem
|
|
|
|
@return: list of folder's names
|
|
|
|
"""
|
2016-03-08 21:21:44 +13:00
|
|
|
# Root item
|
|
|
|
if not item.parent():
|
2016-03-10 03:48:59 +13:00
|
|
|
return ["outline"]
|
2016-03-08 21:21:44 +13:00
|
|
|
else:
|
|
|
|
name = "{ID}-{name}{ext}".format(
|
|
|
|
ID=item.row(),
|
|
|
|
name=slugify(item.title()),
|
2016-03-10 03:48:59 +13:00
|
|
|
ext="" if item.type() == "folder" else ".md" # ".{}".format(item.type()) # To have .txt, .t2t, .html, ...
|
2016-03-08 21:21:44 +13:00
|
|
|
)
|
|
|
|
return outlineItemPath(item.parent()) + [name]
|
|
|
|
|
|
|
|
def outlineToMMD(item):
|
|
|
|
content = ""
|
|
|
|
|
|
|
|
# We don't want to write some datas (computed)
|
|
|
|
exclude = [Outline.wordCount, Outline.goal, Outline.goalPercentage, Outline.revisions, Outline.text]
|
|
|
|
# We want to force some data even if they're empty
|
|
|
|
force = [Outline.compile]
|
|
|
|
|
|
|
|
for attrib in Outline:
|
|
|
|
if attrib in exclude: continue
|
|
|
|
val = item.data(attrib.value)
|
|
|
|
if val or attrib in force:
|
|
|
|
content += formatMetaData(attrib.name, str(val), 15)
|
|
|
|
|
|
|
|
content += "\n\n"
|
|
|
|
content += item.data(Outline.text.value)
|
|
|
|
|
|
|
|
# Saving revisions
|
2016-03-10 02:10:22 +13:00
|
|
|
# TODO: saving revisions?
|
2016-03-08 21:21:44 +13:00
|
|
|
# rev = item.revisions()
|
|
|
|
# for r in rev:
|
|
|
|
# revItem = ET.Element("revision")
|
|
|
|
# revItem.set("timestamp", str(r[0]))
|
|
|
|
# revItem.set("text", r[1])
|
|
|
|
# item.append(revItem)
|
|
|
|
|
|
|
|
return content
|
|
|
|
|
2016-03-05 09:57:38 +13:00
|
|
|
def loadProject(project):
|
|
|
|
"""
|
|
|
|
Loads a project.
|
|
|
|
@param project: the filename of the project to open.
|
|
|
|
@return: an array of errors, empty if None.
|
|
|
|
"""
|
2016-03-05 12:35:14 +13:00
|
|
|
|
|
|
|
# Don't forget to cache everything that is loaded
|
|
|
|
# In order to save only what has changed.
|
|
|
|
|
2016-03-05 09:57:38 +13:00
|
|
|
pass
|