diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eb7c86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,187 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Venv stuff +bin/ +include/ +pyvenv.cfg + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +lib64 +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +out/ +*.epub + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +.vscode/ + +# PyPI configuration file +.pypirc \ No newline at end of file diff --git a/README.md b/README.md index 288c0a0..b5745ec 100755 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ mark2epub requires: - Python (>= 3.4) - markdown (>= 3.1) +- BeautifulSoup4 (>= 4.14.3) +- lxml (>= 6.0.2) ### Running mark2epub @@ -18,22 +20,24 @@ The syntax for mark2epub is the following: $ python md2epub.py +The output file path can contain unexisting directories, as the code will handle their creation. + The directory `epub_md` is a sample markdown directory for mark2epub. Note that the directory `markdown_directory` **must** contain -* Markdown `.md` files. Each file represent a chapter in the resulting ePub. -They are processed by name order, and will appear correspondingly in the e-book. +- Markdown `.md` files. Each file represent a chapter in the resulting ePub. + They are processed by name order, and will appear correspondingly in the e-book. -* An `images` folder, containing the images to be included. Only GIF (`.gif` +- An `images` folder, containing the images to be included. Only GIF (`.gif` extension), JPEG (`.jpg` or `.jpeg` extensions), and PNG (`.png` extension) - files are currently supported. This folder is *not* processed recursively, so + files are currently supported. This folder is _not_ processed recursively, so all images should be placed at the root of this folder. -* A `css` folder, containing the CSS files. This folder is *not* processed - recursively, so all css files should be placed at the root of this folder. +- A `css` folder, containing the CSS files. This folder is _not_ processed + recursively, so all css files should be placed at the root of this folder. -* A `description.json` containing meta-information about the e-book. The key +- A `description.json` containing meta-information about the e-book. The key `cover_image` should indicate the name of the cover image. The key `default_css` is a list of css file names that are applied by default on all chapters. @@ -42,9 +46,41 @@ They are processed by name order, and will appear correspondingly in the e-book. the name of the css file that should be applied specifically to this chapter. See the example in the repository for a typical `description.json` file. +## Specific class names for elements + +After your text, on every line (at the end of it) you can add class names to that element, as a list separated by whitespaces, included in curly braces, as follows: + +```markdown +# Chapter 1 {name1 name-2 names-of-names etc} +``` + +Class names can consist of latin letters, digits, hyphens and underscores. + +After compiling the braces and their insides will be removed. + +The code won't handle any typos in class names, so it's up to you to handle it. + +## Obsidian tags + +You can safely use obsidian tags in your markdown files, as the preprocessor will handle their exclusion from the final built epub. + +You can also specify which style of new line you use (before the tag, after it, or without any newline before or after the tag) in the description.json, `tag_new_line_style` field. This new line will be excluded along with the tag. + +The valid values are: `before`, `after` and `zero`, respectively, in upper, lower or mixed case (you do you!). + +## No cover + +To not set any cover, simply leave the `"cover_image": ""`. + ## Limitations/Features to be addressed -* Robustness checks in the `mark2epub.py` script -* Recursive processing of the `images` and `css` folders -* Support for additional fonts -* Support for mathematical notation +- Robustness checks in the `mark2epub.py` script +- Recursive processing of the `images` and `css` folders +- Support for additional fonts +- Support for mathematical notation + +## TODO + +- [ ] Error handling for class names parsing +- [x] Add support for symlinks +- [x] Add support for obsidian tags diff --git a/build.sh b/build.sh new file mode 100755 index 0000000..f129e35 --- /dev/null +++ b/build.sh @@ -0,0 +1,5 @@ +#! /usr/bin/bash + +source ./bin/activate +pip install -r requirements.txt +pyinstaller --clean -F -y -n "mark2epub" mark2epub.py \ No newline at end of file diff --git a/epub_md/chapter1.md b/epub_md/chapter1.md index bf14bb2..ae23b6c 100755 --- a/epub_md/chapter1.md +++ b/epub_md/chapter1.md @@ -1,8 +1,10 @@ -# Chapter 1 +# Chapter 1 {example of multiple class names} -This is some simple text in Markdown. +This is some simple text in Markdown. {WORKS ON EVERY TAG ALLEGEDLY THO} -## Section 1 +#example_of_an_obsidian_tag #multiple_tags + +## Section 1 {Allows only whitelisted characters google it} Denique Antiochensis **ordinis vertices sub uno elogio iussit occidi** ideo efferatus, quod ei celebrari vilitatem intempestivam urgenti, cum inpenderet inopia, @@ -11,7 +13,7 @@ Honoratus fixa constantia restitisset. ## Section 2 -Et interdum acciderat, *ut siquid in penetrali secreto nullo citerioris vitae* +Et interdum acciderat, _ut siquid in penetrali secreto nullo citerioris vitae_ ministro praesente paterfamilias uxori susurrasset in aurem, velut Amphiarao referente aut Marcio, quondam vatibus inclitis, postridie disceret imperator. ideoque etiam parietes arcanorum soli conscii timebantur. @@ -20,14 +22,14 @@ ideoque etiam parietes arcanorum soli conscii timebantur. Saepissime igitur mihi de amicitia cogitanti maxime illud considerandum videri solet, utrum propter imbecillitatem atque inopiam desiderata sit amicitia, -ut dandis recipiendisque meritis +ut dandis recipiendisque meritis -* quod quisque minus per se ipse posset, - * id acciperet -* ab alio vicissimque redderet, - * an esset hoc quidem proprium amicitiae, - *sed antiquior et - * pulchrior et magis a natura ipsa profecta alia causa. +- quod quisque minus per se ipse posset, + - id acciperet +- ab alio vicissimque redderet, + - an esset hoc quidem proprium amicitiae, + \*sed antiquior et + - pulchrior et magis a natura ipsa profecta alia causa. ### Subsection @@ -35,6 +37,6 @@ Amor enim, ex quo amicitia nominata est, princeps est ad benevolentiam coniungendam. 1. Nam utilitates quidem etiam ab iis percipiuntur saepe - 1. qui simulatione amicitiae coluntur - 2. et observantur temporis causa, -2. in amicitia autem nihil fictum est, nihil simulatum et, quidquid est, id est verum et voluntarium. \ No newline at end of file + 1. qui simulatione amicitiae coluntur + 2. et observantur temporis causa, +2. in amicitia autem nihil fictum est, nihil simulatum et, quidquid est, id est verum et voluntarium. diff --git a/epub_md/css/general.css b/epub_md/css/general.css index d3f2200..bfe33b4 100755 --- a/epub_md/css/general.css +++ b/epub_md/css/general.css @@ -1,26 +1,31 @@ body { - font-size: 0.8em; + font-size: 0.8em; } +.example { + color: red; +} img { - max-width: 100%; - height: auto; + max-width: 100%; + height: auto; } - table { border-collapse: collapse; width: 100%; font-size: 0.5em; } -td,th { +td, +th { border: 1px solid #ddd; padding: 8px; } -tr:nth-child(even){background-color: #f2f2f2;} +tr:nth-child(even) { + background-color: #f2f2f2; +} th { padding-top: 12px; @@ -29,4 +34,4 @@ th { background-color: #444444; color: white; font-size: 0.8em; -} \ No newline at end of file +} diff --git a/epub_md/description.json b/epub_md/description.json index 7057383..1109cf8 100755 --- a/epub_md/description.json +++ b/epub_md/description.json @@ -1,24 +1,25 @@ { -"metadata":{ - "dc:title":"Mark2Epub Sample", - "dc:creator":"Mark2Epub", - "dc:language":"en-US", - "dc:identifier":"mark2epub-sample", - "dc:source":"", - "meta":"", - "dc:date":"2023-01-01", - "dc:publisher":"", - "dc:contributor":"", - "dc:rights":"", - "dc:description":"", - "dc:subject":"" - }, -"cover_image":"cover.jpg", -"default_css":["code_styles.css","general.css"], -"chapters":[ - {"markdown":"chapter1.md","css":""}, - {"markdown":"chapter2.md","css":""}, - {"markdown":"chapter3.md","css":""}, - {"markdown":"chapter4.md","css":"specific.css"} - ] -} \ No newline at end of file + "metadata": { + "dc:title": "Mark2Epub Sample", + "dc:creator": "Mark2Epub", + "dc:language": "en-US", + "dc:identifier": "mark2epub-sample", + "dc:source": "", + "meta": "", + "dc:date": "2023-01-01", + "dc:publisher": "", + "dc:contributor": "", + "dc:rights": "", + "dc:description": "", + "dc:subject": "" + }, + "tag_new_line_style": "after", + "cover_image": "cover.jpg", + "default_css": ["code_styles.css", "general.css"], + "chapters": [ + { "markdown": "chapter1.md", "css": "" }, + { "markdown": "chapter2.md", "css": "" }, + { "markdown": "chapter3.md", "css": "" }, + { "markdown": "chapter4.md", "css": "specific.css" } + ] +} diff --git a/file_preprocessor.py b/file_preprocessor.py new file mode 100644 index 0000000..5bfab9b --- /dev/null +++ b/file_preprocessor.py @@ -0,0 +1,96 @@ +""" +Docstring for file_preprocessor + +Author: Artiom Guțan + +This is a module to be used before parsing an md file, to eliminate any comments/hashtags (e.g. Obsidian). +A comment, in this sense, is any line that starts with a hashtag "#" and doesn't contain any whitespaces and ends with a new-line character sequence (\n). +While using this module it is recomended to specify what blank line must be deleted along with the hashtag line itself. For this purpose the enum HTagNLStyle exists. + +HTagNLStyle.BEFORE will remove the line before the hashtag as follows: +# Example of a header +-> +->#hashtag + +Just a normal paragraph + +--- + +HTagNLStyle.AFTER, which is the default one, will remove the line after the hashtag as follows: +# Example of a header + +->#hashtag +-> +Just a normal paragraph + +--- + +HTagNLStyle.ZERO, will remove just the hashtag line: +# Example of a header + +->#hashtag + +Just a normal paragraph + +--- + +I don't really know if it's useful or not, but I implemented it anyways. + +""" + +# + +import sys +import re +from enum import Enum + + +class HTagNLStyle(Enum): + BEFORE = -1 + ZERO = 0 + AFTER = 1 + + +class FilePreprocessor: + def __init__(self, path: str, style: HTagNLStyle = HTagNLStyle.AFTER): + """ + Initiates the preprocessor using path to a file and new line style (enum) + """ + self.md_file = open(path, "r", encoding="utf-8") + self.lines = self.md_file.readlines() + self.style = style + + def delete_tags(self): + """ + Deletes obsidian style tags (check file docstring) + """ + for line in self.lines: + htag_match = re.fullmatch(r"^#\w+(?:\s+#\w+)*\n$", line) + if htag_match: + try: + if self.style.value != 0: + if self.lines.index(line) != 0: + self.lines.pop(self.lines.index(line) + self.style.value) + self.lines.remove(line) + except IndexError: + self.lines.remove(line) + return self + + def close(self): + """ + Closes the file connection + """ + self.md_file.close() + return self + + def get(self) -> str: + """ + :return: Final processed string + :rtype: str + """ + self.close() + return "".join(self.lines) + + +if __name__ == "__main__": + print(FilePreprocessor(sys.argv[1], style=HTagNLStyle.ZERO).delete_tags().get()) diff --git a/mark2epub.py b/mark2epub.py index 851ef00..89f1306 100755 --- a/mark2epub.py +++ b/mark2epub.py @@ -4,10 +4,14 @@ import zipfile import sys import json +from bs4 import BeautifulSoup +import lxml +import re +from file_preprocessor import FilePreprocessor, HTagNLStyle ## markdown version 3.1 -''' +""" import numpy as np import matplotlib.pyplot as plt @@ -22,124 +26,129 @@ plt.plot(X,Z,linewidth=5,c="black") plt.savefig("./a.png",dpi=150) plt.show() -''' +""" -def get_all_filenames(the_dir,extensions=[]): + +def get_all_filenames(the_dir, extensions=[]): all_files = [x for x in os.listdir(the_dir)] all_files = [x for x in all_files if x.split(".")[-1] in extensions] return all_files -def get_packageOPF_XML(md_filenames=[],image_filenames=[],css_filenames=[],description_data=None): +def get_packageOPF_XML( + md_filenames=[], image_filenames=[], css_filenames=[], description_data=None +): doc = minidom.Document() - package = doc.createElement('package') - package.setAttribute('xmlns',"http://www.idpf.org/2007/opf") - package.setAttribute('version',"3.0") - package.setAttribute('xml:lang',"en") - package.setAttribute("unique-identifier","pub-id") + package = doc.createElement("package") + package.setAttribute("xmlns", "http://www.idpf.org/2007/opf") + package.setAttribute("version", "3.0") + package.setAttribute("xml:lang", "en") + package.setAttribute("unique-identifier", "pub-id") ## Now building the metadata - metadata = doc.createElement('metadata') - metadata.setAttribute('xmlns:dc', 'http://purl.org/dc/elements/1.1/') + metadata = doc.createElement("metadata") + metadata.setAttribute("xmlns:dc", "http://purl.org/dc/elements/1.1/") - for k,v in description_data["metadata"].items(): + for k, v in description_data["metadata"].items(): if len(v): x = doc.createElement(k) - for metadata_type,id_label in [("dc:title","title"),("dc:creator","creator"),("dc:identifier","book-id")]: - if k==metadata_type: - x.setAttribute('id',id_label) + for metadata_type, id_label in [ + ("dc:title", "title"), + ("dc:creator", "creator"), + ("dc:identifier", "book-id"), + ]: + if k == metadata_type: + x.setAttribute("id", id_label) x.appendChild(doc.createTextNode(v)) metadata.appendChild(x) - ## Now building the manifest - manifest = doc.createElement('manifest') + manifest = doc.createElement("manifest") ## TOC.xhtml file for EPUB 3 - x = doc.createElement('item') - x.setAttribute('id',"toc") - x.setAttribute('properties',"nav") - x.setAttribute('href',"TOC.xhtml") - x.setAttribute('media-type',"application/xhtml+xml") + x = doc.createElement("item") + x.setAttribute("id", "toc") + x.setAttribute("properties", "nav") + x.setAttribute("href", "TOC.xhtml") + x.setAttribute("media-type", "application/xhtml+xml") manifest.appendChild(x) ## Ensure retrocompatibility by also providing a TOC.ncx file - x = doc.createElement('item') - x.setAttribute('id',"ncx") - x.setAttribute('href',"toc.ncx") - x.setAttribute('media-type',"application/x-dtbncx+xml") + x = doc.createElement("item") + x.setAttribute("id", "ncx") + x.setAttribute("href", "toc.ncx") + x.setAttribute("media-type", "application/x-dtbncx+xml") manifest.appendChild(x) - x = doc.createElement('item') - x.setAttribute('id',"titlepage") - x.setAttribute('href',"titlepage.xhtml") - x.setAttribute('media-type',"application/xhtml+xml") + x = doc.createElement("item") + x.setAttribute("id", "titlepage") + x.setAttribute("href", "titlepage.xhtml") + x.setAttribute("media-type", "application/xhtml+xml") manifest.appendChild(x) - for i,md_filename in enumerate(md_filenames): - x = doc.createElement('item') - x.setAttribute('id',"s{:05d}".format(i)) - x.setAttribute('href',"s{:05d}-{}.xhtml".format(i,md_filename.split(".")[0])) - x.setAttribute('media-type',"application/xhtml+xml") + for i, md_filename in enumerate(md_filenames): + x = doc.createElement("item") + x.setAttribute("id", "s{:05d}".format(i)) + x.setAttribute("href", "s{:05d}-{}.xhtml".format(i, md_filename.split(".")[0])) + x.setAttribute("media-type", "application/xhtml+xml") manifest.appendChild(x) - for i,image_filename in enumerate(image_filenames): - x = doc.createElement('item') - x.setAttribute('id',"image-{:05d}".format(i)) - x.setAttribute('href',"images/{}".format(image_filename)) + for i, image_filename in enumerate(image_filenames): + x = doc.createElement("item") + x.setAttribute("id", "image-{:05d}".format(i)) + x.setAttribute("href", "images/{}".format(image_filename)) if "gif" in image_filename: - x.setAttribute('media-type',"image/gif") + x.setAttribute("media-type", "image/gif") elif "jpg" in image_filename: - x.setAttribute('media-type',"image/jpeg") + x.setAttribute("media-type", "image/jpeg") elif "jpeg" in image_filename: - x.setAttribute('media-type',"image/jpg") + x.setAttribute("media-type", "image/jpg") elif "png" in image_filename: - x.setAttribute('media-type',"image/png") - if image_filename==description_data["cover_image"]: - x.setAttribute('properties',"cover-image") + x.setAttribute("media-type", "image/png") + if image_filename == description_data["cover_image"]: + x.setAttribute("properties", "cover-image") ## Ensure compatibility by also providing a meta tag in the metadata - y = doc.createElement('meta') - y.setAttribute('name',"cover") - y.setAttribute('content',"image-{:05d}".format(i)) + y = doc.createElement("meta") + y.setAttribute("name", "cover") + y.setAttribute("content", "image-{:05d}".format(i)) metadata.appendChild(y) manifest.appendChild(x) - for i,css_filename in enumerate(css_filenames): - x = doc.createElement('item') - x.setAttribute('id',"css-{:05d}".format(i)) - x.setAttribute('href',"css/{}".format(css_filename)) - x.setAttribute('media-type',"text/css") + for i, css_filename in enumerate(css_filenames): + x = doc.createElement("item") + x.setAttribute("id", "css-{:05d}".format(i)) + x.setAttribute("href", "css/{}".format(css_filename)) + x.setAttribute("media-type", "text/css") manifest.appendChild(x) ## Now building the spine - spine = doc.createElement('spine') - spine.setAttribute('toc', "ncx") + spine = doc.createElement("spine") + spine.setAttribute("toc", "ncx") - x = doc.createElement('itemref') - x.setAttribute('idref',"titlepage") - x.setAttribute('linear',"yes") + x = doc.createElement("itemref") + x.setAttribute("idref", "titlepage") + x.setAttribute("linear", "yes") spine.appendChild(x) - for i,md_filename in enumerate(all_md_filenames): - x = doc.createElement('itemref') - x.setAttribute('idref',"s{:05d}".format(i)) - x.setAttribute('linear',"yes") + for i, md_filename in enumerate(all_md_filenames): + x = doc.createElement("itemref") + x.setAttribute("idref", "s{:05d}".format(i)) + x.setAttribute("linear", "yes") spine.appendChild(x) - guide = doc.createElement('guide') - x = doc.createElement('reference') - x.setAttribute('type',"cover") - x.setAttribute('title',"Cover image") - x.setAttribute('href',"titlepage.xhtml") + guide = doc.createElement("guide") + x = doc.createElement("reference") + x.setAttribute("type", "cover") + x.setAttribute("title", "Cover image") + x.setAttribute("href", "titlepage.xhtml") guide.appendChild(x) - package.appendChild(metadata) package.appendChild(manifest) package.appendChild(spine) @@ -165,12 +174,17 @@ def get_coverpage_XML(cover_image_path): all_xhtml = """\n""" all_xhtml += """\n""" all_xhtml += """\n\n\n""" - all_xhtml += """\n""".format(cover_image_path) + all_xhtml += ( + """\n""".format( + cover_image_path + ) + ) all_xhtml += """\n""" return all_xhtml -def get_TOC_XML(default_css_filenames,markdown_filenames): + +def get_TOC_XML(default_css_filenames, markdown_filenames): ## Returns the XML data for the TOC.xhtml file toc_xhtml = """\n""" @@ -179,16 +193,23 @@ def get_TOC_XML(default_css_filenames,markdown_filenames): toc_xhtml += """Contents\n""" for css_filename in default_css_filenames: - toc_xhtml += """\n""".format(css_filename) + toc_xhtml += ( + """\n""".format( + css_filename + ) + ) toc_xhtml += """\n\n""" toc_xhtml += """\n\n""" return toc_xhtml + def get_TOCNCX_XML(markdown_filenames): ## Returns the XML data for the TOC.ncx file @@ -196,32 +217,71 @@ def get_TOCNCX_XML(markdown_filenames): toc_ncx += """\n""" toc_ncx += """\n\n""" toc_ncx += """\n""" - for i,md_filename in enumerate(markdown_filenames): + for i, md_filename in enumerate(markdown_filenames): toc_ncx += """\n""".format(i) - toc_ncx += """\n{}\n""".format(md_filename.split(".")[0]) - toc_ncx += """""".format(i,md_filename.split(".")[0]) + toc_ncx += """\n{}\n""".format( + md_filename.split(".")[0] + ) + toc_ncx += """""".format( + i, md_filename.split(".")[0] + ) toc_ncx += """ """ toc_ncx += """\n""" return toc_ncx -def get_chapter_XML(md_filename,css_filenames): + +def parse_classes(element): + el_content = element.string + if el_content == "\n" or el_content == None: + return + return re.findall(r"\s*{[a-zA-Z0-9\s\-\_]+}\s*", el_content) + + +def get_chapter_XML(md_filename, css_filenames, tag_new_line_style="AFTER"): ## Returns the XML data for a given markdown chapter file, with the corresponding css chapter files - with open(os.path.join(work_dir,md_filename),"r",encoding="utf-8") as f: - markdown_data = f.read() - html_text = markdown.markdown(markdown_data, - extensions=["codehilite","tables","fenced_code","footnotes"], - extension_configs={"codehilite":{"guess_lang":False}} - ) + # Setup path of the md file + file_path = os.path.join(work_dir, md_filename) + + # Check if the said file is symlink, and if so - reassign the file_path with the absolute path of the symlink target + if os.path.islink(file_path): + file_path = os.path.abspath(os.readlink(os.path.join(work_dir, md_filename))) + + markdown_data = ( + FilePreprocessor(file_path, HTagNLStyle[tag_new_line_style.upper()]) + .delete_tags() + .get() + ) + html_text = markdown.markdown( + markdown_data, + extensions=["codehilite", "tables", "fenced_code", "footnotes"], + extension_configs={"codehilite": {"guess_lang": False}}, + ) + + soup = BeautifulSoup(html_text, features="lxml").body + + for tag in soup.contents: + classes = parse_classes(tag) + if classes: + formated_str_of_classes = re.sub(r"\s*{", "", classes[0]) + formated_str_of_classes = re.sub(r"}\s*", "", formated_str_of_classes) + intermediary = re.sub(r"\s*{.+}\s*", "", tag.string) + tag.string = intermediary + tag["class"] = formated_str_of_classes + + html_text = str(soup) all_xhtml = """\n""" all_xhtml += """\n""" all_xhtml += """\n\n""" - for css_filename in css_filenames: - all_xhtml += """\n""".format(css_filename) + all_xhtml += ( + """\n""".format( + css_filename + ) + ) all_xhtml += """\n\n""" @@ -230,92 +290,118 @@ def get_chapter_XML(md_filename,css_filenames): return all_xhtml + if __name__ == "__main__": - if len(sys.argv[1:])<2: + if len(sys.argv[1:]) < 2: print("\nUsage:\n python md2epub.py ") - exit(1) - + sys.exit(1) work_dir = sys.argv[1] output_path = sys.argv[2] - images_dir = os.path.join(work_dir,r'images/') - css_dir = os.path.join(work_dir,r'css/') + images_dir = os.path.join(work_dir, r"images/") + css_dir = os.path.join(work_dir, r"css/") ## Reading the JSON file containing the description of the eBook ## and compiling the list of relevant Markdown, CSS, and image files - with open(os.path.join(work_dir,"description.json"),"r") as f: + with open(os.path.join(work_dir, "description.json"), "r") as f: json_data = json.load(f) - all_md_filenames=[] - all_css_filenames=json_data["default_css"][:] + all_md_filenames = [] + all_css_filenames = json_data["default_css"][:] for chapter in json_data["chapters"]: if not chapter["markdown"] in all_md_filenames: all_md_filenames.append(chapter["markdown"]) if len(chapter["css"]) and (not chapter["css"] in all_css_filenames): all_css_filenames.append(chapter["css"]) - all_image_filenames = get_all_filenames(images_dir,extensions=["gif","jpg","jpeg","png"]) + all_image_filenames = get_all_filenames( + images_dir, extensions=["gif", "jpg", "jpeg", "png"] + ) ###################################################### ## Now creating the ePUB book - with zipfile.ZipFile(output_path, "w" ) as myZipFile: + try: + os.makedirs(os.path.dirname(output_path), exist_ok=True) + except FileNotFoundError: + pass + + with zipfile.ZipFile(output_path, "w") as myZipFile: ## First, write the mimetype - myZipFile.writestr("mimetype","application/epub+zip", zipfile.ZIP_DEFLATED ) + myZipFile.writestr("mimetype", "application/epub+zip", zipfile.ZIP_DEFLATED) ## Then, the file container.xml which just points to package.opf container_data = get_container_XML() - myZipFile.writestr("META-INF/container.xml",container_data, zipfile.ZIP_DEFLATED ) + myZipFile.writestr( + "META-INF/container.xml", container_data, zipfile.ZIP_DEFLATED + ) ## Then, the package.opf file itself - package_data = get_packageOPF_XML(md_filenames=all_md_filenames, - image_filenames=all_image_filenames, - css_filenames=all_css_filenames, - description_data=json_data - ) - myZipFile.writestr("OPS/package.opf",package_data, zipfile.ZIP_DEFLATED) + package_data = get_packageOPF_XML( + md_filenames=all_md_filenames, + image_filenames=all_image_filenames, + css_filenames=all_css_filenames, + description_data=json_data, + ) + myZipFile.writestr("OPS/package.opf", package_data, zipfile.ZIP_DEFLATED) ## First, we create the cover page - coverpage_data = get_coverpage_XML(json_data["cover_image"]) - myZipFile.writestr("OPS/titlepage.xhtml",coverpage_data.encode('utf-8'),zipfile.ZIP_DEFLATED) + if json_data["cover_image"] != "": + coverpage_data = get_coverpage_XML(json_data["cover_image"]) + myZipFile.writestr( + "OPS/titlepage.xhtml", + coverpage_data.encode("utf-8"), + zipfile.ZIP_DEFLATED, + ) ## Now, we are going to convert the Markdown files to xhtml files - for i,chapter in enumerate(json_data["chapters"]): + for i, chapter in enumerate(json_data["chapters"]): chapter_md_filename = chapter["markdown"] chapter_css_filenames = json_data["default_css"][:] if len(chapter["css"]): chapter_css_filenames.append(chapter["css"]) - chapter_data = get_chapter_XML(chapter_md_filename,chapter_css_filenames) - myZipFile.writestr("OPS/s{:05d}-{}.xhtml".format(i,chapter_md_filename.split(".")[0]), - chapter_data.encode('utf-8'), - zipfile.ZIP_DEFLATED) - + chapter_data = get_chapter_XML( + chapter_md_filename, + chapter_css_filenames, + json_data["tag_new_line_style"], + ) + myZipFile.writestr( + "OPS/s{:05d}-{}.xhtml".format(i, chapter_md_filename.split(".")[0]), + chapter_data.encode("utf-8"), + zipfile.ZIP_DEFLATED, + ) ## Writing the TOC.xhtml file - toc_xml_data = get_TOC_XML(json_data["default_css"],all_md_filenames) - myZipFile.writestr("OPS/TOC.xhtml",toc_xml_data.encode('utf-8'),zipfile.ZIP_DEFLATED) + toc_xml_data = get_TOC_XML(json_data["default_css"], all_md_filenames) + myZipFile.writestr( + "OPS/TOC.xhtml", toc_xml_data.encode("utf-8"), zipfile.ZIP_DEFLATED + ) ## Writing the TOC.ncx file toc_ncx_data = get_TOCNCX_XML(all_md_filenames) - myZipFile.writestr("OPS/toc.ncx",toc_ncx_data.encode('utf-8'),zipfile.ZIP_DEFLATED) + myZipFile.writestr( + "OPS/toc.ncx", toc_ncx_data.encode("utf-8"), zipfile.ZIP_DEFLATED + ) ## Copy image files - for i,image_filename in enumerate(all_image_filenames): - with open(os.path.join(images_dir,image_filename),"rb") as f: + for i, image_filename in enumerate(all_image_filenames): + with open(os.path.join(images_dir, image_filename), "rb") as f: filedata = f.read() - myZipFile.writestr("OPS/images/{}".format(image_filename), - filedata, - zipfile.ZIP_DEFLATED) + myZipFile.writestr( + "OPS/images/{}".format(image_filename), filedata, zipfile.ZIP_DEFLATED + ) ## Copy CSS files - for i,css_filename in enumerate(all_css_filenames): - with open(os.path.join(css_dir,css_filename),"rb") as f: + for i, css_filename in enumerate(all_css_filenames): + with open(os.path.join(css_dir, css_filename), "rb") as f: filedata = f.read() - myZipFile.writestr("OPS/css/{}".format(css_filename), - filedata, - zipfile.ZIP_DEFLATED) + myZipFile.writestr( + "OPS/css/{}".format(css_filename), + filedata, + zipfile.ZIP_DEFLATED, + ) print("eBook creation complete") diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..30b338c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +beautifulsoup4==4.14.3 +lxml==6.0.2 +Markdown==3.10 +pyinstaller==6.17.0