updated with functional changes, testing
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
pythonpath = src
|
||||
testpaths = tests
|
||||
@@ -0,0 +1 @@
|
||||
pytest
|
||||
|
||||
+27
-32
@@ -1,50 +1,45 @@
|
||||
import os
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from . import state
|
||||
from .models import Book
|
||||
from .parser import parseBook
|
||||
from .models import BuildContext
|
||||
from .parser import parse_book
|
||||
from .writer import (
|
||||
writeStructure, writeMisc, writeStyles, writeNCX,
|
||||
writeCover, writeContents, writeSections, writeIllustrations, writeEpub,
|
||||
write_structure, write_misc, write_styles, write_ncx,
|
||||
write_cover, write_contents, write_sections, write_illustrations, write_epub,
|
||||
)
|
||||
|
||||
|
||||
def parseArgs():
|
||||
def parse_args(ctx: BuildContext) -> bool:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("fileName", help="File to process")
|
||||
parser.add_argument("-f", "--force", help="Force overwrite an existing book directory", action="store_true")
|
||||
parser.add_argument("-v", "--verbose", help="Provide updates while processing", action="store_true")
|
||||
args = parser.parse_args()
|
||||
state.book.filename = str(args.fileName)
|
||||
if not os.path.isfile(state.book.filename):
|
||||
print("File '{}' is not found. Exiting...".format(state.book.filename))
|
||||
ctx.book.filename = str(args.fileName)
|
||||
if not os.path.isfile(ctx.book.filename):
|
||||
print("File '{}' is not found. Exiting...".format(ctx.book.filename))
|
||||
exit()
|
||||
state.verbose = args.verbose
|
||||
ctx.verbose = args.verbose
|
||||
return args.force
|
||||
|
||||
|
||||
def main():
|
||||
state.book = Book()
|
||||
state.book.publishDate = (
|
||||
str(datetime.now().year) + "-" +
|
||||
str(datetime.now().month) + "-" +
|
||||
str(datetime.now().day)
|
||||
def main() -> None:
|
||||
ctx = BuildContext()
|
||||
ctx.book.publish_date = "{}-{}-{}".format(
|
||||
datetime.now().year,
|
||||
datetime.now().month,
|
||||
datetime.now().day,
|
||||
)
|
||||
state.inSection = False
|
||||
state.inStyle = False
|
||||
state.verbose = False
|
||||
|
||||
overwrite = parseArgs()
|
||||
parseBook()
|
||||
if overwrite and state.verbose:
|
||||
overwrite = parse_args(ctx)
|
||||
parse_book(ctx)
|
||||
if overwrite and ctx.verbose:
|
||||
print("Force flag, overwriting existing directory")
|
||||
writeStructure(overwrite)
|
||||
writeMisc()
|
||||
writeStyles()
|
||||
writeNCX()
|
||||
writeCover()
|
||||
writeContents()
|
||||
writeSections()
|
||||
writeIllustrations()
|
||||
writeEpub()
|
||||
write_structure(ctx, overwrite)
|
||||
write_misc(ctx)
|
||||
write_styles(ctx)
|
||||
write_ncx(ctx)
|
||||
write_cover(ctx)
|
||||
write_contents(ctx)
|
||||
write_sections(ctx)
|
||||
write_illustrations(ctx)
|
||||
write_epub(ctx)
|
||||
|
||||
+47
-36
@@ -1,49 +1,60 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List
|
||||
|
||||
|
||||
@dataclass
|
||||
class Name:
|
||||
def __init__(self):
|
||||
self.first = ""
|
||||
self.last = ""
|
||||
first: str = ""
|
||||
last: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Series:
|
||||
def __init__(self):
|
||||
self.number = ""
|
||||
self.name = ""
|
||||
number: str = ""
|
||||
name: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Image:
|
||||
def __init__(self):
|
||||
self.name = ""
|
||||
self.type = ""
|
||||
self.alt = ""
|
||||
self.caption = ""
|
||||
name: str = ""
|
||||
type: str = ""
|
||||
alt: str = ""
|
||||
caption: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Section:
|
||||
def __init__(self):
|
||||
self.type = ""
|
||||
self.name = ""
|
||||
self.title = ""
|
||||
self.body = ""
|
||||
type: str = ""
|
||||
name: str = ""
|
||||
title: str = ""
|
||||
body: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class Book:
|
||||
def __init__(self):
|
||||
self.filename = ""
|
||||
self.title = ""
|
||||
self.titlesort = ""
|
||||
self.series = Series()
|
||||
self.author = []
|
||||
self.artist = Name()
|
||||
self.editor = Name()
|
||||
self.publisher = ""
|
||||
self.copyright = ""
|
||||
self.subjects = ""
|
||||
self.style = ""
|
||||
self.directory = ""
|
||||
self.isbn = ""
|
||||
self.uuid = ""
|
||||
self.source = ""
|
||||
self.images = []
|
||||
self.sections = []
|
||||
self.cover = Image()
|
||||
self.publishDate = ""
|
||||
filename: str = ""
|
||||
title: str = ""
|
||||
titlesort: str = ""
|
||||
series: Series = field(default_factory=Series)
|
||||
author: List[Name] = field(default_factory=list)
|
||||
artist: Name = field(default_factory=Name)
|
||||
editor: Name = field(default_factory=Name)
|
||||
publisher: str = ""
|
||||
copyright: str = ""
|
||||
subjects: str = ""
|
||||
style: str = ""
|
||||
directory: str = ""
|
||||
isbn: str = ""
|
||||
uuid: str = ""
|
||||
source: str = ""
|
||||
images: List[Image] = field(default_factory=list)
|
||||
sections: List[Section] = field(default_factory=list)
|
||||
cover: Image = field(default_factory=Image)
|
||||
publish_date: str = ""
|
||||
|
||||
|
||||
@dataclass
|
||||
class BuildContext:
|
||||
book: Book = field(default_factory=Book)
|
||||
verbose: bool = False
|
||||
in_section: bool = False
|
||||
|
||||
+105
-123
@@ -1,174 +1,156 @@
|
||||
from uuid import uuid4
|
||||
from . import state
|
||||
from .models import Name, Image, Section
|
||||
from .models import Name, Image, Section, BuildContext
|
||||
|
||||
|
||||
def extract(line):
|
||||
def extract(line: str) -> str | None:
|
||||
if line.startswith("<!-- "):
|
||||
return line[5:-4]
|
||||
return None
|
||||
|
||||
|
||||
def identify(line):
|
||||
def identify(line: str, ctx: BuildContext) -> None:
|
||||
key, value = extract(line).split(" ", 1)
|
||||
match key:
|
||||
case "Directory:":
|
||||
state.inSection = False
|
||||
state.book.directory = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.directory = value.strip()
|
||||
case "Title:":
|
||||
state.inSection = False
|
||||
state.book.title = value.strip()
|
||||
if state.book.title.startswith("A "):
|
||||
state.book.titlesort = state.book.title[2:] + ", " + state.book.title[:1]
|
||||
if state.book.title.startswith("An "):
|
||||
state.book.titlesort = state.book.title[3:] + ", " + state.book.title[:2]
|
||||
if state.book.title.startswith("The "):
|
||||
state.book.titlesort = state.book.title[4:] + ", " + state.book.title[:3]
|
||||
ctx.in_section = False
|
||||
ctx.book.title = value.strip()
|
||||
if ctx.book.title.startswith("A "):
|
||||
ctx.book.titlesort = ctx.book.title[2:] + ", " + ctx.book.title[:1]
|
||||
if ctx.book.title.startswith("An "):
|
||||
ctx.book.titlesort = ctx.book.title[3:] + ", " + ctx.book.title[:2]
|
||||
if ctx.book.title.startswith("The "):
|
||||
ctx.book.titlesort = ctx.book.title[4:] + ", " + ctx.book.title[:3]
|
||||
case "Titlesort":
|
||||
state.inSection = False
|
||||
state.book.titlesort = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.titlesort = value.strip()
|
||||
case "Author:":
|
||||
state.inSection = False
|
||||
last, first = value.strip().split(', ')
|
||||
name = Name()
|
||||
name.first = first
|
||||
name.last = last
|
||||
state.book.author.append(name)
|
||||
ctx.in_section = False
|
||||
last, first = value.strip().split(", ")
|
||||
ctx.book.author.append(Name(first=first, last=last))
|
||||
case "Editor:":
|
||||
state.inSection = False
|
||||
state.book.editor.last, state.book.editor.first = value.strip().split(', ')
|
||||
ctx.in_section = False
|
||||
ctx.book.editor.last, ctx.book.editor.first = value.strip().split(", ")
|
||||
case "Artist:":
|
||||
state.inSection = False
|
||||
state.book.artist.last, state.book.artist.first = value.strip().split(', ')
|
||||
ctx.in_section = False
|
||||
ctx.book.artist.last, ctx.book.artist.first = value.strip().split(", ")
|
||||
case "Publisher:":
|
||||
state.inSection = False
|
||||
state.book.publisher = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.publisher = value.strip()
|
||||
case "Copyright:":
|
||||
state.inSection = False
|
||||
state.book.copyright = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.copyright = value.strip()
|
||||
case "ISBN:":
|
||||
state.inSection = False
|
||||
state.book.isbn = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.isbn = value.strip()
|
||||
case "UUID:":
|
||||
state.inSection = False
|
||||
if value.strip() == "":
|
||||
state.book.uuid = uuid4()
|
||||
else:
|
||||
state.book.uuid = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.uuid = str(uuid4()) if value.strip() == "" else value.strip()
|
||||
case "Source:":
|
||||
state.inSection = False
|
||||
state.book.source = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.source = value.strip()
|
||||
case "Subjects:":
|
||||
state.inSection = False
|
||||
state.book.subjects = value.strip()
|
||||
ctx.in_section = False
|
||||
ctx.book.subjects = value.strip()
|
||||
case "Series:":
|
||||
state.inSection = False
|
||||
state.book.series.number, state.book.series.name = value.strip().split(' ', 1)
|
||||
ctx.in_section = False
|
||||
ctx.book.series.number, ctx.book.series.name = value.strip().split(" ", 1)
|
||||
case "Cover:":
|
||||
state.inSection = False
|
||||
state.book.cover.name, state.book.cover.type = value.strip().split('.')
|
||||
ctx.in_section = False
|
||||
ctx.book.cover.name, ctx.book.cover.type = value.strip().split(".")
|
||||
case "TitlePage:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "title"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="title")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "CopyrightPage:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "copyright-page"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="copyright-page")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "Dedication:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "dedication"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="dedication")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "Text:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "text"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="text")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "Foreward:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "forward"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="forward")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "Notes:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "notes"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="notes")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "License:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "license"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="license")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "Acknowledgement:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "acknowledgement"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="acknowledgement")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "TOC:":
|
||||
state.inSection = True
|
||||
section = Section()
|
||||
section.type = "toc"
|
||||
section.name, section.title = value.strip().split(' ', 1)
|
||||
state.book.sections.append(section)
|
||||
ctx.in_section = True
|
||||
section = Section(type="toc")
|
||||
section.name, section.title = value.strip().split(" ", 1)
|
||||
ctx.book.sections.append(section)
|
||||
case "Image:":
|
||||
image = Image()
|
||||
image.name, imageText = value.strip().split(' ', 1)
|
||||
extension = image.name.strip().split('.')[1]
|
||||
image.name, image_text = value.strip().split(" ", 1)
|
||||
extension = image.name.strip().split(".")[1]
|
||||
match extension:
|
||||
case "jpg":
|
||||
image.type = "jpeg"
|
||||
case "jpeg":
|
||||
case "jpg" | "jpeg":
|
||||
image.type = "jpeg"
|
||||
case "png":
|
||||
image.type = "png"
|
||||
image.alt, image.caption = imageText.strip().split('::')
|
||||
state.book.images.append(image)
|
||||
image.alt, image.caption = image_text.strip().split("::")
|
||||
ctx.book.images.append(image)
|
||||
if not image.caption:
|
||||
imageCode = '<img src="../Images/' + image.name + '" alt="' + image.alt + '"/>\n'
|
||||
imageCode += ' <a id="' + image.name.split('.')[0] + '"><!-- Anchor --></a>\n'
|
||||
image_code = '<img src="../Images/' + image.name + '" alt="' + image.alt + '"/>\n'
|
||||
image_code += ' <a id="' + image.name.split(".")[0] + '"><!-- Anchor --></a>\n'
|
||||
else:
|
||||
imageCode = '<div class="center group">\n'
|
||||
imageCode += ' <img src="../Images/' + image.name + '" alt="' + image.alt + '" />\n'
|
||||
imageCode += ' <a id="' + image.name.split('.')[0] + '"><!-- Anchor --></a>\n'
|
||||
imageCode += ' <p class="caption">' + image.caption + '</p>\n'
|
||||
imageCode += '</div>\n<br />\n'
|
||||
state.book.sections[-1].body += imageCode
|
||||
image_code = '<div class="center group">\n'
|
||||
image_code += ' <img src="../Images/' + image.name + '" alt="' + image.alt + '" />\n'
|
||||
image_code += ' <a id="' + image.name.split(".")[0] + '"><!-- Anchor --></a>\n'
|
||||
image_code += ' <p class="caption">' + image.caption + "</p>\n"
|
||||
image_code += "</div>\n<br />\n"
|
||||
ctx.book.sections[-1].body += image_code
|
||||
case _:
|
||||
pass
|
||||
|
||||
|
||||
def findStyle(line):
|
||||
stylesheetFile = line.split(' ')[2].split('=')[1].replace('"', '').replace('>', '').strip()
|
||||
def find_style(line: str, ctx: BuildContext) -> None:
|
||||
stylesheet_file = line.split(" ")[2].split("=")[1].replace('"', "").replace(">", "").strip()
|
||||
contents = ""
|
||||
with open(stylesheetFile) as file:
|
||||
for line in file:
|
||||
if line != '<style>\n' and line != '</style>' and not line.strip().startswith("/*"):
|
||||
with open(stylesheet_file) as f:
|
||||
for line in f:
|
||||
if line != "<style>\n" and line != "</style>" and not line.strip().startswith("/*"):
|
||||
contents += line
|
||||
state.book.style = contents
|
||||
ctx.book.style = contents
|
||||
print("found style")
|
||||
|
||||
|
||||
def parseBook():
|
||||
filename = state.book.filename
|
||||
if state.verbose:
|
||||
print("Parsing {}".format(filename))
|
||||
with open(filename) as source:
|
||||
def parse_book(ctx: BuildContext) -> None:
|
||||
if ctx.verbose:
|
||||
print("Parsing {}".format(ctx.book.filename))
|
||||
with open(ctx.book.filename) as source:
|
||||
for line in source:
|
||||
if line.startswith('<!-- '):
|
||||
identify(line)
|
||||
if line.startswith("<!-- "):
|
||||
identify(line, ctx)
|
||||
elif line.startswith('<link rel="stylesheet"'):
|
||||
findStyle(line)
|
||||
elif state.inSection:
|
||||
state.book.sections[-1].body += line
|
||||
if state.verbose:
|
||||
print("Parse complete for {}".format(state.book.title))
|
||||
find_style(line, ctx)
|
||||
elif ctx.in_section:
|
||||
ctx.book.sections[-1].body += line
|
||||
if ctx.verbose:
|
||||
print("Parse complete for {}".format(ctx.book.title))
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
book = None
|
||||
inSection = False
|
||||
inStyle = False
|
||||
verbose = False
|
||||
+185
-186
@@ -1,202 +1,202 @@
|
||||
import os
|
||||
import subprocess
|
||||
from shutil import rmtree, copy
|
||||
from . import state
|
||||
from .models import BuildContext
|
||||
|
||||
|
||||
def writeStructure(overwrite):
|
||||
def write_structure(ctx: BuildContext, overwrite: bool) -> None:
|
||||
mode = 0o775
|
||||
|
||||
if os.path.isdir(state.book.directory):
|
||||
if os.path.isdir(ctx.book.directory):
|
||||
if overwrite:
|
||||
rmtree(state.book.directory)
|
||||
rmtree(ctx.book.directory)
|
||||
else:
|
||||
print("writeStructure: Directory exists.")
|
||||
print("write_structure: Directory exists.")
|
||||
exit()
|
||||
|
||||
os.mkdir(state.book.directory, mode)
|
||||
os.mkdir(ctx.book.directory, mode)
|
||||
|
||||
for directory in ["META-INF", "OEBPS", "OEBPS/Images", "OEBPS/Styles", "OEBPS/Text"]:
|
||||
path = os.path.join(state.book.directory, directory)
|
||||
os.mkdir(path, mode)
|
||||
if state.verbose:
|
||||
os.mkdir(os.path.join(ctx.book.directory, directory), mode)
|
||||
|
||||
if ctx.verbose:
|
||||
print("Finished writing ePub structure")
|
||||
|
||||
|
||||
def writeMisc():
|
||||
access = 'w'
|
||||
directory = state.book.directory
|
||||
def write_misc(ctx: BuildContext) -> None:
|
||||
directory = ctx.book.directory
|
||||
|
||||
file = os.path.join(state.book.directory, "mimetype")
|
||||
mimetype = open(file, access)
|
||||
mimetype.write("application/epub+zip")
|
||||
mimetype.close()
|
||||
with open(os.path.join(directory, "mimetype"), "w") as f:
|
||||
f.write("application/epub+zip")
|
||||
|
||||
file = os.path.join(directory, "META-INF", "com.apple.ibooks.display-options.xml")
|
||||
meta1 = open(file, access)
|
||||
meta1.write('<?xml version="1.0" encoding="UTF-8"?>\n<display_options>\n <platform name="*">\n <option name="specified-fonts">false</option>\n </platform>\n</display_options>')
|
||||
meta1.close()
|
||||
with open(os.path.join(directory, "META-INF", "com.apple.ibooks.display-options.xml"), "w") as f:
|
||||
f.write(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
"<display_options>\n"
|
||||
' <platform name="*">\n'
|
||||
' <option name="specified-fonts">false</option>\n'
|
||||
" </platform>\n"
|
||||
"</display_options>"
|
||||
)
|
||||
|
||||
file = os.path.join(directory, "META-INF", "container.xml")
|
||||
meta2 = open(file, access)
|
||||
meta2.write('<?xml version="1.0" encoding="UTF-8"?>\n<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n <rootfiles>\n <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>\n </rootfiles>\n</container>')
|
||||
meta2.close()
|
||||
if state.verbose:
|
||||
with open(os.path.join(directory, "META-INF", "container.xml"), "w") as f:
|
||||
f.write(
|
||||
'<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
'<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">\n'
|
||||
" <rootfiles>\n"
|
||||
' <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>\n'
|
||||
" </rootfiles>\n"
|
||||
"</container>"
|
||||
)
|
||||
|
||||
if ctx.verbose:
|
||||
print("Finished writing metadata")
|
||||
|
||||
|
||||
def writeStyles():
|
||||
access = 'w'
|
||||
file = os.path.join(state.book.directory, "OEBPS", "Styles", "style.css")
|
||||
style = open(file, access)
|
||||
style.write(state.book.style)
|
||||
style.close()
|
||||
def write_styles(ctx: BuildContext) -> None:
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "Styles", "style.css"), "w") as f:
|
||||
f.write(ctx.book.style)
|
||||
|
||||
source_dir = os.path.split(state.book.filename)[0]
|
||||
styles_dir = os.path.join(state.book.directory, "OEBPS", "Styles")
|
||||
source_dir = os.path.split(ctx.book.filename)[0]
|
||||
styles_dir = os.path.join(ctx.book.directory, "OEBPS", "Styles")
|
||||
for font in ["Inconsolata-Regular.otf", "invisible1.ttf", "MgOpenModataRegular.ttf"]:
|
||||
copy(os.path.join(source_dir, "Images", font), os.path.join(styles_dir, font))
|
||||
|
||||
if state.verbose:
|
||||
if ctx.verbose:
|
||||
print("Finished writing style")
|
||||
|
||||
|
||||
def writeNCX():
|
||||
directory = state.book.directory
|
||||
access = 'w'
|
||||
file = os.path.join(directory, 'OEBPS', 'toc.ncx')
|
||||
def write_ncx(ctx: BuildContext) -> None:
|
||||
navpoints = []
|
||||
|
||||
contentsstart = (
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n'
|
||||
'<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">\n'
|
||||
'<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">\n'
|
||||
' <head>\n'
|
||||
' <meta content="urn:isbn:' + str(state.book.uuid) + '" name="dtb:uid"/>\n'
|
||||
' <meta content="1" name="dtb:depth"/>\n'
|
||||
' <meta content="-1" name="dtb:totalPageCount"/>\n'
|
||||
' <meta content="-1" name="dtb:maxPageNumber"/>\n'
|
||||
' </head>\n'
|
||||
' <docTitle>\n'
|
||||
' <text>' + state.book.title.strip() + '</text>\n'
|
||||
' </docTitle>\n'
|
||||
' <navMap>\n'
|
||||
' <navPoint id="cover" playOrder="1">\n'
|
||||
' <navLabel>\n'
|
||||
' <text>Cover</text>\n'
|
||||
' </navLabel>\n'
|
||||
' <content src="Text/cover.xhtml"/>\n'
|
||||
' </navPoint>\n'
|
||||
)
|
||||
contentsend = " </navMap>\n</ncx>"
|
||||
|
||||
number = 2
|
||||
for section in state.book.sections:
|
||||
for section in ctx.book.sections:
|
||||
if section.type != "toc":
|
||||
navpoints.append(
|
||||
'\t<navPoint id ="' + section.name.strip() + '" playOrder="' + str(number) + '">\n'
|
||||
'\t\t<navLabel>\n\t\t\t<text>' + section.title.strip() + '</text>\n\t\t</navLabel>\n'
|
||||
"\t\t<navLabel>\n\t\t\t<text>" + section.title.strip() + "</text>\n\t\t</navLabel>\n"
|
||||
'\t\t<content src="Text/' + section.name.strip() + '.xhtml"/>\n\t</navPoint>\n'
|
||||
)
|
||||
number += 1
|
||||
|
||||
contents = contentsstart + "".join(navpoints) + contentsend
|
||||
contents = (
|
||||
'<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n'
|
||||
'<!DOCTYPE ncx PUBLIC "-//NISO//DTD ncx 2005-1//EN" "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd">\n'
|
||||
'<ncx xmlns="http://www.daisy.org/z3986/2005/ncx/" version="2005-1">\n'
|
||||
" <head>\n"
|
||||
' <meta content="urn:isbn:' + str(ctx.book.uuid) + '" name="dtb:uid"/>\n'
|
||||
' <meta content="1" name="dtb:depth"/>\n'
|
||||
' <meta content="-1" name="dtb:totalPageCount"/>\n'
|
||||
' <meta content="-1" name="dtb:maxPageNumber"/>\n'
|
||||
" </head>\n"
|
||||
" <docTitle>\n"
|
||||
" <text>" + ctx.book.title.strip() + "</text>\n"
|
||||
" </docTitle>\n"
|
||||
" <navMap>\n"
|
||||
' <navPoint id="cover" playOrder="1">\n'
|
||||
" <navLabel>\n"
|
||||
" <text>Cover</text>\n"
|
||||
" </navLabel>\n"
|
||||
' <content src="Text/cover.xhtml"/>\n'
|
||||
" </navPoint>\n"
|
||||
+ "".join(navpoints)
|
||||
+ " </navMap>\n</ncx>"
|
||||
)
|
||||
|
||||
ncx = open(file, access)
|
||||
ncx.write(contents)
|
||||
ncx.close()
|
||||
if state.verbose:
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx"), "w") as f:
|
||||
f.write(contents)
|
||||
|
||||
if ctx.verbose:
|
||||
print("Finished writing ncx")
|
||||
|
||||
|
||||
def writeTOC(section):
|
||||
access = 'w'
|
||||
file = os.path.join(state.book.directory, 'OEBPS', 'toc.xhtml')
|
||||
def write_toc(ctx: BuildContext, section) -> None:
|
||||
contents = (
|
||||
'<?xml version="1.0" encoding="utf-8"?>\n'
|
||||
'<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">\n'
|
||||
' <head>\n'
|
||||
' <title>Table of Contents</title>\n'
|
||||
" <head>\n"
|
||||
" <title>Table of Contents</title>\n"
|
||||
' <link href="Styles/style.css" rel="stylesheet" type="text/css" />\n'
|
||||
'</head>\n'
|
||||
'<body>\n'
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
+ section.body
|
||||
+ "\n</body>\n</html>"
|
||||
)
|
||||
contents += section.body
|
||||
contents += '\n</body>\n</html>'
|
||||
toc = open(file, access)
|
||||
toc.write(contents)
|
||||
toc.close()
|
||||
if state.verbose:
|
||||
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.xhtml"), "w") as f:
|
||||
f.write(contents)
|
||||
|
||||
if ctx.verbose:
|
||||
print("Finished writing toc")
|
||||
|
||||
|
||||
def writeCover():
|
||||
access = 'w'
|
||||
file = os.path.join(state.book.directory, 'OEBPS', 'Text', 'cover.xhtml')
|
||||
def write_cover(ctx: BuildContext) -> None:
|
||||
contents = (
|
||||
'<?xml version="1.0" encoding="utf-8" standalone="no"?>\n'
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"\n'
|
||||
'"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n\n'
|
||||
'<html xmlns="http://www.w3.org/1999/xhtml">\n'
|
||||
' <head>\n'
|
||||
' <title>Cover</title>\n'
|
||||
" <head>\n"
|
||||
" <title>Cover</title>\n"
|
||||
' <style type="text/css">\n'
|
||||
' /* <![CDATA[ */\n'
|
||||
' img { max-width: 100%; }\n'
|
||||
' body { oeb-column-number: 1; }\n'
|
||||
' #cover-image { text-align: center; }\n'
|
||||
' /* ]]> */\n'
|
||||
' </style>\n'
|
||||
' </head>\n'
|
||||
' <body>\n'
|
||||
" /* <![CDATA[ */\n"
|
||||
" img { max-width: 100%; }\n"
|
||||
" body { oeb-column-number: 1; }\n"
|
||||
" #cover-image { text-align: center; }\n"
|
||||
" /* ]]> */\n"
|
||||
" </style>\n"
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
' <div id="cover-image">\n'
|
||||
' <img alt="' + state.book.title.strip() + '" src="../Images/cover.jpeg" />\n'
|
||||
' </div>\n'
|
||||
' </body>\n'
|
||||
'</html>\n'
|
||||
' <img alt="' + ctx.book.title.strip() + '" src="../Images/cover.jpeg" />\n'
|
||||
" </div>\n"
|
||||
" </body>\n"
|
||||
"</html>\n"
|
||||
)
|
||||
cover = open(file, access)
|
||||
cover.write(contents)
|
||||
cover.close()
|
||||
|
||||
new = os.path.join(state.book.directory, "OEBPS", "Images", "cover.jpeg")
|
||||
old = os.path.join(os.path.split(state.book.filename)[0], "Images", "cover.jpeg")
|
||||
copy(old, new)
|
||||
if state.verbose:
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "Text", "cover.xhtml"), "w") as f:
|
||||
f.write(contents)
|
||||
|
||||
source_dir = os.path.split(ctx.book.filename)[0]
|
||||
copy(
|
||||
os.path.join(source_dir, "Images", "cover.jpeg"),
|
||||
os.path.join(ctx.book.directory, "OEBPS", "Images", "cover.jpeg"),
|
||||
)
|
||||
|
||||
if ctx.verbose:
|
||||
print("Finished writing cover")
|
||||
|
||||
|
||||
def writeContents():
|
||||
def write_contents(ctx: BuildContext) -> None:
|
||||
contents = '<?xml version="1.0" encoding="UTF-8"?>\n'
|
||||
contents += '<package xmlns="http://www.idpf.org/2007/opf" version="3.0" xml:lang="en" unique-identifier="bookid" prefix="rendition: http://www.idpf.org/vocab/rendition/# cc: http://creativecommons.org/ns#">\n'
|
||||
contents += ' <metadata xmlns:dc="http://purl.org/dc/elements/1.1/">\n'
|
||||
contents += ' <dc:identifier id="bookid">urn:uuid' + state.book.isbn + '</dc:identifier>\n'
|
||||
contents += ' <dc:title id="pub-title">' + state.book.title.strip() + '</dc:title>\n'
|
||||
contents += ' <dc:identifier id="bookid">urn:uuid' + ctx.book.isbn + "</dc:identifier>\n"
|
||||
contents += ' <dc:title id="pub-title">' + ctx.book.title.strip() + "</dc:title>\n"
|
||||
contents += ' <meta refines="#pub-title" property="title-type">main</meta>\n'
|
||||
if state.book.titlesort != "":
|
||||
contents += ' <meta refines="#pub-title" property="file-as">' + state.book.titlesort + '</meta>\n'
|
||||
if state.book.series.name != "":
|
||||
if ctx.book.titlesort:
|
||||
contents += ' <meta refines="#pub-title" property="file-as">' + ctx.book.titlesort + "</meta>\n"
|
||||
if ctx.book.series.name:
|
||||
print("found a book series")
|
||||
contents += ' <dc:title id="pub-title2">' + state.book.series.name.strip() + '</dc:title>\n'
|
||||
contents += ' <dc:title id="pub-title2">' + ctx.book.series.name.strip() + "</dc:title>\n"
|
||||
contents += ' <meta refines="#pub-title2" property="title-type">collection</meta>\n'
|
||||
contents += ' <dc:title id="pub-title3">' + state.book.series.number + '</dc:title>\n'
|
||||
contents += ' <dc:title id="pub-title3">' + ctx.book.series.number + "</dc:title>\n"
|
||||
contents += ' <meta refines="#pub-title3" property="title-type">display-seq</meta>\n'
|
||||
contents += ' <meta name="calibre:series" content="' + state.book.series.name.strip() + '" />\n'
|
||||
contents += ' <meta name="calibre:series_index" content="' + state.book.series.number.strip() + '" />\n'
|
||||
contents += ' <meta name="calibre:series" content="' + ctx.book.series.name.strip() + '" />\n'
|
||||
contents += ' <meta name="calibre:series_index" content="' + ctx.book.series.number.strip() + '" />\n'
|
||||
contents += ' <dc:language id="pub-language">en</dc:language>\n'
|
||||
contents += ' <dc:date>' + state.book.publishDate + '</dc:date>\n'
|
||||
for name in state.book.author:
|
||||
contents += ' <dc:creator id="author">' + name.first.strip() + ' ' + name.last.strip() + '</dc:creator>\n'
|
||||
contents += " <dc:date>" + ctx.book.publish_date + "</dc:date>\n"
|
||||
for name in ctx.book.author:
|
||||
contents += ' <dc:creator id="author">' + name.first.strip() + " " + name.last.strip() + "</dc:creator>\n"
|
||||
contents += ' <meta refines="#author" scheme="marc:relators" property="role">aut</meta>\n'
|
||||
for name in state.book.author:
|
||||
contents += ' <meta refines="#author" property="file-as">' + name.last.strip() + ', ' + name.first.strip() + '</meta>\n'
|
||||
contents += ' <dc:publisher>' + state.book.publisher.strip() + '</dc:publisher>\n'
|
||||
contents += ' <dc:rights>' + state.book.copyright.strip() + '</dc:rights>\n'
|
||||
contents += ' <dc:subject>' + state.book.subjects.strip() + '</dc:subject>\n'
|
||||
contents += ' <dc:source>' + state.book.source.strip() + '</dc:source>\n'
|
||||
for name in ctx.book.author:
|
||||
contents += ' <meta refines="#author" property="file-as">' + name.last.strip() + ", " + name.first.strip() + "</meta>\n"
|
||||
contents += " <dc:publisher>" + ctx.book.publisher.strip() + "</dc:publisher>\n"
|
||||
contents += " <dc:rights>" + ctx.book.copyright.strip() + "</dc:rights>\n"
|
||||
contents += " <dc:subject>" + ctx.book.subjects.strip() + "</dc:subject>\n"
|
||||
contents += " <dc:source>" + ctx.book.source.strip() + "</dc:source>\n"
|
||||
contents += ' <meta name="cover" content="cover.jpeg" />\n'
|
||||
contents += ' </metadata>\n'
|
||||
contents += ' <manifest>\n'
|
||||
contents += " </metadata>\n"
|
||||
contents += " <manifest>\n"
|
||||
contents += ' <item href="Images/cover.jpeg" id="cover.jpeg" media-type="image/jpeg" properties="cover-image" />\n'
|
||||
contents += ' <item href="Text/cover.xhtml" id="cover" media-type="application/xhtml+xml" />\n'
|
||||
contents += ' <item href="toc.xhtml" id="toc" media-type="application/xhtml+xml" properties="nav"/>\n'
|
||||
@@ -206,107 +206,106 @@ def writeContents():
|
||||
contents += ' <item href="Styles/invisible1.ttf" id="invisible1" media-type="application/vnd.ms-opentype" />\n'
|
||||
contents += ' <item href="Styles/Inconsolata-Regular.otf" id="inconsolate" media-type="application/vnd.ms-opentype" />\n'
|
||||
|
||||
if len(state.book.images) > 0:
|
||||
if ctx.book.images:
|
||||
contents += ' <item href="Text/illustrations.xhtml" id="illustrations" media-type="application/xhtml+xml" />\n'
|
||||
|
||||
for section in state.book.sections:
|
||||
if section.type != 'toc':
|
||||
for section in ctx.book.sections:
|
||||
if section.type != "toc":
|
||||
contents += ' <item href="Text/' + section.name + '.xhtml" id="' + section.name + '" media-type="application/xhtml+xml" />\n'
|
||||
|
||||
for image in state.book.images:
|
||||
contents += ' <item href="Images/' + image.name + '" id="' + image.name.split('.')[0] + '" media-type="image/' + image.type + '" />\n'
|
||||
new = os.path.join(state.book.directory, "OEBPS", "Images", image.name)
|
||||
old = os.path.join(os.path.split(state.book.filename)[0], "Images", image.name)
|
||||
copy(old, new)
|
||||
source_dir = os.path.split(ctx.book.filename)[0]
|
||||
for image in ctx.book.images:
|
||||
contents += ' <item href="Images/' + image.name + '" id="' + image.name.split(".")[0] + '" media-type="image/' + image.type + '" />\n'
|
||||
copy(
|
||||
os.path.join(source_dir, "Images", image.name),
|
||||
os.path.join(ctx.book.directory, "OEBPS", "Images", image.name),
|
||||
)
|
||||
|
||||
contents += ' </manifest>\n'
|
||||
contents += " </manifest>\n"
|
||||
contents += ' <spine toc="ncx">\n'
|
||||
contents += ' <itemref idref="cover" />\n'
|
||||
if len(state.book.images) > 0:
|
||||
if ctx.book.images:
|
||||
contents += ' <itemref idref="illustrations" />\n'
|
||||
for section in state.book.sections:
|
||||
for section in ctx.book.sections:
|
||||
contents += ' <itemref idref="' + section.name + '" />\n'
|
||||
contents += ' </spine>\n'
|
||||
contents += ' <guide>\n'
|
||||
contents += " </spine>\n"
|
||||
contents += " <guide>\n"
|
||||
contents += ' <reference href="Text/cover.xhtml" title="Cover" type="cover" />\n'
|
||||
contents += ' <reference href="toc.xhtml" title="Table of Contents" type="toc" />\n'
|
||||
if len(state.book.images) > 0:
|
||||
if ctx.book.images:
|
||||
contents += ' <reference href="Text/illustrations.xhtml" title="Illustrations" type="text" />\n'
|
||||
for section in state.book.sections:
|
||||
if section.type != 'toc':
|
||||
for section in ctx.book.sections:
|
||||
if section.type != "toc":
|
||||
contents += ' <reference href="Text/' + section.name.strip() + '.xhtml" title="' + section.title.strip() + '" type="' + section.type.strip() + '" />\n'
|
||||
contents += ' </guide>\n'
|
||||
contents += '</package>\n'
|
||||
contents += " </guide>\n"
|
||||
contents += "</package>\n"
|
||||
|
||||
access = 'w'
|
||||
file = os.path.join(state.book.directory, 'OEBPS', 'content.opf')
|
||||
content = open(file, access)
|
||||
content.write(contents)
|
||||
content.close()
|
||||
if state.verbose:
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf"), "w") as f:
|
||||
f.write(contents)
|
||||
|
||||
if ctx.verbose:
|
||||
print("Finished writing content")
|
||||
|
||||
|
||||
def writeSections():
|
||||
access = 'w'
|
||||
for section in state.book.sections:
|
||||
def write_sections(ctx: BuildContext) -> None:
|
||||
for section in ctx.book.sections:
|
||||
if section.type.strip() == "toc":
|
||||
writeTOC(section)
|
||||
write_toc(ctx, section)
|
||||
continue
|
||||
contents = (
|
||||
'<?xml version="1.0" encoding="utf-8" standalone="no"?>\n'
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"\n'
|
||||
'"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n\n'
|
||||
'<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">\n'
|
||||
' <head>\n'
|
||||
' <title>' + section.title.strip() + '</title>\n'
|
||||
" <head>\n"
|
||||
" <title>" + section.title.strip() + "</title>\n"
|
||||
' <link href="../Styles/style.css" rel="stylesheet" type="text/css" />\n'
|
||||
' </head>\n'
|
||||
' <body>\n'
|
||||
" </head>\n"
|
||||
" <body>\n"
|
||||
' <div id="' + section.name.strip() + '" xml:lang="en-US">\n'
|
||||
+ section.body
|
||||
+ " </div>\n </body>\n</html>\n"
|
||||
)
|
||||
contents += section.body
|
||||
contents += " </div>\n </body>\n</html>\n"
|
||||
file = os.path.join(state.book.directory, 'OEBPS', 'Text', section.name.strip() + ".xhtml")
|
||||
content = open(file, access)
|
||||
content.write(contents)
|
||||
content.close()
|
||||
if state.verbose:
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "Text", section.name.strip() + ".xhtml"), "w") as f:
|
||||
f.write(contents)
|
||||
if ctx.verbose:
|
||||
print(f"Finished writing section {section.name}.xhtml")
|
||||
|
||||
|
||||
def writeIllustrations():
|
||||
if len(state.book.images) == 0:
|
||||
def write_illustrations(ctx: BuildContext) -> None:
|
||||
if not ctx.book.images:
|
||||
return
|
||||
|
||||
access = 'w'
|
||||
contents = (
|
||||
'<?xml version="1.0" encoding="utf-8" standalone="no"?>\n'
|
||||
'<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"\n'
|
||||
'"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">\n\n'
|
||||
'<html xmlns="http://www.w3.org/1999/xhtml" xmlns:epub="http://www.idpf.org/2007/ops">\n'
|
||||
'<head>\n'
|
||||
'<title>Illustrations</title>\n'
|
||||
"<head>\n"
|
||||
"<title>Illustrations</title>\n"
|
||||
'<link href="../Styles/style.css" rel="stylesheet" type="text/css" />\n'
|
||||
'</head>\n'
|
||||
'<body>\n'
|
||||
"</head>\n"
|
||||
"<body>\n"
|
||||
' <h1 class="title">Illustrations</h1>\n'
|
||||
' <table>\n'
|
||||
" <table>\n"
|
||||
)
|
||||
for image in state.book.images:
|
||||
contents += ' <tr><td><a href="../Images/' + image.name + '">' + image.caption + '</a></td></tr>\n'
|
||||
contents += ' </table>\n</body>\n</html>'
|
||||
for image in ctx.book.images:
|
||||
contents += ' <tr><td><a href="../Images/' + image.name + '">' + image.caption + "</a></td></tr>\n"
|
||||
contents += " </table>\n</body>\n</html>"
|
||||
|
||||
file = os.path.join(state.book.directory, 'OEBPS', 'Text', "illustrations.xhtml")
|
||||
content = open(file, access)
|
||||
content.write(contents)
|
||||
content.close()
|
||||
if state.verbose:
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "Text", "illustrations.xhtml"), "w") as f:
|
||||
f.write(contents)
|
||||
|
||||
if ctx.verbose:
|
||||
print("Finished writing section illustrations.xhtml")
|
||||
|
||||
|
||||
def writeEpub():
|
||||
cmd = f'cd {state.book.directory}; zip -X0 "{state.book.directory}.epub" mimetype; zip -X9Dr "{state.book.directory}.epub" META-INF OEBPS'
|
||||
def write_epub(ctx: BuildContext) -> None:
|
||||
cmd = (
|
||||
f'cd {ctx.book.directory}; '
|
||||
f'zip -X0 "{ctx.book.directory}.epub" mimetype; '
|
||||
f'zip -X9Dr "{ctx.book.directory}.epub" META-INF OEBPS'
|
||||
)
|
||||
subprocess.run(cmd, shell=True)
|
||||
if state.verbose:
|
||||
print(f"Finished writing {state.book.directory}.epub")
|
||||
if ctx.verbose:
|
||||
print(f"Finished writing {ctx.book.directory}.epub")
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
from epubmaker.models import Name, Series, Image, Section, Book, BuildContext
|
||||
|
||||
|
||||
def test_name_defaults():
|
||||
n = Name()
|
||||
assert n.first == ""
|
||||
assert n.last == ""
|
||||
|
||||
|
||||
def test_series_defaults():
|
||||
s = Series()
|
||||
assert s.number == ""
|
||||
assert s.name == ""
|
||||
|
||||
|
||||
def test_image_defaults():
|
||||
img = Image()
|
||||
assert img.name == ""
|
||||
assert img.type == ""
|
||||
assert img.alt == ""
|
||||
assert img.caption == ""
|
||||
|
||||
|
||||
def test_section_defaults():
|
||||
s = Section()
|
||||
assert s.type == ""
|
||||
assert s.name == ""
|
||||
assert s.title == ""
|
||||
assert s.body == ""
|
||||
|
||||
|
||||
def test_book_defaults():
|
||||
b = Book()
|
||||
assert b.filename == ""
|
||||
assert b.title == ""
|
||||
assert b.titlesort == ""
|
||||
assert b.publisher == ""
|
||||
assert b.isbn == ""
|
||||
assert b.uuid == ""
|
||||
assert b.author == []
|
||||
assert b.images == []
|
||||
assert b.sections == []
|
||||
|
||||
|
||||
def test_book_nested_objects():
|
||||
b = Book()
|
||||
assert isinstance(b.series, Series)
|
||||
assert isinstance(b.artist, Name)
|
||||
assert isinstance(b.editor, Name)
|
||||
assert isinstance(b.cover, Image)
|
||||
|
||||
|
||||
def test_book_author_lists_are_independent():
|
||||
b1 = Book()
|
||||
b2 = Book()
|
||||
b1.author.append(Name(first="Jane", last="Doe"))
|
||||
assert len(b2.author) == 0
|
||||
|
||||
|
||||
def test_build_context_defaults():
|
||||
ctx = BuildContext()
|
||||
assert isinstance(ctx.book, Book)
|
||||
assert ctx.verbose is False
|
||||
assert ctx.in_section is False
|
||||
|
||||
|
||||
def test_build_context_books_are_independent():
|
||||
ctx1 = BuildContext()
|
||||
ctx2 = BuildContext()
|
||||
ctx1.book.title = "Book One"
|
||||
assert ctx2.book.title == ""
|
||||
@@ -0,0 +1,249 @@
|
||||
import pytest
|
||||
from epubmaker.models import BuildContext
|
||||
from epubmaker.parser import extract, identify, parse_book
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx():
|
||||
return BuildContext()
|
||||
|
||||
|
||||
def test_extract_returns_inner_content():
|
||||
assert extract("<!-- Title: Foo Bar -->") == "Title: Foo Bar"
|
||||
|
||||
|
||||
def test_extract_with_newline():
|
||||
assert extract("<!-- Title: Foo Bar -->\n") == "Title: Foo Bar"
|
||||
|
||||
|
||||
def test_extract_returns_none_for_non_comment():
|
||||
assert extract("<p>Not a comment</p>") is None
|
||||
|
||||
|
||||
def test_identify_directory(ctx):
|
||||
identify("<!-- Directory: mybook -->", ctx)
|
||||
assert ctx.book.directory == "mybook"
|
||||
assert ctx.in_section is False
|
||||
|
||||
|
||||
def test_identify_title_plain(ctx):
|
||||
identify("<!-- Title: My Book -->", ctx)
|
||||
assert ctx.book.title == "My Book"
|
||||
assert ctx.book.titlesort == ""
|
||||
|
||||
|
||||
def test_identify_title_the(ctx):
|
||||
identify("<!-- Title: The Great Book -->", ctx)
|
||||
assert ctx.book.title == "The Great Book"
|
||||
assert ctx.book.titlesort == "Great Book, The"
|
||||
|
||||
|
||||
def test_identify_title_a(ctx):
|
||||
identify("<!-- Title: A Good Story -->", ctx)
|
||||
assert ctx.book.title == "A Good Story"
|
||||
assert ctx.book.titlesort == "Good Story, A"
|
||||
|
||||
|
||||
def test_identify_title_an(ctx):
|
||||
identify("<!-- Title: An Old Tale -->", ctx)
|
||||
assert ctx.book.title == "An Old Tale"
|
||||
assert ctx.book.titlesort == "Old Tale, An"
|
||||
|
||||
|
||||
def test_identify_author(ctx):
|
||||
identify("<!-- Author: Smith, John -->", ctx)
|
||||
assert len(ctx.book.author) == 1
|
||||
assert ctx.book.author[0].last == "Smith"
|
||||
assert ctx.book.author[0].first == "John"
|
||||
|
||||
|
||||
def test_identify_multiple_authors(ctx):
|
||||
identify("<!-- Author: Smith, John -->", ctx)
|
||||
identify("<!-- Author: Jones, Jane -->", ctx)
|
||||
assert len(ctx.book.author) == 2
|
||||
assert ctx.book.author[1].last == "Jones"
|
||||
|
||||
|
||||
def test_identify_editor(ctx):
|
||||
identify("<!-- Editor: Brown, Alice -->", ctx)
|
||||
assert ctx.book.editor.last == "Brown"
|
||||
assert ctx.book.editor.first == "Alice"
|
||||
|
||||
|
||||
def test_identify_publisher(ctx):
|
||||
identify("<!-- Publisher: Acme Press -->", ctx)
|
||||
assert ctx.book.publisher == "Acme Press"
|
||||
|
||||
|
||||
def test_identify_copyright(ctx):
|
||||
identify("<!-- Copyright: 2024 Author -->", ctx)
|
||||
assert ctx.book.copyright == "2024 Author"
|
||||
|
||||
|
||||
def test_identify_isbn(ctx):
|
||||
identify("<!-- ISBN: 978-0-000-00000-0 -->", ctx)
|
||||
assert ctx.book.isbn == "978-0-000-00000-0"
|
||||
|
||||
|
||||
def test_identify_uuid_explicit(ctx):
|
||||
identify("<!-- UUID: abc-123 -->", ctx)
|
||||
assert ctx.book.uuid == "abc-123"
|
||||
|
||||
|
||||
def test_identify_uuid_empty_generates_uuid(ctx):
|
||||
identify("<!-- UUID: -->", ctx)
|
||||
assert ctx.book.uuid != ""
|
||||
assert ctx.book.uuid is not None
|
||||
|
||||
|
||||
def test_identify_source(ctx):
|
||||
identify("<!-- Source: Some Original Work -->", ctx)
|
||||
assert ctx.book.source == "Some Original Work"
|
||||
|
||||
|
||||
def test_identify_subjects(ctx):
|
||||
identify("<!-- Subjects: Fiction, Mystery -->", ctx)
|
||||
assert ctx.book.subjects == "Fiction, Mystery"
|
||||
|
||||
|
||||
def test_identify_series(ctx):
|
||||
identify("<!-- Series: 1 My Series Name -->", ctx)
|
||||
assert ctx.book.series.number == "1"
|
||||
assert ctx.book.series.name == "My Series Name"
|
||||
|
||||
|
||||
def test_identify_cover(ctx):
|
||||
identify("<!-- Cover: cover.jpeg -->", ctx)
|
||||
assert ctx.book.cover.name == "cover"
|
||||
assert ctx.book.cover.type == "jpeg"
|
||||
|
||||
|
||||
def test_identify_text_section(ctx):
|
||||
identify("<!-- Text: chapter1 Chapter One -->", ctx)
|
||||
assert ctx.in_section is True
|
||||
assert len(ctx.book.sections) == 1
|
||||
assert ctx.book.sections[0].type == "text"
|
||||
assert ctx.book.sections[0].name == "chapter1"
|
||||
assert ctx.book.sections[0].title == "Chapter One"
|
||||
|
||||
|
||||
def test_identify_toc_section(ctx):
|
||||
identify("<!-- TOC: toc Table of Contents -->", ctx)
|
||||
assert ctx.in_section is True
|
||||
assert ctx.book.sections[0].type == "toc"
|
||||
|
||||
|
||||
def test_identify_title_page(ctx):
|
||||
identify("<!-- TitlePage: tp Title Page -->", ctx)
|
||||
assert ctx.book.sections[0].type == "title"
|
||||
|
||||
|
||||
def test_identify_copyright_page(ctx):
|
||||
identify("<!-- CopyrightPage: cp Copyright -->", ctx)
|
||||
assert ctx.book.sections[0].type == "copyright-page"
|
||||
|
||||
|
||||
def test_identify_dedication(ctx):
|
||||
identify("<!-- Dedication: ded For someone -->", ctx)
|
||||
assert ctx.book.sections[0].type == "dedication"
|
||||
|
||||
|
||||
def test_identify_foreward(ctx):
|
||||
identify("<!-- Foreward: fw Foreword -->", ctx)
|
||||
assert ctx.book.sections[0].type == "forward"
|
||||
|
||||
|
||||
def test_identify_notes(ctx):
|
||||
identify("<!-- Notes: notes Notes -->", ctx)
|
||||
assert ctx.book.sections[0].type == "notes"
|
||||
|
||||
|
||||
def test_identify_acknowledgement(ctx):
|
||||
identify("<!-- Acknowledgement: ack Acknowledgements -->", ctx)
|
||||
assert ctx.book.sections[0].type == "acknowledgement"
|
||||
|
||||
|
||||
def test_identify_metadata_clears_in_section(ctx):
|
||||
identify("<!-- Text: ch1 Chapter One -->", ctx)
|
||||
assert ctx.in_section is True
|
||||
identify("<!-- Title: My Book -->", ctx)
|
||||
assert ctx.in_section is False
|
||||
|
||||
|
||||
def test_identify_image_jpeg(ctx):
|
||||
identify("<!-- Text: ch1 Chapter One -->", ctx)
|
||||
identify("<!-- Image: photo.jpg Alt text::Caption text -->", ctx)
|
||||
assert len(ctx.book.images) == 1
|
||||
assert ctx.book.images[0].name == "photo.jpg"
|
||||
assert ctx.book.images[0].type == "jpeg"
|
||||
assert ctx.book.images[0].alt == "Alt text"
|
||||
assert ctx.book.images[0].caption == "Caption text"
|
||||
|
||||
|
||||
def test_identify_image_png(ctx):
|
||||
identify("<!-- Text: ch1 Chapter One -->", ctx)
|
||||
identify("<!-- Image: photo.png Alt text::Caption -->", ctx)
|
||||
assert ctx.book.images[0].type == "png"
|
||||
|
||||
|
||||
def test_identify_image_appends_to_section_body(ctx):
|
||||
identify("<!-- Text: ch1 Chapter One -->", ctx)
|
||||
identify("<!-- Image: photo.jpg Alt::Caption -->", ctx)
|
||||
assert "photo.jpg" in ctx.book.sections[-1].body
|
||||
|
||||
|
||||
def test_identify_image_with_caption_uses_div(ctx):
|
||||
identify("<!-- Text: ch1 Chapter One -->", ctx)
|
||||
identify("<!-- Image: photo.jpg Alt::My Caption -->", ctx)
|
||||
assert '<div class="center group">' in ctx.book.sections[-1].body
|
||||
assert "My Caption" in ctx.book.sections[-1].body
|
||||
|
||||
|
||||
def test_identify_image_without_caption_uses_img(ctx):
|
||||
identify("<!-- Text: ch1 Chapter One -->", ctx)
|
||||
identify("<!-- Image: photo.jpg Alt:: -->", ctx)
|
||||
body = ctx.book.sections[-1].body
|
||||
assert "<img" in body
|
||||
assert '<div class="center group">' not in body
|
||||
|
||||
|
||||
def test_identify_unknown_key_ignored(ctx):
|
||||
identify("<!-- UnknownKey: some value -->", ctx)
|
||||
assert ctx.book.title == ""
|
||||
assert ctx.book.directory == ""
|
||||
|
||||
|
||||
def test_parse_book_populates_book(tmp_path):
|
||||
source = tmp_path / "book.html"
|
||||
source.write_text(
|
||||
"<!-- Directory: mybook -->\n"
|
||||
"<!-- Title: Test Book -->\n"
|
||||
"<!-- Author: Doe, Jane -->\n"
|
||||
"<!-- Text: ch1 Chapter One -->\n"
|
||||
"<p>Body content</p>\n"
|
||||
)
|
||||
ctx = BuildContext()
|
||||
ctx.book.filename = str(source)
|
||||
parse_book(ctx)
|
||||
assert ctx.book.title == "Test Book"
|
||||
assert ctx.book.directory == "mybook"
|
||||
assert len(ctx.book.author) == 1
|
||||
assert ctx.book.author[0].last == "Doe"
|
||||
assert len(ctx.book.sections) == 1
|
||||
assert "<p>Body content</p>\n" in ctx.book.sections[0].body
|
||||
|
||||
|
||||
def test_parse_book_body_not_captured_outside_section(tmp_path):
|
||||
source = tmp_path / "book.html"
|
||||
source.write_text(
|
||||
"<!-- Title: Test Book -->\n"
|
||||
"<p>This is outside any section</p>\n"
|
||||
"<!-- Text: ch1 Chapter One -->\n"
|
||||
"<p>Inside section</p>\n"
|
||||
)
|
||||
ctx = BuildContext()
|
||||
ctx.book.filename = str(source)
|
||||
parse_book(ctx)
|
||||
assert len(ctx.book.sections) == 1
|
||||
assert "outside any section" not in ctx.book.sections[0].body
|
||||
assert "Inside section" in ctx.book.sections[0].body
|
||||
@@ -0,0 +1,243 @@
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from epubmaker.models import BuildContext, Book, Section, Image, Name
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ctx(tmp_path):
|
||||
context = BuildContext()
|
||||
context.book.title = "Test Book"
|
||||
context.book.uuid = "test-uuid-1234"
|
||||
context.book.isbn = "978-0-000-00000-0"
|
||||
context.book.publisher = "Test Publisher"
|
||||
context.book.copyright = "2024 Test"
|
||||
context.book.subjects = "Fiction"
|
||||
context.book.source = "Original Source"
|
||||
context.book.publish_date = "2024-1-1"
|
||||
context.book.directory = str(tmp_path / "testbook")
|
||||
context.book.filename = str(tmp_path / "source" / "book.html")
|
||||
context.book.author.append(Name(first="Jane", last="Doe"))
|
||||
return context
|
||||
|
||||
|
||||
from epubmaker.writer import (
|
||||
write_structure, write_misc, write_ncx, write_toc,
|
||||
write_cover, write_contents, write_sections, write_illustrations,
|
||||
)
|
||||
|
||||
|
||||
# --- write_structure ---
|
||||
|
||||
def test_write_structure_creates_epub_directories(ctx):
|
||||
write_structure(ctx, overwrite=False)
|
||||
for subdir in ["META-INF", "OEBPS", "OEBPS/Images", "OEBPS/Styles", "OEBPS/Text"]:
|
||||
assert os.path.isdir(os.path.join(ctx.book.directory, subdir))
|
||||
|
||||
|
||||
def test_write_structure_exits_if_exists(ctx):
|
||||
os.makedirs(ctx.book.directory)
|
||||
with pytest.raises(SystemExit):
|
||||
write_structure(ctx, overwrite=False)
|
||||
|
||||
|
||||
def test_write_structure_overwrite_replaces_existing(ctx):
|
||||
os.makedirs(ctx.book.directory)
|
||||
sentinel = os.path.join(ctx.book.directory, "old_file.txt")
|
||||
open(sentinel, "w").close()
|
||||
write_structure(ctx, overwrite=True)
|
||||
assert os.path.isdir(ctx.book.directory)
|
||||
assert not os.path.exists(sentinel)
|
||||
|
||||
|
||||
# --- write_misc ---
|
||||
|
||||
def test_write_misc_mimetype_content(ctx):
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_misc(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "mimetype")) as f:
|
||||
assert f.read() == "application/epub+zip"
|
||||
|
||||
|
||||
def test_write_misc_container_xml_references_opf(ctx):
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_misc(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "META-INF", "container.xml")) as f:
|
||||
assert 'full-path="OEBPS/content.opf"' in f.read()
|
||||
|
||||
|
||||
def test_write_misc_ibooks_display_options_created(ctx):
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_misc(ctx)
|
||||
path = os.path.join(ctx.book.directory, "META-INF", "com.apple.ibooks.display-options.xml")
|
||||
assert os.path.isfile(path)
|
||||
|
||||
|
||||
# --- write_ncx ---
|
||||
|
||||
def test_write_ncx_contains_title_and_uuid(ctx):
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_ncx(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
|
||||
content = f.read()
|
||||
assert ctx.book.title in content
|
||||
assert ctx.book.uuid in content
|
||||
|
||||
|
||||
def test_write_ncx_includes_non_toc_sections(ctx):
|
||||
ctx.book.sections.append(Section(type="text", name="chapter1", title="Chapter One"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_ncx(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
|
||||
content = f.read()
|
||||
assert "chapter1" in content
|
||||
assert "Chapter One" in content
|
||||
|
||||
|
||||
def test_write_ncx_excludes_toc_type_sections(ctx):
|
||||
ctx.book.sections.append(Section(type="toc", name="toc", title="Table of Contents"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_ncx(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
|
||||
content = f.read()
|
||||
assert 'id ="toc"' not in content
|
||||
|
||||
|
||||
def test_write_ncx_play_order_increments(ctx):
|
||||
for i in range(3):
|
||||
ctx.book.sections.append(Section(type="text", name=f"ch{i}", title=f"Chapter {i}"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_ncx(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.ncx")) as f:
|
||||
content = f.read()
|
||||
assert 'playOrder="2"' in content
|
||||
assert 'playOrder="3"' in content
|
||||
assert 'playOrder="4"' in content
|
||||
|
||||
|
||||
# --- write_toc ---
|
||||
|
||||
def test_write_toc_writes_section_body(ctx):
|
||||
section = Section(body="<p>Contents here</p>\n")
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_toc(ctx, section)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "toc.xhtml")) as f:
|
||||
content = f.read()
|
||||
assert "<p>Contents here</p>" in content
|
||||
assert "Table of Contents" in content
|
||||
|
||||
|
||||
# --- write_cover ---
|
||||
|
||||
def test_write_cover_creates_xhtml_with_title(ctx, tmp_path):
|
||||
source_images = tmp_path / "source" / "Images"
|
||||
source_images.mkdir(parents=True)
|
||||
(source_images / "cover.jpeg").write_bytes(b"fake")
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_cover(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "Text", "cover.xhtml")) as f:
|
||||
content = f.read()
|
||||
assert ctx.book.title in content
|
||||
assert 'src="../Images/cover.jpeg"' in content
|
||||
|
||||
|
||||
def test_write_cover_copies_image_to_output(ctx, tmp_path):
|
||||
source_images = tmp_path / "source" / "Images"
|
||||
source_images.mkdir(parents=True)
|
||||
(source_images / "cover.jpeg").write_bytes(b"image-data")
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_cover(ctx)
|
||||
assert os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Images", "cover.jpeg"))
|
||||
|
||||
|
||||
# --- write_sections ---
|
||||
|
||||
def test_write_sections_creates_xhtml_per_section(ctx):
|
||||
ctx.book.sections.append(Section(type="text", name="chapter1", title="Chapter One", body="<p>Content</p>\n"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_sections(ctx)
|
||||
path = os.path.join(ctx.book.directory, "OEBPS", "Text", "chapter1.xhtml")
|
||||
assert os.path.isfile(path)
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
assert "Chapter One" in content
|
||||
assert "<p>Content</p>" in content
|
||||
|
||||
|
||||
def test_write_sections_routes_toc_to_toc_xhtml(ctx):
|
||||
ctx.book.sections.append(Section(type="toc", name="toc", title="Table of Contents", body="<p>toc</p>\n"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_sections(ctx)
|
||||
assert os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "toc.xhtml"))
|
||||
assert not os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Text", "toc.xhtml"))
|
||||
|
||||
|
||||
def test_write_sections_multiple_sections(ctx):
|
||||
for i in range(3):
|
||||
ctx.book.sections.append(Section(type="text", name=f"ch{i}", title=f"Chapter {i}", body=f"<p>ch{i}</p>\n"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_sections(ctx)
|
||||
for i in range(3):
|
||||
assert os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Text", f"ch{i}.xhtml"))
|
||||
|
||||
|
||||
# --- write_illustrations ---
|
||||
|
||||
def test_write_illustrations_skips_when_no_images(ctx):
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_illustrations(ctx)
|
||||
assert not os.path.isfile(os.path.join(ctx.book.directory, "OEBPS", "Text", "illustrations.xhtml"))
|
||||
|
||||
|
||||
def test_write_illustrations_creates_file_with_image_links(ctx):
|
||||
ctx.book.images.append(Image(name="photo.jpg", caption="A scenic photo"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_illustrations(ctx)
|
||||
path = os.path.join(ctx.book.directory, "OEBPS", "Text", "illustrations.xhtml")
|
||||
assert os.path.isfile(path)
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
assert "photo.jpg" in content
|
||||
assert "A scenic photo" in content
|
||||
|
||||
|
||||
# --- write_contents ---
|
||||
|
||||
def test_write_contents_creates_opf_with_metadata(ctx):
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_contents(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
|
||||
content = f.read()
|
||||
assert ctx.book.title in content
|
||||
assert ctx.book.publisher in content
|
||||
assert ctx.book.isbn in content
|
||||
|
||||
|
||||
def test_write_contents_includes_section_in_manifest(ctx):
|
||||
ctx.book.sections.append(Section(type="text", name="chapter1", title="Chapter One"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_contents(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
|
||||
content = f.read()
|
||||
assert "chapter1.xhtml" in content
|
||||
|
||||
|
||||
def test_write_contents_includes_series_metadata(ctx):
|
||||
ctx.book.series.name = "My Series"
|
||||
ctx.book.series.number = "1"
|
||||
write_structure(ctx, overwrite=False)
|
||||
write_contents(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
|
||||
content = f.read()
|
||||
assert "My Series" in content
|
||||
assert "calibre:series" in content
|
||||
|
||||
|
||||
def test_write_contents_includes_illustrations_when_images_present(ctx):
|
||||
ctx.book.images.append(Image(name="photo.jpg", type="jpeg", caption="caption"))
|
||||
write_structure(ctx, overwrite=False)
|
||||
with patch("epubmaker.writer.copy"):
|
||||
write_contents(ctx)
|
||||
with open(os.path.join(ctx.book.directory, "OEBPS", "content.opf")) as f:
|
||||
content = f.read()
|
||||
assert "illustrations.xhtml" in content
|
||||
Reference in New Issue
Block a user