Compare commits
3 Commits
8ff23b8d17
...
314341ff9e
Author | SHA1 | Date |
---|---|---|
Dmytro Yeroshkin | 314341ff9e | |
Dmytro Yeroshkin | bdb2d54159 | |
Dmytro Yeroshkin | c7bb77db7d |
2
LICENSE
2
LICENSE
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) <year> <owner>. All rights reserved.
|
Copyright (c) 2022 Dmytro Yeroshkin. All rights reserved.
|
||||||
|
|
||||||
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
|
||||||
|
|
||||||
|
|
23
README.md
23
README.md
|
@ -1,2 +1,25 @@
|
||||||
# novel-compiler
|
# novel-compiler
|
||||||
|
|
||||||
|
This is a simple python script that compiles a novel written in markdown into a Microsoft Word `.docx` file.
|
||||||
|
|
||||||
|
Dependencies: `docxtpl`, `markdown`, `MarkdownPP`, `novel_stats`. The first three can be installed using `pip`, and the last one is available at [https://projects.torsion.org/witten/novel-stats]().
|
||||||
|
|
||||||
|
Formatting:
|
||||||
|
```md
|
||||||
|
# PROJECT TITLE
|
||||||
|
### AUTHOR NAME
|
||||||
|
|
||||||
|
## CHAPTER HEADING
|
||||||
|
```
|
||||||
|
|
||||||
|
Currently the only formatting supported is italics and bold (`* and **`).
|
||||||
|
|
||||||
|
A sample format is included in the distribution.
|
||||||
|
|
||||||
|
## Supported arguments
|
||||||
|
|
||||||
|
* `-o [output.docx]` or `--output [output.docx]` - the file to which the formatted novel is to be written
|
||||||
|
* `-t [template.docx]` or `--template [template.docx]` - provide a template file
|
||||||
|
* `-s [settings.json]` or `--settings [settings.json]` - provide a file with information to be filled into the template
|
||||||
|
* `-pp` - use a preprocessor to enable multi-file novels
|
||||||
|
* `[markdown_file]` - name of the novel source file
|
||||||
|
|
|
@ -0,0 +1,146 @@
|
||||||
|
import argparse
|
||||||
|
from docxtpl import DocxTemplate, RichText
|
||||||
|
import markdown
|
||||||
|
from novel_stats.novel_stats import count_words
|
||||||
|
import tempfile
|
||||||
|
import MarkdownPP
|
||||||
|
import json
|
||||||
|
|
||||||
|
TITLE_MARKER = '# '
|
||||||
|
AUTHOR_MARKER = '### '
|
||||||
|
CHAPTER_MARKER = '## '
|
||||||
|
STATUS_MARKER = '[status]: # '
|
||||||
|
ACT_MARKER = '[act]: # '
|
||||||
|
COMMENT_MARKER = '[//]: # ' # Strandard markdown comment marker, supported by pandoc and calibre's ebook-convert
|
||||||
|
|
||||||
|
class Chapter:
|
||||||
|
def __init__(self, heading):
|
||||||
|
self.heading = heading
|
||||||
|
self.paragraphs = []
|
||||||
|
|
||||||
|
def md_re_parser(md_paragraph):
|
||||||
|
# Correct xml tags
|
||||||
|
pre = '<w:r><w:t xml:space="preserve">'
|
||||||
|
post = '</w:t></w:r>'
|
||||||
|
it_pre = '<w:r><w:rPr><w:i/></w:rPr><w:t xml:space="preserve">'
|
||||||
|
bf_pre = '<w:r><w:rPr><w:b/></w:rPr><w:t xml:space="preserve">'
|
||||||
|
bfit_pre = '<w:r><w:rPr><w:b/><w:i/></w:rPr><w:t xml:space="preserve">'
|
||||||
|
|
||||||
|
# Tag replacement
|
||||||
|
html = markdown.markdown(md_paragraph)
|
||||||
|
html = html.replace('<p>', pre)
|
||||||
|
html = html.replace('</p>', post)
|
||||||
|
html = html.replace('<strong><em>', post+bfit_pre)
|
||||||
|
html = html.replace('</strong></em>', post+pre)
|
||||||
|
html = html.replace('<em>', post+it_pre)
|
||||||
|
html = html.replace('</em>', post+pre)
|
||||||
|
html = html.replace('<strong>', post+bf_pre)
|
||||||
|
html = html.replace('</strong>', post+pre)
|
||||||
|
|
||||||
|
# xml cleanup
|
||||||
|
while pre+post in html:
|
||||||
|
html = html.replace(pre+post,'')
|
||||||
|
|
||||||
|
# convert to a rich text paragraph
|
||||||
|
par = RichText()
|
||||||
|
par.xml = html
|
||||||
|
if len(html) == 0:
|
||||||
|
print(md_paragraph)
|
||||||
|
return par
|
||||||
|
|
||||||
|
def novel_parser(source_file, context = None):
|
||||||
|
if not context:
|
||||||
|
context = {'author_address': 'Street\nTown, State ZIP\nCountry',
|
||||||
|
'author_email': 'name@email.com',
|
||||||
|
'author_phone': 'PhoneNumber(s)',
|
||||||
|
'author_website': 'https://www.author.com'}
|
||||||
|
context['chapters'] = []
|
||||||
|
|
||||||
|
wc = 0
|
||||||
|
chapter = Chapter('')
|
||||||
|
for line in source_file:
|
||||||
|
if line.startswith(TITLE_MARKER):
|
||||||
|
title = line[len(TITLE_MARKER):].strip('()\n')
|
||||||
|
context['project_title'] = title
|
||||||
|
wc += count_words(title)
|
||||||
|
elif line.startswith(AUTHOR_MARKER):
|
||||||
|
author_name = line[len(AUTHOR_MARKER):].strip('()\n')
|
||||||
|
if 'author_name' not in context:
|
||||||
|
context['author_name'] = author_name
|
||||||
|
context['penname'] = author_name
|
||||||
|
elif line.startswith(CHAPTER_MARKER):
|
||||||
|
if chapter.heading or chapter.paragraphs:
|
||||||
|
context['chapters'].append(chapter)
|
||||||
|
chapter = Chapter(line[len(CHAPTER_MARKER):].strip('()\n'))
|
||||||
|
wc += count_words(chapter.heading)
|
||||||
|
elif line.startswith(STATUS_MARKER) or line.startswith(ACT_MARKER) or line.startswith(COMMENT_MARKER):
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped:
|
||||||
|
wc += count_words(stripped)
|
||||||
|
chapter.paragraphs.append(md_re_parser(stripped))
|
||||||
|
context['chapters'].append(chapter)
|
||||||
|
|
||||||
|
source_file.close()
|
||||||
|
|
||||||
|
context['wc_1000'] = f'{(wc//1000)*1000:,}'
|
||||||
|
|
||||||
|
if 'header' not in context:
|
||||||
|
context['header'] = context['author_name'].split()[-1] + ' - ' + context['project_title'] + ' - '
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument(
|
||||||
|
'markdown_file',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
help='The markdown file for the novel, main file if a multi-file novel',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'-pp',
|
||||||
|
action='store_true',
|
||||||
|
help='run markdown pre-processor, this allows for a multi-file input (e.g. each chapter in its own file), but requires the MarkdownPP python library',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--settings',
|
||||||
|
'-s',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
help='setting json file',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--template',
|
||||||
|
'-t',
|
||||||
|
type=argparse.FileType('r'),
|
||||||
|
help='template docx file',
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output',
|
||||||
|
'-o',
|
||||||
|
type=argparse.FileType('w'),
|
||||||
|
help='output docx file',
|
||||||
|
)
|
||||||
|
arguments = parser.parse_args()
|
||||||
|
|
||||||
|
arguments.template.close()
|
||||||
|
doc = DocxTemplate(arguments.template.name)
|
||||||
|
if arguments.pp:
|
||||||
|
mdfile = tempfile.TemporaryFile(mode='w+')
|
||||||
|
MarkdownPP.MarkdownPP(
|
||||||
|
input=arguments.markdown_file, output=mdfile, modules=list(MarkdownPP.modules)
|
||||||
|
)
|
||||||
|
mdfile.seek(0)
|
||||||
|
else:
|
||||||
|
mdfile = arguments.markdown_file
|
||||||
|
if arguments.settings:
|
||||||
|
context = json.load(arguments.settings)
|
||||||
|
else:
|
||||||
|
context = None
|
||||||
|
context = novel_parser(mdfile, context = context)
|
||||||
|
doc.render(context)
|
||||||
|
arguments.output.close()
|
||||||
|
doc.save(arguments.output.name)
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"author_address": "Address",
|
||||||
|
"author_email": "Email",
|
||||||
|
"author_phone": "Phone number",
|
||||||
|
"author_website": "Website"
|
||||||
|
}
|
Binary file not shown.
|
@ -0,0 +1,26 @@
|
||||||
|
from setuptools import find_packages, setup
|
||||||
|
|
||||||
|
VERSION = "0.1.0"
|
||||||
|
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="novel-compiler",
|
||||||
|
version=VERSION,
|
||||||
|
description="Convert a markdown novel to a docx file in a standard style for sharing or submission.",
|
||||||
|
author="Dmytro Yeroshkin",
|
||||||
|
author_email="dmytro.yeroshkin@gmail.com",
|
||||||
|
url="https://projects.torsion.org/",
|
||||||
|
classifiers=[
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Environment :: Console",
|
||||||
|
"Intended Audience :: Other Audience",
|
||||||
|
"License :: OSI Approved :: BSD License",
|
||||||
|
"Programming Language :: Python",
|
||||||
|
"Topic :: Office/Business",
|
||||||
|
],
|
||||||
|
packages=find_packages(),
|
||||||
|
entry_points={"console_scripts": ["novel-compiler = novel_compiler.novel_compiler:main",]},
|
||||||
|
install_requires=('docxtpl','markdown','novel_stats','MarkdownPP'),
|
||||||
|
include_package_data=True,
|
||||||
|
python_requires='>3.7.0',
|
||||||
|
)
|
Loading…
Reference in New Issue