├── .gitignore
├── images
├── shas.jpg
├── center.jpg
├── rashi.jpg
├── four_rows.jpg
└── sample_page.png
├── fonts
├── fell_flowers
│ ├── Fell Types License.txt
│ └── IMFeFlow2.otf
├── fell_french_canon
│ ├── Fell Types License.txt
│ ├── IMFeFCit29C.otf
│ ├── IMFeFCrm29C.otf
│ └── AveriaLibre-Bold.ttf
├── rashi
│ └── Mekorot-Rashi.ttf
├── averia
│ ├── AveriaLibre-Bold.ttf
│ ├── AveriaLibre-Light.ttf
│ ├── AveriaLibre-Regular.ttf
│ ├── AveriaLibre-BoldItalic.ttf
│ ├── AveriaLibre-LightItalic.ttf
│ ├── AveriaLibre-RegularItalic.ttf
│ └── OFL.txt
├── garamond
│ ├── EBGaramond-Bold.ttf
│ ├── EBGaramond-SemiBold.ttf
│ ├── EBGaramond-BoldItalic.ttf
│ ├── static
│ │ ├── EBGaramond-Bold.ttf
│ │ ├── EBGaramond-Italic.ttf
│ │ ├── EBGaramond-Medium.ttf
│ │ ├── EBGaramond-Regular.ttf
│ │ ├── EBGaramond-SemiBold.ttf
│ │ ├── EBGaramond-BoldItalic.ttf
│ │ ├── EBGaramond-ExtraBold.ttf
│ │ ├── EBGaramond-MediumItalic.ttf
│ │ ├── EBGaramond-ExtraBoldItalic.ttf
│ │ └── EBGaramond-SemiBoldItalic.ttf
│ ├── EBGaramond-SemiBoldItalic.ttf
│ ├── README
│ └── OFL.txt
└── frankruehl
│ └── FrankRuehlCLM-Medium.ttf
├── talmudifier
├── style.py
├── header.txt
├── util.py
├── citation.py
├── pdf_reader.py
├── pdf_writer.py
├── word.py
├── paracol.py
├── column.py
├── row_maker.py
└── talmudifier.py
├── test_input_reader.py
├── LICENSE
├── setup.py
├── recipes
└── default.json
├── test
└── test_input.md
├── row_length_calculator.py
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | __pycache__/
3 | *.pyc
4 | Output/*
5 | talmudifier.egg-info/
6 |
--------------------------------------------------------------------------------
/images/shas.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/images/shas.jpg
--------------------------------------------------------------------------------
/images/center.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/images/center.jpg
--------------------------------------------------------------------------------
/images/rashi.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/images/rashi.jpg
--------------------------------------------------------------------------------
/images/four_rows.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/images/four_rows.jpg
--------------------------------------------------------------------------------
/images/sample_page.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/images/sample_page.png
--------------------------------------------------------------------------------
/fonts/fell_flowers/Fell Types License.txt:
--------------------------------------------------------------------------------
1 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
--------------------------------------------------------------------------------
/fonts/fell_french_canon/Fell Types License.txt:
--------------------------------------------------------------------------------
1 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
--------------------------------------------------------------------------------
/fonts/rashi/Mekorot-Rashi.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/rashi/Mekorot-Rashi.ttf
--------------------------------------------------------------------------------
/fonts/averia/AveriaLibre-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/averia/AveriaLibre-Bold.ttf
--------------------------------------------------------------------------------
/fonts/fell_flowers/IMFeFlow2.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/fell_flowers/IMFeFlow2.otf
--------------------------------------------------------------------------------
/fonts/averia/AveriaLibre-Light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/averia/AveriaLibre-Light.ttf
--------------------------------------------------------------------------------
/fonts/garamond/EBGaramond-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/EBGaramond-Bold.ttf
--------------------------------------------------------------------------------
/fonts/averia/AveriaLibre-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/averia/AveriaLibre-Regular.ttf
--------------------------------------------------------------------------------
/fonts/garamond/EBGaramond-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/EBGaramond-SemiBold.ttf
--------------------------------------------------------------------------------
/fonts/averia/AveriaLibre-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/averia/AveriaLibre-BoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/averia/AveriaLibre-LightItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/averia/AveriaLibre-LightItalic.ttf
--------------------------------------------------------------------------------
/fonts/fell_french_canon/IMFeFCit29C.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/fell_french_canon/IMFeFCit29C.otf
--------------------------------------------------------------------------------
/fonts/fell_french_canon/IMFeFCrm29C.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/fell_french_canon/IMFeFCrm29C.otf
--------------------------------------------------------------------------------
/fonts/garamond/EBGaramond-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/EBGaramond-BoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/averia/AveriaLibre-RegularItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/averia/AveriaLibre-RegularItalic.ttf
--------------------------------------------------------------------------------
/fonts/frankruehl/FrankRuehlCLM-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/frankruehl/FrankRuehlCLM-Medium.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-Bold.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-Italic.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-Medium.ttf
--------------------------------------------------------------------------------
/fonts/fell_french_canon/AveriaLibre-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/fell_french_canon/AveriaLibre-Bold.ttf
--------------------------------------------------------------------------------
/fonts/garamond/EBGaramond-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/EBGaramond-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-Regular.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-SemiBold.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-BoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-ExtraBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-ExtraBold.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-MediumItalic.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-ExtraBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-ExtraBoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/garamond/static/EBGaramond-SemiBoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/subalterngames/talmudifier/HEAD/fonts/garamond/static/EBGaramond-SemiBoldItalic.ttf
--------------------------------------------------------------------------------
/talmudifier/style.py:
--------------------------------------------------------------------------------
1 | class Style:
2 | """
3 | A style (e.g. bold) for a font.
4 | """
5 |
6 | def __init__(self, bold: bool, italic: bool, underline: bool):
7 | """
8 | :param bold: This style is bolded.
9 | :param italic: This style is italicized.
10 | :param underline: This style is underlined.
11 | """
12 |
13 | self.bold = bold
14 | self.italic = italic
15 | self.underline = underline
16 |
--------------------------------------------------------------------------------
/talmudifier/header.txt:
--------------------------------------------------------------------------------
1 | \documentclass[11pt,letterpaper,openany]{scrbook}
2 | \usepackage[letterpaper, bindingoffset=0.2in, left=1in,right=1in,top=.5in,bottom=.5in,footskip=.25in,marginparwidth=5em]{geometry}
3 |
4 | \usepackage{marginnote}
5 | \usepackage{sectsty}
6 | \usepackage{ragged2e}
7 | \usepackage{lineno}
8 | \usepackage{xcolor}
9 | \usepackage{paracol}
10 | \usepackage{fontspec}
11 |
12 | \allsectionsfont{\centering}
13 |
14 | \setlength\parindent{0pt}
15 | \setlength{\columnsep}{1.25em}
16 | \setlength{\parfillskip}{0pt}
17 | \setlength{\tabcolsep}{1em}
18 | \raggedbottom
19 |
20 |
--------------------------------------------------------------------------------
/talmudifier/util.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | import re
3 |
4 |
5 | # Set and create the output directory.
6 | output_directory = Path("Output")
7 | if not output_directory.exists():
8 | output_directory.mkdir()
9 | output_directory = str(output_directory.resolve())
10 |
11 |
12 | def to_camelcase(s: str) -> str:
13 | """
14 | Converts underscore_strings to UpperCamelCaseStrings.
15 | Source: https://rodic.fr/blog/camelcase-and-snake_case-strings-conversion-with-python/
16 |
17 | :param s: The string.
18 | """
19 |
20 | return re.sub(r'(?:^|_)(\w)', lambda m: m.group(1).upper(), s)
21 |
--------------------------------------------------------------------------------
/test_input_reader.py:
--------------------------------------------------------------------------------
1 | import io
2 | from talmudifier.talmudifier import Talmudifier
3 |
4 |
5 | if __name__ == "__main__":
6 | # Read the test page.
7 | with io.open("test/test_input.md", "rt", encoding="utf-8") as f:
8 | txt = f.read()
9 |
10 | # Parse the test page into 3 columns.
11 | lines = txt.split("\n")
12 | left = lines[2]
13 | center = lines[6]
14 | right = lines[10]
15 |
16 | # Generate the PDF.
17 | t = Talmudifier(left, center, right)
18 | tex = t.get_chapter("Talmudifier Test Page") + "\n"
19 | tex += t.get_tex()
20 | doc = t.writer.write(tex, "test_page")
21 |
22 | # Output the LaTeX string used to generated the PDF as a .tex file.
23 | with io.open("Output/test_page.tex", "wt", encoding="utf-8") as f:
24 | f.write(doc)
25 |
--------------------------------------------------------------------------------
/talmudifier/citation.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 |
4 | class Citation:
5 | """
6 | The rules defining a citation in a column.
7 | """
8 |
9 | def __init__(self, data: dict):
10 | """
11 | :param data: The citation data dictionary (see default.json).
12 | """
13 |
14 | assert "command" in data, "No command found"
15 | assert "pattern" in data, "No pattern found"
16 |
17 | # Set the command to start the citation and the pattern to search for.
18 | self.command = data["command"]
19 | self.pattern = data["pattern"]
20 |
21 | def apply_citation_to(self, word: str) -> (str, bool):
22 | """
23 | If this word is a citation, apply the citation.
24 |
25 | :param word: The string.
26 | :return: (The modified word, True if the word was modified)
27 | """
28 |
29 | match = re.match(self.pattern, word)
30 | if match is not None:
31 | return self.command + "{" + match.group(1) + "}", True
32 | else:
33 | return word, False
34 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Seth Alter
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/talmudifier/pdf_reader.py:
--------------------------------------------------------------------------------
1 | from os.path import exists
2 | from pdfminer.converter import TextConverter
3 | from pdfminer.pdfinterp import PDFPageInterpreter
4 | from pdfminer.pdfinterp import PDFResourceManager
5 | from pdfminer.pdfpage import PDFPage
6 | from pdfminer.layout import LAParams
7 | import io
8 |
9 |
10 | class PDFReader:
11 | """
12 | Extracts the raw text of a PDF and reads it.
13 | """
14 |
15 | @staticmethod
16 | def extract_text_from_pdf(pdf_path: str) -> str:
17 | """
18 | Source: http://www.blog.pythonlibrary.org/2018/05/03/exporting-data-from-pdfs-with-python/
19 | """
20 |
21 | # Added by me.
22 | assert exists(pdf_path), f"{pdf_path} does not exist."
23 |
24 | resource_manager = PDFResourceManager()
25 | fake_file_handle = io.StringIO()
26 | converter = TextConverter(resource_manager, fake_file_handle, laparams=LAParams())
27 | page_interpreter = PDFPageInterpreter(resource_manager, converter)
28 |
29 | with open(pdf_path, 'rb') as fh:
30 | for page in PDFPage.get_pages(fh,
31 | caching=True,
32 | check_extractable=True):
33 | page_interpreter.process_page(page)
34 |
35 | text = fake_file_handle.getvalue()
36 |
37 | # close open handles
38 | converter.close()
39 | fake_file_handle.close()
40 |
41 | return text
42 |
43 | @staticmethod
44 | def get_num_rows(pdf_path: str) -> int:
45 | """
46 | Returns the number of rows in a PDF file.
47 | This assumes that the PDF was created with LaTeX with the lineno package.
48 |
49 | :param pdf_path: The filepath to the PDF file.
50 | """
51 |
52 | text = PDFReader.extract_text_from_pdf(pdf_path)
53 |
54 | # Get the line numbers.
55 | lines = text.split("\n")
56 | lines = [line for line in lines if line.isdigit()][:-1]
57 |
58 | if len(lines) > 0:
59 | return int(lines[-1])
60 | else:
61 | return 1
62 |
--------------------------------------------------------------------------------
/talmudifier/pdf_writer.py:
--------------------------------------------------------------------------------
1 | from subprocess import call
2 | from pathlib import Path
3 | from platform import system
4 | from os import devnull
5 | from talmudifier.util import output_directory
6 |
7 |
8 | class PDFWriter:
9 | """
10 | Given LaTeX text, write a PDF.
11 | """
12 |
13 | END_DOCUMENT = r"\end{sloppypar}\end{document}"
14 |
15 | def __init__(self, preamble: str):
16 | """
17 | :param preamble: The preamble text.
18 | """
19 |
20 | # Begin the document.
21 | self.preamble = preamble + r"\begin{document}\begin{sloppypar}" + "\n\n"
22 |
23 | def write(self, text: str, filename: str) -> str:
24 | """
25 | Create a PDF from LaTeX text.
26 |
27 | :param text: The LaTeX text.
28 | :param filename: The filename of the PDF.
29 | :return: The LaTeX text, including the preamble and the end command(s).
30 | """
31 |
32 | # Combine the preamble, the new text, and the end command(s).
33 | doc_raw = self.preamble + text + PDFWriter.END_DOCUMENT
34 |
35 | # Replace line breaks with spaces.
36 | doc = doc_raw.replace("\n", " ")
37 |
38 | # Verify that all curly braces are balanced.
39 | num_start = len([c for c in doc if c == "{"])
40 | num_end = len([c for c in doc if c == "}"])
41 | assert num_start == num_end, f"Unbalanced curly braces!\n\n{doc_raw}"
42 |
43 | p = system()
44 | # Generate the PDF.
45 | if p == "Linux" or p == "Darwin":
46 | call(
47 | ["xelatex",
48 | "-output-directory", str(Path(output_directory).resolve()),
49 | "-jobname", filename, doc],
50 | stdout=open(devnull, "wb"))
51 | elif p == "Windows":
52 | call(['xelatex.exe',
53 | '-output-directory',
54 | str(Path(output_directory).resolve()),
55 | '-job-name=' + filename,
56 | doc],
57 | stdout=open(devnull, "wb"))
58 | else:
59 | raise Exception(f"Platform not supported: {p}")
60 |
61 | assert Path(output_directory).joinpath(filename + ".pdf").exists(), f"Failed to create: {filename}"
62 |
63 | return doc_raw
64 |
--------------------------------------------------------------------------------
/fonts/garamond/README:
--------------------------------------------------------------------------------
1 | EB Garamond Variable Font
2 | =========================
3 |
4 | This download contains EB Garamond as both variable fonts and static fonts.
5 |
6 | EB Garamond is a variable font with this axis:
7 | wght
8 |
9 | This means all the styles are contained in these files:
10 | EBGaramond-VariableFont:wght.ttf
11 | EBGaramond-Italic-VariableFont:wght.ttf
12 |
13 | If your app fully supports variable fonts, you can now pick intermediate styles
14 | that aren’t available as static fonts. Not all apps support variable fonts, and
15 | in those cases you can use the static font files for EB Garamond:
16 | static/EBGaramond-Regular.ttf
17 | static/EBGaramond-Medium.ttf
18 | static/EBGaramond-SemiBold.ttf
19 | static/EBGaramond-Bold.ttf
20 | static/EBGaramond-ExtraBold.ttf
21 | static/EBGaramond-Italic.ttf
22 | static/EBGaramond-MediumItalic.ttf
23 | static/EBGaramond-SemiBoldItalic.ttf
24 | static/EBGaramond-BoldItalic.ttf
25 | static/EBGaramond-ExtraBoldItalic.ttf
26 |
27 | Get started
28 | -----------
29 |
30 | 1. Install the font files you want to use
31 |
32 | 2. Use your app's font picker to view the font family and all the
33 | available styles
34 |
35 | Learn more about variable fonts
36 | -------------------------------
37 |
38 | https://developers.google.com/web/fundamentals/design-and-ux/typography/variable-fonts
39 | https://variablefonts.typenetwork.com
40 | https://medium.com/variable-fonts
41 |
42 | In desktop apps
43 |
44 | https://theblog.adobe.com/can-variable-fonts-illustrator-cc
45 | https://helpx.adobe.com/nz/photoshop/using/fonts.html#variable_fonts
46 |
47 | Online
48 |
49 | https://developers.google.com/fonts/docs/getting_started
50 | https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Fonts/Variable_Fonts_Guide
51 | https://developer.microsoft.com/en-us/microsoft-edge/testdrive/demos/variable-fonts
52 |
53 | Installing fonts
54 |
55 | MacOS: https://support.apple.com/en-us/HT201749
56 | Linux: https://www.google.com/search?q=how+to+install+a+font+on+gnu%2Blinux
57 | Windows: https://support.microsoft.com/en-us/help/314960/how-to-install-or-remove-a-font-in-windows
58 |
59 | Android Apps
60 |
61 | https://developers.google.com/fonts/docs/android
62 | https://developer.android.com/guide/topics/ui/look-and-feel/downloadable-fonts
63 |
64 | License
65 | -------
66 | Please read the full license text (OFL.txt) to understand the permissions,
67 | restrictions and requirements for usage, redistribution, and modification.
68 |
69 | Commercial usage is allowed for any purpose, in any medium, but some
70 | requirements apply in some situations. Always read your font licenses and
71 | understand what they mean.
72 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | """A setuptools based setup module.
2 | See:
3 | https://packaging.python.org/en/latest/distributing.html
4 | https://github.com/pypa/sampleproject
5 | """
6 |
7 | # Always prefer setuptools over distutils
8 | from setuptools import setup, find_packages
9 | # To use a consistent encoding
10 | import os
11 |
12 |
13 | # Get version
14 | here = os.path.abspath(os.path.dirname(__file__))
15 |
16 |
17 | setup(
18 | name='talmudifier',
19 |
20 | # Versions should comply with PEP440. For a discussion on single-sourcing
21 | # the version across setup.py and the project code, see
22 | # https://packaging.python.org/en/latest/single_source_version.html
23 | version="1.1.0",
24 |
25 | description='Generate Talmud-esque PDFs.',
26 | long_description="Given three blocks of text (corresponding to three columns), generate a Talmud page.",
27 |
28 | # The project's main homepage.
29 | url='https://github.com/subalterngames/talmudifier',
30 |
31 | # Author details
32 | author='Seth Alter',
33 |
34 | # Choose your license
35 | license='MIT',
36 |
37 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers
38 | classifiers=[
39 | # How mature is this project? Common values are
40 | # 3 - Alpha
41 | # 4 - Beta
42 | # 5 - Production/Stable
43 | 'Development Status :: 5 - Production/Stable',
44 |
45 | # Indicate who your project is intended for
46 | 'Intended Audience :: Developers',
47 | 'Topic :: Software Development',
48 |
49 | # Pick your license as you wish (should match "license" above)
50 | 'License :: OSI Approved :: MIT License',
51 |
52 | # Specify the Python versions you support here. In particular, ensure
53 | # that you indicate whether you support Python 2, Python 3 or both.
54 | 'Programming Language :: Python :: 3.6',
55 | 'Programming Language :: Python :: 3.7',
56 | ],
57 |
58 | # What does your project relate to?
59 | keywords='typesetting',
60 |
61 | # You can just specify the packages manually here if your project is
62 | # simple. Or you can use find_packages().
63 | packages=find_packages(exclude=['contrib', 'docs', 'tests']),
64 |
65 | # Alternatively, if you want to distribute just a my_module.py, uncomment
66 | # this:
67 | # py_modules=["my_module"],
68 |
69 | # List run-time dependencies here. These will be installed by pip when
70 | # your project is installed. For an analysis of "install_requires" vs pip's
71 | # requirements files see:
72 | # https://packaging.python.org/en/latest/requirements.html
73 | install_requires=['pdfminer.six', 'tqdm', 'pyhyphen'],
74 |
75 | )
--------------------------------------------------------------------------------
/talmudifier/word.py:
--------------------------------------------------------------------------------
1 | from hyphen import Hyphenator
2 | from typing import Dict, Optional
3 | from talmudifier.style import Style
4 | from talmudifier.citation import Citation
5 | import re
6 |
7 |
8 | class Word:
9 | """
10 | A word is a string plus style metadata (bold, italic, etc.)
11 | """
12 |
13 | H = Hyphenator('en_US')
14 |
15 | def __init__(self, word: str,
16 | style: Style,
17 | substitutions: Optional[Dict[str, str]],
18 | citation: Optional[Citation],
19 | get_pairs=True):
20 | """
21 | :param word: The actual word, stripped of any markdown styling.
22 | :param style: The font style for this word.
23 | :param substitutions: A list of keys to replace for values to make a valid TeX string.
24 | :param get_pairs: If true, get pairs of hyphenated fragments.
25 | """
26 |
27 | self.word = word
28 | self.pairs = []
29 |
30 | # Try to make this word a citation. If it is a citation, stop right here (citations are never hyphenated).
31 | if citation is not None:
32 | self.word, self.is_citation = citation.apply_citation_to(self.word)
33 | else:
34 | self.is_citation = False
35 |
36 | if self.is_citation:
37 | self.style = Style(False, False, False)
38 | return
39 | else:
40 | self.style = style
41 |
42 | # Get the pairs.
43 | if get_pairs:
44 | self.pairs = self._get_hyphenated_pairs(substitutions)
45 |
46 | # Do the substitutions.
47 | if substitutions is not None:
48 | for key in substitutions:
49 | self.word = re.sub(key, substitutions[key], self.word).replace("&", " ")
50 |
51 | if word.startswith('"'):
52 | self.word = "``" + word[1:]
53 | elif word. startswith("'"):
54 | self.word = "`" + word[1:]
55 |
56 | def _get_hyphenated_pairs(self, substitutions: Dict[str, str]) -> list:
57 | """
58 | Get all possible hyphenated pairs of this word (e.g. Cal- ifornia).
59 | """
60 |
61 | try:
62 | pairs_text = self.H.pairs(self.word)
63 | except IndexError:
64 | return []
65 |
66 | pairs = []
67 |
68 | for pair in pairs_text:
69 | # Append the hyphen to the first word.
70 | p0 = pair[0] + "-"
71 | p1 = pair[1]
72 | h = []
73 |
74 | # Add pairs of Word objects.
75 | for p in [p0, p1]:
76 | w = Word(p, self.style, substitutions if p == p0 else None,
77 | None,
78 | get_pairs=False)
79 |
80 | h.append(w)
81 | pairs.append(h)
82 | return pairs
83 |
84 | @staticmethod
85 | def is_valid(word: str) -> bool:
86 | """
87 | Returns true if the word is valid. Invalid words include characters that are illegal in TeX.
88 | There might not be a way to make this list exhaustive! We shall see...
89 |
90 | :param word: The word string.
91 | """
92 |
93 | return word not in ["}", "{", "[", "]", "\\", "|", "*", "**", "***"]
94 |
--------------------------------------------------------------------------------
/recipes/default.json:
--------------------------------------------------------------------------------
1 | {
2 | "fonts":
3 | {
4 | "left":
5 | {
6 | "path": "fonts/fell_french_canon",
7 | "ligatures": "TeX",
8 | "regular_font": "IMFeFCrm29C.otf",
9 | "italic_font": "IMFeFCit29C.otf",
10 | "bold_font": "AveriaLibre-Bold.ttf",
11 | "size": 11,
12 | "skip": 13,
13 | "substitutions":
14 | {
15 | "`=(.*)`": "\\\\marginnote{\\\\noindent\\\\justifying \\\\tiny \\1}",
16 | "->": "\\\\flowerfont 1 \\\\leftfont"
17 | },
18 | "citation":
19 | {
20 | "path": "fonts/frankruehl",
21 | "font": "FrankRuehlCLM-Medium.ttf",
22 | "command": "\\leftcitation",
23 | "font_command": "\\leftcitationfont",
24 | "pattern": "`L(.*)`"
25 | }
26 | },
27 | "center":
28 | {
29 | "path": "fonts/garamond",
30 | "ligatures": "TeX",
31 | "regular_font": "EBGaramond-SemiBold.ttf",
32 | "italic_font": "EBGaramond-SemiBoldItalic.ttf",
33 | "size": 11,
34 | "skip": 13,
35 | "substitutions":
36 | {
37 | "`L(.*)`": "\\\\leftcitationfont{\\\\leftcitation \\1} \\\\centerfont ",
38 | "`R(.*)`": "\\\\rightcitationfont{\\\\rightcitation \\1} \\\\centerfont "
39 | }
40 | },
41 | "right":
42 | {
43 | "path": "fonts/averia",
44 | "ligatures": "TeX",
45 | "regular_font": "AveriaLibre-Light.ttf",
46 | "italic_font": "AveriaLibre-RegularItalic.ttf",
47 | "bold_font": "AveriaLibre-Bold.ttf",
48 | "bold_italic_font": "AveriaLibre-BoldItalic.ttf",
49 | "size": 11,
50 | "skip": 13,
51 | "substitutions":
52 | {
53 | "\\|": "§"
54 | },
55 | "citation":
56 | {
57 | "path": "fonts/rashi/",
58 | "font": "Mekorot-Rashi.ttf",
59 | "command": "\\rightcitation",
60 | "font_command": "\\rightcitationfont",
61 | "pattern": "`R(.*)`"
62 | }
63 | }
64 | },
65 | "character_counts":
66 | {
67 | "one_third":
68 | {
69 | "left":
70 | {
71 | "1": 29
72 | },
73 | "center":
74 | {
75 | "1": 24
76 | },
77 | "right":
78 | {
79 | "1": 27
80 | }
81 | },
82 | "half":
83 | {
84 | "left":
85 | {
86 | "1": 47,
87 | "4": 183
88 | },
89 | "right":
90 | {
91 | "1": 44,
92 | "4": 174
93 | }
94 | },
95 | "two_thirds":
96 | {
97 | "center":
98 | {
99 | "1": 54
100 | },
101 | "right":
102 | {
103 | "1": 60
104 | }
105 | }
106 | },
107 | "chapter":
108 | {
109 | "definition": "\\newcommand{\\chfont}[1]{\\centerfont{\\huge\\textcolor{hcolor}{#1}}}",
110 | "command": "\\chfont",
111 | "numbering": false
112 | },
113 | "colors":
114 | {
115 | "hcolor": "D3230C",
116 | "rcolor": "D36F0C"
117 | },
118 | "misc_definitions":
119 | [
120 | "\\newcommand{\\leftcitation}[1]{\\leftcitationfont{\\Large\\textcolor{hcolor}{#1}}}",
121 | "\\newcommand{\\rightcitation}[1]{\\rightcitationfont{\\normalsize\\textcolor{rcolor}{#1}}}",
122 | "\\newfontfamily\\flowerfont[Path=fonts/fell_flowers/]{IMFeFlow2.otf}"
123 | ]
124 | }
--------------------------------------------------------------------------------
/talmudifier/paracol.py:
--------------------------------------------------------------------------------
1 | class Paracol:
2 | """
3 | A LaTeX paracol environment, composed of boxes.
4 | """
5 |
6 | TWO_THIRDS = "0.675"
7 | ONE_THIRD = "0.32"
8 | END = "\n\n\\end{paracol}"
9 |
10 | @staticmethod
11 | def get_paracol_header(left: bool, center: bool, right: bool) -> str:
12 | """
13 | Returns a paracol header with the correct column widths.
14 |
15 | :param left: If true, a left column exists.
16 | :param center: If true, a center column exists.
17 | :param right: If true, a right column exists.
18 | """
19 |
20 | if left:
21 | if center:
22 | if right:
23 | return r"\columnratio{" + f"{Paracol.ONE_THIRD},{Paracol.ONE_THIRD},{Paracol.ONE_THIRD}" + \
24 | "}\n\\begin{paracol}{3}"
25 | else:
26 | return r"\columnratio{0.31}" +"\n" + r"\begin{paracol}{2}"
27 | elif right:
28 | return r"\columnratio{0.5,0.5}" + "\n" + r"\begin{paracol}{2}"
29 | else:
30 | return r"\columnratio{1}\begin{paracol}{1}"
31 | elif right:
32 | if center:
33 | return r"\columnratio{" + Paracol.TWO_THIRDS + "}\n" + r"\begin{paracol}{2}"
34 | else:
35 | return r"\columnratio{1}" + "\n" + r"\begin{paracol}{1}"
36 | elif center:
37 | return r"\columnratio{1}" + "\n" + r"\begin{paracol}{1}"
38 | else:
39 | raise Exception("Tried defining a paracol environment for 0 columns.")
40 |
41 | @staticmethod
42 | def get_switch_from_left(left: bool, center: bool, right: bool, target: str) -> str:
43 | """
44 | Returns the switch-column command to switch from the left column to the target column.
45 |
46 | :param left: If true, a left column exists.
47 | :param center: If true, a center column exists.
48 | :param right: If true, a right column exists.
49 | :param target: The name of the target column: left, center, or right.
50 | """
51 |
52 | if target == "left":
53 | assert left, r"Tried to add a left column but there is none."
54 | return ""
55 | elif target == "center":
56 | assert center, r"Tried to add a center column but there is none."
57 | if left:
58 | return r"\switchcolumn"
59 | else:
60 | return ""
61 | elif target == "right":
62 | assert right, r"Tried to add a right column but there is none."
63 | if left:
64 | if center:
65 | return r"\switchcolumn[2]"
66 | else:
67 | return r"\switchcolumn"
68 | elif center:
69 | return r"\switchcolumn"
70 | else:
71 | return ""
72 | else:
73 | raise Exception(f"Bad column name: {target}")
74 |
75 | @staticmethod
76 | def get_column(left: bool, center: bool, right: bool, target: str, tex: str) -> str:
77 | """
78 | Returns the text of a column formatted in a paracol environment at a target column.
79 |
80 | :param left: If true, a left column exists.
81 | :param center: If true, a center column exists.
82 | :param right: If true, a right column exists.
83 | :param target: The name of the target column: left, center, or right.
84 | :param tex: A valid LaTeX string.
85 | """
86 |
87 | return Paracol.get_paracol_header(left, center, right) + "\n" + \
88 | Paracol.get_switch_from_left(left, center, right, target) + "\n" + \
89 | tex + "\n" + Paracol.END
90 |
--------------------------------------------------------------------------------
/talmudifier/column.py:
--------------------------------------------------------------------------------
1 | from talmudifier.word import Word
2 | from typing import List
3 | from talmudifier.style import Style
4 |
5 |
6 | class Column:
7 | """
8 | A column is a list of words and a font rule.
9 | From this, a valid block of TeX text can be generated.
10 | """
11 |
12 | def __init__(self, words: List[Word], font: str, font_size: int, font_skip: int):
13 | """
14 | :param words: The list of words in the column.
15 | :param font: The command used to start the font.
16 | :param font_size: The font size.
17 | :param font_skip: The font skip size.
18 | """
19 |
20 | self.words = words
21 | self.font = font
22 | self.font_size = font_size
23 | self.font_skip = font_skip
24 |
25 | if self.font_size > 0 and self.font_skip > 0:
26 | self.font_command = "\\fontsize{" + str(font_size) + "}{" + str(font_skip) + "}"
27 | else:
28 | self.font_command = ""
29 |
30 | def get_tex(self, close_braces: bool, start_index=0, end_index=-1) -> str:
31 | """
32 | Generate a LaTeX string from the words.
33 |
34 | :param close_braces: If true, make sure that all curly braces are closed.
35 | :param start_index: The start index.
36 | :param end_index: The end index. If this is -1, it is ignored.
37 | """
38 |
39 | # Start the text with the font size and the font command.
40 | tex = self.font_command + self.font + " "
41 |
42 | style = Style(False, False, False)
43 |
44 | # Get the slice of words.
45 | if end_index == -1:
46 | end_index = len(self.words)
47 |
48 | for word, w in zip(self.words[start_index: end_index], range(start_index, end_index)):
49 | # Add a citation word.
50 | if word.is_citation:
51 | # Close all braces.
52 | tex = Column._close_braces(tex)
53 | tex += word.word + " " + self.font + " "
54 | continue
55 |
56 | # Set bold style.
57 | if word.style.bold and not style.bold:
58 | tex += r"\textbf{"
59 | style.bold = True
60 | # Set italic style.
61 | if word.style.italic and not style.italic:
62 | tex += r"\textit{"
63 | style.italic = True
64 | # Set underline style.
65 | if word.style.underline and not style.underline:
66 | tex += r"\underline{"
67 | style.underline = True
68 |
69 | # Append the word.
70 | tex += word.word
71 |
72 | # Try to close style braces.
73 | if w < end_index - 1:
74 | next_word = self.words[w + 1]
75 | if style.bold and not next_word.style.bold:
76 | style.bold = False
77 | tex += "}"
78 | if style.italic and not next_word.style.italic:
79 | style.italic = False
80 | tex += "}"
81 | if style.underline and not next_word.style.underline:
82 | style.underline = False
83 | tex += "}"
84 |
85 | tex += " "
86 |
87 | if close_braces:
88 | tex = Column._close_braces(tex)
89 |
90 | return tex
91 |
92 | @staticmethod
93 | def _close_braces(tex: str) -> str:
94 | """
95 | Add a } for every {.
96 |
97 | :param tex: The TeX string.
98 | """
99 |
100 | num_braces = 0
101 | for c in tex:
102 | if c == "{":
103 | num_braces += 1
104 | elif c == "}":
105 | num_braces -= 1
106 | for i in range(num_braces):
107 | tex += "}"
108 | return tex
109 |
--------------------------------------------------------------------------------
/test/test_input.md:
--------------------------------------------------------------------------------
1 | # Left
2 |
3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis vehicula ligula at est bibendum, in eleifend erat dictum. Etiam sit amet tellus id ex ullamcorper faucibus. Suspendisse sed elit vel neque convallis iaculis id in urna. Sed tincidunt varius ipsum at scelerisque. Phasellus lacus lectus, sodales sit amet orci in, rutrum malesuada diam. Cras pulvinar elit sit amet lacus fringilla, in elementum mauris maximus. Phasellus euismod dolor sed pretium elementum. Nulla sagittis, elit eget semper porttitor, erat nunc commodo turpis, et bibendum ex lorem laoreet ipsum. Morbi auctor dignissim velit eget consequat. R. Seth: Blah blah blah blah. As it is written: "Aecenas lacinia nisi diam, vel pulvinar metus aliquet ut. Sed non lorem quis dui ultrices volutpat quis at diam."`=Lorem&1:23` Quisque at nisi magna. Duis nec lacus arcu. Morbi vel fermentum leo. Pellentesque hendrerit sagittis vulputate. Fusce laoreet malesuada odio, sit amet fringilla lectus ultrices porta. Aliquam feugiat finibus turpis id malesuada. Suspendisse hendrerit eros sit amet tempor pulvinar. Duis velit mauris, facilisis ut tincidunt sed, pharetra eu libero. Aenean lobortis tincidunt nisi. Praesent metus lacus, tristique sed porta non, tempus id quam. `Lא` I use these red Hebrew letters in my own WIP project along with various other font changes... **You might find this funcitonality useful.** R. Alter: I strongly disgree with you, and future readers of this will have to comb through pages upon pages of what might as well be lorem ipsum to figure out why we're dunking on each other so much. As it is written: "Sed ut eros id arcu tincidunt accumsan. Vestibulum vitae nisl blandit, commodo odio vitae, dictum nunc. Suspendisse pharetra lorem vitae ex tincidunt ornare. Maecenas efficitur tristique libero, eget commodo urna. Pellentesque libero sem, interdum ut nibh interdum, consequat elementum magna."`=Ipsum&69:420` `Lב` Aliquam facilisis vel turpis eu semper. Donec eget purus lectus. -> Check out how nice that little hand looks. Nice. -> Fusce porta pretium diam. Etiam venenatis nisl nec tempus fringilla. Vivamus vehicula nunc sed libero scelerisque viverra a quis libero. Integer ac urna ut lectus faucibus mattis ac id nunc. Morbi fermentum magna dui, at rhoncus nibh porttitor quis. Donec dui ante, semper non quam at, accumsan volutpat leo. Maecenas magna risus, finibus sit amet felis ut, vulputate euismod nunc.
4 |
5 | # Center
6 |
7 | Quisque at nisi magna. Duis nec lacus arcu. Morbi vel fermentum leo. Pellentesque hendrerit sagittis vulputate. Fusce laoreet malesuada odio, sit amet fringilla lectus ultrices porta. Aliquam feugiat finibus turpis id malesuada. `Lא` Suspendisse hendrerit eros sit amet tempor pulvinar. Duis velit mauris, facilisis ut tincidunt sed, pharetra eu libero. Aenean lobortis tincidunt nisi. `Lב` Praesent metus lacus, tristique sed porta non, tempus id quam. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; `Rא` In eu porta velit, quis pellentesque elit. `Rב` Quisque vehicula massa sit amet justo rhoncus auctor.
8 |
9 | # Right
10 |
11 | Aenean sed dolor suscipit, dignissim nisl at, blandit eros. Pellentesque scelerisque viverra ligula, vel congue lorem semper non. Nulla nec convallis neque. Donec ut leo velit. Donec felis odio, tincidunt non vestibulum ut, consectetur eget lectus. Donec varius finibus scelerisque. Aliquam nisl odio, sollicitudin eget sem non, tincidunt tempor felis. Curabitur et ullamcorper lacus. Cras eu ante quis arcu dictum ultrices. Proin vel ante quis elit sollicitudin auctor. Nullam ultricies tempor neque, varius scelerisque neque. Nam et arcu ut odio maximus tempor et sit amet elit. `Rא` Morbi fermentum dapibus elementum. Proin id metus ipsum. Aenean posuere nunc quis lacus varius, eget molestie mauris accumsan. | Fusce sapien ipsum, cursus a tincidunt vel, dignissim eu mi. Morbi id velit ac turpis ullamcorper lacinia. | Cras bibendum tellus vitae eros rutrum scelerisque. Vivamus sed pellentesque elit, non imperdiet massa. Curabitur dictum nisi sollicitudin luctus malesuada. Vestibulum id pulvinar risus, sit amet ornare libero. Etiam a nunc dolor. `Rב` In ac velit maximus, elementum ex et, blandit massa. Aliquam vehicula at neque sit amet ultrices. Integer id justo est. Quisque luctus erat eget aliquam faucibus. Etiam eu mi ac odio pretium dictum. Vestibulum viverra congue risus, ac egestas est dapibus eget. Aenean ut orci leo. Nulla dignissim erat pulvinar elit facilisis, ac venenatis leo tincidunt. Quisque eu lorem tortor. Quisque nec porttitor elit. Ut finibus ullamcorper odio, in porttitor lorem suscipit ut.
--------------------------------------------------------------------------------
/fonts/garamond/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright 2017 The EB Garamond Project Authors (https://github.com/octaviopardo/EBGaramond12)
2 |
3 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
4 | This license is copied below, and is also available with a FAQ at:
5 | http://scripts.sil.org/OFL
6 |
7 |
8 | -----------------------------------------------------------
9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10 | -----------------------------------------------------------
11 |
12 | PREAMBLE
13 | The goals of the Open Font License (OFL) are to stimulate worldwide
14 | development of collaborative font projects, to support the font creation
15 | efforts of academic and linguistic communities, and to provide a free and
16 | open framework in which fonts may be shared and improved in partnership
17 | with others.
18 |
19 | The OFL allows the licensed fonts to be used, studied, modified and
20 | redistributed freely as long as they are not sold by themselves. The
21 | fonts, including any derivative works, can be bundled, embedded,
22 | redistributed and/or sold with any software provided that any reserved
23 | names are not used by derivative works. The fonts and derivatives,
24 | however, cannot be released under any other type of license. The
25 | requirement for fonts to remain under this license does not apply
26 | to any document created using the fonts or their derivatives.
27 |
28 | DEFINITIONS
29 | "Font Software" refers to the set of files released by the Copyright
30 | Holder(s) under this license and clearly marked as such. This may
31 | include source files, build scripts and documentation.
32 |
33 | "Reserved Font Name" refers to any names specified as such after the
34 | copyright statement(s).
35 |
36 | "Original Version" refers to the collection of Font Software components as
37 | distributed by the Copyright Holder(s).
38 |
39 | "Modified Version" refers to any derivative made by adding to, deleting,
40 | or substituting -- in part or in whole -- any of the components of the
41 | Original Version, by changing formats or by porting the Font Software to a
42 | new environment.
43 |
44 | "Author" refers to any designer, engineer, programmer, technical
45 | writer or other person who contributed to the Font Software.
46 |
47 | PERMISSION & CONDITIONS
48 | Permission is hereby granted, free of charge, to any person obtaining
49 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
50 | redistribute, and sell modified and unmodified copies of the Font
51 | Software, subject to the following conditions:
52 |
53 | 1) Neither the Font Software nor any of its individual components,
54 | in Original or Modified Versions, may be sold by itself.
55 |
56 | 2) Original or Modified Versions of the Font Software may be bundled,
57 | redistributed and/or sold with any software, provided that each copy
58 | contains the above copyright notice and this license. These can be
59 | included either as stand-alone text files, human-readable headers or
60 | in the appropriate machine-readable metadata fields within text or
61 | binary files as long as those fields can be easily viewed by the user.
62 |
63 | 3) No Modified Version of the Font Software may use the Reserved Font
64 | Name(s) unless explicit written permission is granted by the corresponding
65 | Copyright Holder. This restriction only applies to the primary font name as
66 | presented to the users.
67 |
68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69 | Software shall not be used to promote, endorse or advertise any
70 | Modified Version, except to acknowledge the contribution(s) of the
71 | Copyright Holder(s) and the Author(s) or with their explicit written
72 | permission.
73 |
74 | 5) The Font Software, modified or unmodified, in part or in whole,
75 | must be distributed entirely under this license, and must not be
76 | distributed under any other license. The requirement for fonts to
77 | remain under this license does not apply to any document created
78 | using the Font Software.
79 |
80 | TERMINATION
81 | This license becomes null and void if any of the above conditions are
82 | not met.
83 |
84 | DISCLAIMER
85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93 | OTHER DEALINGS IN THE FONT SOFTWARE.
94 |
--------------------------------------------------------------------------------
/fonts/averia/OFL.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2011, Dan Sayers (i@iotic.com),
2 | with Reserved Font Name 'Averia' and 'Averia Libre'.
3 |
4 | This Font Software is licensed under the SIL Open Font License, Version 1.1.
5 | This license is copied below, and is also available with a FAQ at:
6 | http://scripts.sil.org/OFL
7 |
8 |
9 | -----------------------------------------------------------
10 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
11 | -----------------------------------------------------------
12 |
13 | PREAMBLE
14 | The goals of the Open Font License (OFL) are to stimulate worldwide
15 | development of collaborative font projects, to support the font creation
16 | efforts of academic and linguistic communities, and to provide a free and
17 | open framework in which fonts may be shared and improved in partnership
18 | with others.
19 |
20 | The OFL allows the licensed fonts to be used, studied, modified and
21 | redistributed freely as long as they are not sold by themselves. The
22 | fonts, including any derivative works, can be bundled, embedded,
23 | redistributed and/or sold with any software provided that any reserved
24 | names are not used by derivative works. The fonts and derivatives,
25 | however, cannot be released under any other type of license. The
26 | requirement for fonts to remain under this license does not apply
27 | to any document created using the fonts or their derivatives.
28 |
29 | DEFINITIONS
30 | "Font Software" refers to the set of files released by the Copyright
31 | Holder(s) under this license and clearly marked as such. This may
32 | include source files, build scripts and documentation.
33 |
34 | "Reserved Font Name" refers to any names specified as such after the
35 | copyright statement(s).
36 |
37 | "Original Version" refers to the collection of Font Software components as
38 | distributed by the Copyright Holder(s).
39 |
40 | "Modified Version" refers to any derivative made by adding to, deleting,
41 | or substituting -- in part or in whole -- any of the components of the
42 | Original Version, by changing formats or by porting the Font Software to a
43 | new environment.
44 |
45 | "Author" refers to any designer, engineer, programmer, technical
46 | writer or other person who contributed to the Font Software.
47 |
48 | PERMISSION & CONDITIONS
49 | Permission is hereby granted, free of charge, to any person obtaining
50 | a copy of the Font Software, to use, study, copy, merge, embed, modify,
51 | redistribute, and sell modified and unmodified copies of the Font
52 | Software, subject to the following conditions:
53 |
54 | 1) Neither the Font Software nor any of its individual components,
55 | in Original or Modified Versions, may be sold by itself.
56 |
57 | 2) Original or Modified Versions of the Font Software may be bundled,
58 | redistributed and/or sold with any software, provided that each copy
59 | contains the above copyright notice and this license. These can be
60 | included either as stand-alone text files, human-readable headers or
61 | in the appropriate machine-readable metadata fields within text or
62 | binary files as long as those fields can be easily viewed by the user.
63 |
64 | 3) No Modified Version of the Font Software may use the Reserved Font
65 | Name(s) unless explicit written permission is granted by the corresponding
66 | Copyright Holder. This restriction only applies to the primary font name as
67 | presented to the users.
68 |
69 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
70 | Software shall not be used to promote, endorse or advertise any
71 | Modified Version, except to acknowledge the contribution(s) of the
72 | Copyright Holder(s) and the Author(s) or with their explicit written
73 | permission.
74 |
75 | 5) The Font Software, modified or unmodified, in part or in whole,
76 | must be distributed entirely under this license, and must not be
77 | distributed under any other license. The requirement for fonts to
78 | remain under this license does not apply to any document created
79 | using the Font Software.
80 |
81 | TERMINATION
82 | This license becomes null and void if any of the above conditions are
83 | not met.
84 |
85 | DISCLAIMER
86 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
87 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
88 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
89 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
90 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
91 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
92 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
93 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
94 | OTHER DEALINGS IN THE FONT SOFTWARE.
95 |
--------------------------------------------------------------------------------
/talmudifier/row_maker.py:
--------------------------------------------------------------------------------
1 | from talmudifier.column import Column
2 | from talmudifier.paracol import Paracol
3 | from talmudifier.pdf_reader import PDFReader
4 | from talmudifier.pdf_writer import PDFWriter
5 | from pathlib import Path
6 |
7 |
8 | class RowMaker:
9 | """
10 | Create a target number of rows from a column in a paracol environment.
11 | """
12 |
13 | def __init__(self, left: bool, center: bool, right: bool, target: str, writer: PDFWriter):
14 | self.paracol = Paracol.get_paracol_header(left, center, right)
15 | self.switch = Paracol.get_switch_from_left(left, center, right, target)
16 | self.writer = writer
17 |
18 | def get_text_of_length(self, column: Column, target_num_rows: int, expected_length: int) -> (str, Column):
19 | """
20 | Returns enough text to fill the target number of rows.
21 |
22 | :param column: The column of words.
23 | :param target_num_rows: The target number of rows.
24 | :param expected_length: The expected length of characters. Used as a baseline for row-making.
25 | """
26 |
27 | col_temp = Column([], column.font, column.font_size, column.font_skip)
28 |
29 | # Try to fill the temporary column with the target number of characters.
30 | filled = False if expected_length > 0 else True
31 | next_word_index = 0
32 | while not filled:
33 | if next_word_index >= len(column.words):
34 | filled = True
35 | continue
36 | # Get the next word.
37 | col_temp.words.append(column.words[next_word_index])
38 | next_word_index += 1
39 |
40 | # Check if we have exceeded the target length.
41 | row_length_estimate = sum([len(w.word) for w in col_temp.words])
42 | filled = row_length_estimate > expected_length
43 |
44 | next_word_index = len(col_temp.words)
45 |
46 | done = False
47 | while not done:
48 | num_rows = self.get_num_rows(col_temp.get_tex(True))
49 |
50 | # Try to overflow the column.
51 | if num_rows <= target_num_rows:
52 | # If there no more words to add, return what we've got.
53 | if next_word_index >= len(column.words):
54 | return col_temp.get_tex(True), Column([], column.font, column.font_size, column.font_skip)
55 |
56 | # Append a new word.
57 | col_temp.words.append(column.words[next_word_index])
58 | next_word_index += 1
59 | # Walk it back.
60 | else:
61 | if len(col_temp.words) == 0:
62 | raise Exception("Empty column? I got nothin'.")
63 |
64 | # Remove the last word.
65 | last_word = col_temp.words.pop()
66 |
67 | num_rows = self.get_num_rows(col_temp.get_tex(True))
68 |
69 | # If removing the last word gave us the target number of rows, try adding hyphenated fragments.
70 | if num_rows == target_num_rows:
71 | for pair in last_word.pairs:
72 | # Create a temporary column that includes the first half of the pair.
73 | words = col_temp.words[:]
74 |
75 | words.append(pair[0])
76 | col_temp_temp = Column(words, column.font, column.font_size, column.font_skip)
77 |
78 | # The hyphenated fragment fits! Add it and return the truncated column.
79 | if self.get_num_rows(col_temp_temp.get_tex(True)) == target_num_rows:
80 | words = column.words[len(col_temp.words) + 1:]
81 |
82 | # Insert the second half of the word pair to the words list and add it to a new column.
83 | words.insert(0, pair[1])
84 | return col_temp_temp.get_tex(True), Column(words, column.font, column.font_size, column.font_skip)
85 | # No hyphenated pair worked. Return what we've got.
86 | return col_temp.get_tex(True), Column(column.words[len(col_temp.words):], column.font, column.font_size, column.font_skip)
87 |
88 | def get_num_rows(self, tex: str) -> int:
89 | """
90 | Returns the number of rows the TeX string fills in the paracol environment.
91 |
92 | :param tex: The TeX string.
93 | """
94 |
95 | tex = r"\internallinenumbers \begin{linenumbers}" + tex + r"\end{linenumbers} \resetlinenumber[1]"
96 | tex = self.paracol + self.switch + " " + tex + "\n\n\\end{paracol}"
97 | self.writer.write(tex, "line_count")
98 | output_path = str(Path("Output/line_count.pdf").resolve())
99 | return PDFReader.get_num_rows(output_path)
100 |
--------------------------------------------------------------------------------
/row_length_calculator.py:
--------------------------------------------------------------------------------
1 | from talmudifier.word import Word
2 | from talmudifier.pdf_writer import PDFWriter
3 | from talmudifier.pdf_reader import PDFReader
4 | from talmudifier.style import Style
5 | from random import shuffle
6 | from tqdm import tqdm
7 | from talmudifier.talmudifier import Paracol
8 | from argparse import ArgumentParser
9 | from json import load
10 | from pathlib import Path
11 |
12 |
13 | class RowLengthCalculator:
14 | """
15 | Calculate the average number of characters in a given number of rows.
16 | """
17 |
18 | def __init__(self, columns: str, target: str, font: str, font_size: str, num_rows: int):
19 | """
20 | :param columns: The columns included in this paracol environment as a string, e.g. "LC"
21 | :param target: The target column, e.g. "left"
22 | :param font: The font command, e.g. "\\leftfont"
23 | :param font_size: The font size command, e.g. "\\fontsize{11}{13}"
24 | :param num_rows: The number of rows to make.
25 | """
26 |
27 | self.paracol = Paracol.get_paracol_header("L" in columns, "C" in columns, "R" in columns)
28 | switch = Paracol.get_switch_from_left("L" in columns, "C" in columns, "R" in columns, target)
29 | self.paracol += "\n\n" + switch
30 | self.paracol += "\n\n" + font_size + font
31 |
32 | self.writer = PDFWriter(Path("header.txt").read_text())
33 | self.num_rows = num_rows
34 |
35 | def _get_num_rows(self, line: str, word: Word) -> int:
36 | """
37 | Returns the number of rows if the word was added to the line.
38 |
39 | :param line: The line.
40 | :param word: The new word.
41 | """
42 | tex = r"\internallinenumbers \begin{linenumbers}" + line + " " + word.word + r"\end{linenumbers} \resetlinenumber[1]"
43 | tex = self.paracol + tex + "\n\n\\end{paracol}"
44 | self.writer.write(tex, "line_count")
45 | output_path = str(Path("Output/line_count.pdf").resolve())
46 | return PDFReader.get_num_rows(output_path)
47 |
48 | def _get_num_characters_in_trial(self, josephus: list, style: Style) -> int:
49 | """
50 | Fill a row with random words from Josephus' Antiquities, and return the number of characters.
51 | Try to fill the row as much as possible by adding a hyphenated fragment at the end.
52 |
53 | :param josephus: Random words from Antiquities.
54 | :param style: The font style (regular).
55 | """
56 |
57 | line = ""
58 |
59 | random_words = josephus[:]
60 | shuffle(random_words)
61 |
62 | # Build a row of random words.
63 | done = False
64 | while not done:
65 | word = Word(random_words.pop(), style, None, None)
66 |
67 | # Filter out invalid words.
68 | while not Word.is_valid(word.word):
69 | word = Word(random_words.pop(), style, None, None)
70 |
71 | num_lines = self._get_num_rows(line, word)
72 |
73 | # We went over the end. Try to get a hyphenated fragment.
74 | if num_lines > self.num_rows:
75 | pairs = word.pairs
76 | for pair in pairs:
77 | for p in pair:
78 | num_lines = self._get_num_rows(line, p)
79 | if num_lines == self.num_rows:
80 | line += " " + p.word
81 | return len(line.strip())
82 | done = True
83 | else:
84 | line += " " + word.word
85 | return len(line.strip())
86 |
87 | def get_num_chars(self, num_trials: int) -> int:
88 | """
89 | Get the average number of characters over the course of many trials.
90 |
91 | :param num_trials: The number of trials.
92 | """
93 |
94 | num_chars_total = 0
95 |
96 | pbar = tqdm(total=num_trials)
97 |
98 | # Get some words.
99 | with open("test/josephus.txt", "rt") as f:
100 | josephus = f.read()
101 | josephus = josephus.split(" ")
102 | josephus = [j for j in josephus if j != ""]
103 |
104 | style = Style(False, False, False)
105 |
106 | for i in range(num_trials):
107 | num_chars_total += self._get_num_characters_in_trial(josephus, style)
108 | pbar.update(1)
109 | pbar.set_description(str(round(num_chars_total / (i + 1))))
110 | pbar.close()
111 |
112 | return round(num_chars_total / num_trials)
113 |
114 |
115 | if __name__ == "__main__":
116 | parser = ArgumentParser()
117 | parser.add_argument("--columns", type=str)
118 | parser.add_argument("--target", type=str)
119 | parser.add_argument("--rows", nargs="?", default=1, type=int)
120 | parser.add_argument("--trials", nargs="?", default=100, type=int)
121 | parser.add_argument("--recipe", nargs="?", default="default.json")
122 |
123 | args = parser.parse_args()
124 |
125 | # Load the recipe
126 | with open("recipes/" + args.recipe, "rt") as f:
127 | recipe = load(f)
128 | if args.target == "left":
129 | font = r"\leftfont"
130 | elif args.target == "center":
131 | font = r"\centerfont"
132 | elif args.target == "right":
133 | font = r"\rightfont"
134 | else:
135 | raise Exception(f"Invalid target: {args.target}")
136 |
137 | size = r"\fontsize{" + str(recipe["fonts"][args.target]["size"]) + "}{" + str(recipe["fonts"][args.target]["skip"]) + "}"
138 |
139 | num_chars = RowLengthCalculator(args.columns, args.target, font, size, args.rows).get_num_chars(args.trials)
140 | print(f"Cols: {args.columns}\nTarget: {args.target}\nRows: {args.rows}\nAVERAGE: {num_chars}")
141 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Talmudifier
4 |
5 | Talmudifier is a Python module that will procedurally generate page layouts similar to the [Talmud](https://en.wikipedia.org/wiki/Talmud#/media/File:First_page_of_the_first_tractate_of_the_Talmud_(Daf_Beis_of_Maseches_Brachos).jpg).
6 |
7 |
8 |
9 | That .PDF was generated from [this input file](test/test_input.md).
10 |
11 | ## 1. Who is this for?
12 |
13 | This is primarily for people familiar with [Markdown](https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet), [Python](https://www.python.org/), [LaTeX](https://www.overleaf.com/learn/latex/XeLaTeX), and medieval typesetting. There might not be many of these people. You should at minimum:
14 |
15 | 1. Have some experience with opening files and parsing strings in Python.
16 | 2. Feel comfortable installing LaTeX packages. (You won't use LaTeX directly, so you don't need to learn any of its syntax.)
17 | 3. Have at least a cursory understanding of why the Talmud is typeset the way it is.
18 |
19 | If you don't want to code, that's OK! See "Non-Coders" **4. Usage**.
20 |
21 | **Please help me improve this README.** I wrote this README initially just so that _I_ could remember how the program works. There's probably a lot missing, and a lot that is very misleading... So, please email me with suggestions for improving the documentation (or, better yet, create a GitHub Issue if you know how).
22 |
23 | ## 2. Requirements
24 |
25 | - Windows, OS X, or Linux
26 | - Python 3.6 or 3.7
27 | - XeLaTeX
28 | - marginnote
29 | - sectsty
30 | - ragged2e
31 | - lineno
32 | - xcolor
33 | - paracol
34 | - fontspec
35 | - scrbook
36 |
37 | If you're not sure how to install anything and Google isn't being helpful, you can email me.
38 |
39 | ## 3. Setup
40 |
41 | 1. Open the terminal.
42 |
43 | | Windows | OS X | Linux |
44 | | -------------------------------------------------- | -------------------------------------------- | ------------------------------------------------------------ |
45 | | Search for "powershell" in the start menu. Run it. | In Spotlight, search for "Terminal". Run it. | Depends; if you're not sure how to do this, [email me](subalterngames@gmail.com). |
46 |
47 | 2. In the terminal, type:
48 |
49 | ```bash
50 | cd ~//talmudifier
51 | ```
52 |
53 | For example, if this folder is in `Downloads/RandomStuff`:
54 |
55 | ```bash
56 | cd ~/Downloads/RandomStuff/talmudifier
57 | ```
58 |
59 | 3. In the terminal, type:
60 |
61 | ```bash
62 | pip3 setup.py -e .
63 | ```
64 |
65 | Don't forget the `.` at the end! This will install the Talmudifier Python module.
66 |
67 | 4. In the terminal, try creating a test page.
68 |
69 | | Windows | OS X | Linux |
70 | | ---------------------------- | ------------------------------ | ------------------------------ |
71 | | `py -3 test_input_reader.py` | `python3 test_input_reader.py` | `python3 test_input_reader.py` |
72 |
73 | If this test script works, then everything is set up OK. The script will generate:
74 |
75 | - The test page PDF: `talmudifier/Output/test_page.pdf`
76 | - The .tex file used to create the PDF: `talmudifier/Output/test_page.tex`
77 | - A few other files in `Output/` that you can ignore.
78 |
79 | ## 4. Usage
80 |
81 | ### Coders:
82 |
83 | Talmudifier requires three sources of markdown text. It doesn't care where the sources come from as long as they are imported correctly. (In other words, you're on your own providing the text and slotting it into this program.)
84 |
85 | ```python
86 | left = "This is one source of text."
87 | center = "This is another source of text."
88 | right = "To be honest, you'll need a lot more words per column for this to work right."
89 | ```
90 |
91 | Or just pull the text from three files, e.g.:
92 |
93 | ```python
94 | import io
95 |
96 | with io.open("left.txt", "rt", encoding="utf-8") as f:
97 | left = f.read()
98 | ```
99 |
100 | Then import Talmudifier and generate a .pdf:
101 |
102 | ```
103 | from talmudifier.talmudifier import Talmudifier
104 |
105 | t = Talmudifier(left, center, right)
106 | t.create_pdf()
107 | ```
108 |
109 | ### Non-coders:
110 |
111 | 1. Edit the test input file `talmudifier/test/test_input.md`
112 | 2. Run `test_input_reader.py` (see **3. Setup**)
113 |
114 | ## 5. API
115 |
116 | #### `Talmudifier`
117 |
118 | Generate Talmud-esque page layouts, given markdown plaintext and a recipe JSON file.
119 |
120 | ```python
121 | from talmudifier.talmudifier import Talmudifier
122 |
123 | t = Talmudifier(left, center, right)
124 | ```
125 |
126 | ##### `__init__(self, text_left: str, text_center: str, text_right: str, recipe_filename="default.json")`
127 |
128 | | Parameter | Description |
129 | | --- | --- |
130 | | text_left | The markdown text of the left column.|
131 | | text_center | The markdown text of the center column.|
132 | | text_right | The markdown text of the right column.|
133 | | recipe_filename | The filename of the recipe, located in recipes/|
134 |
135 | ***
136 |
137 | ##### `get_tex(self) -> str`
138 |
139 | Generate the body of text.
140 | 1. Create 4 rows on the left and right (width = one half).
141 | 2. Create 1 row on the left and right (width = one third).
142 | 3. Until the columns are all done: Find the shortest column and add it. Add other columns up to that length.
143 |
144 | ***
145 |
146 | ##### `get_chapter(self, title: str) -> str`
147 |
148 | Returns the chapter command.
149 |
150 | | Parameter | Description |
151 | | --- | --- |
152 | | title | The title of the chapter.|
153 |
154 | ***
155 |
156 | ##### `create_pdf(self, chapter="", output_filename="output", print_tex=False) -> str`
157 |
158 | Create a PDF. Generate the chapter and the body, and append them to the preamble. Returns the LaTeX string.
159 |
160 | | Parameter | Description |
161 | | --- | --- |
162 | | chapter | If not empty, create the header here.|
163 | | output_filename | The name of the output file.|
164 | | print_tex | If true, print the LaTeX string to the console.|
165 |
166 | #### `PDFWriter`
167 |
168 | Given LaTeX text, write a PDF. A `Talmudifier` object has its own writer, but it might be useful for you to create .pdfs manually (especially if you want to stitch a lot of .tex files together).
169 |
170 | ```python
171 | from talmudifier.pdf_writer import PDFWriter
172 |
173 | writer = PDFWriter(preamble)
174 | ```
175 |
176 | ##### `__init__(self, preamble: str)`
177 |
178 | | Parameter | Description |
179 | | --- | --- |
180 | | preamble | The preamble text.|
181 |
182 | ***
183 |
184 | ##### `write(self, text: str, filename: str) -> str`
185 |
186 | Create a PDF from LaTeX text. Returns the LaTeX text, including the preamble and the end command(s).
187 |
188 | | Parameter | Description |
189 | | --- | --- |
190 | | text | The LaTeX text.|
191 | | filename | The filename of the PDF. |
192 |
193 | ## 6. Recipes
194 |
195 | A recipe is a JSON file that defines the fonts and other styling rules for your page. It is functionally the same as just writing your own TeX preamble, but probably a lot more user-friendly.
196 |
197 | By default, `talmudifier.py` uses: `recipes/default.json`.
198 |
199 | All custom recipes should be saved to the `recipes/` directly as `.json` files.
200 |
201 | ### `fonts`
202 |
203 | Definitions for the font per column.
204 |
205 | ```json
206 | "fonts":
207 | {
208 | "left":
209 | {
210 |
211 | }
212 | }
213 | ```
214 |
215 | _Key = The name of the font (left, center, right). Don't change these._
216 |
217 | | Field | Type | Description | Required? |
218 | | ------------------ | ---------- | ------------------------------------------------------------ | --------- |
219 | | `path` | string | Path to the directory of fonts relative to `talmudifier.py`. | ✔ |
220 | | `ligatures` | string | How ligatures are handled. This should probably always be `"TeX"`. There are other options, but the documentation for them is [sparse](https://tex.stackexchange.com/questions/296737/what-do-the-various-values-of-the-ligatures-option-of-fontspec-do). | ✔ |
221 | | `regular_font` | string | The regular font file. Must be a file located in the directory `path`. | ✔ |
222 | | `italic_font` | string | The _italic_ font file. Must be a file located in the directory `path`. | ❌ |
223 | | `bold_font` | string | The **bold** font file. Must be a file located in the directory `path`. | ❌ |
224 | | `bold_italic_font` | string | The _**bold+italic**_ font file. Must be a file located in the directory `path`. | ❌ |
225 | | `size` | integer | The font size. This will default to whatever the font size is in the `header.txt` preamble. If `skip` is not included, this is ignored. | ❌ |
226 | | `skip` | integer | The size of the spacing between two lines. Generally you want this to be 2 more than `size`. If `size` is not included, this is ignored. | ❌ |
227 | | `substitutions` | dictionary | Per word in this column, replace every key in the dictionary with the value. | ❌ |
228 | | `citation` | dictionary | The recipe for citations _pointing to_ this column. See below. | ❌ |
229 |
230 | #### `citation`
231 |
232 | Citations are letters or words that direct the reader from column to column.
233 |
234 | | Field | Type | Description | Required? |
235 | | -------------- | ----------------- | ------------------------------------------------------------ | --------- |
236 | | `path` | string | Path to the directory of fonts relative to `talmudifier.py`. | ✔ |
237 | | `font` | string | The font file. Must be a file located in the directory `path`. | ✔ |
238 | | `command` | string | The command used in the TeX document body to start the citation font. This can be the same as `font_command` but you might want to define a custom version. `default.json` _does_ have a custom version, that makes citations red-colored. | ✔ |
239 | | `font_command` | string | The command used to name the font family. You probably don't want to change this from `default.json`'s values. | ✔ |
240 | | `pattern` | string
(regex) | `talmudifier.py` will replace anything in the input string with this regex pattern with a properly-formatted citation letter. | ✔ |
241 |
242 | ### `character_counts`
243 |
244 | These are the average number of characters in a column across many trials, given different column configurations (e.g. left and right only), a target column (e.g. left), and a target number of rows (e.g. 1).
245 |
246 | `talmudifier.py` will use these numbers to fill columns with a "best guess" number of words before adding and subtracting words to reach a given row number target (e.g. if there are only left and right columns and you want 1 row on the left, `talmudifier.py` will first try to fill the row with 47 characters). If there is no key present, `talmudifier.py` will first look for the `"1"` key and multiply the value by the number of rows (e.g. 47 * 4). If there are no keys at all, `talmudifier.py` will just add a word at a time to the column (which is much slower).
247 |
248 | You can calculate these values yourself by running `row_length_calculator.py`.
249 |
250 | ```json
251 | "character_counts":
252 | {
253 | "half":
254 | {
255 | "left":
256 | {
257 | "1": 47
258 | }
259 | }
260 | }
261 | ```
262 |
263 | | Key | Description |
264 | | -------- | ------------------------------------------------------------ |
265 | | `"half"` | The expected width of the column. Can be `"half"`, `"one_third"`, or `"two_thirds"`. |
266 | | `"left"` | The target column within the table. Can be `"left"`, `"center"`, or `"right"`. |
267 | | `"1"` | The target number of rows. Must be an integer.
Value=The average number of characters across many trials. |
268 |
269 | #### `row_length_calculator.py`
270 |
271 | Use this script to calculate the average number of characters per row given a recipe, a list of tables, and a target column.
272 |
273 | | Argument | Type | Description | Default |
274 | | ----------- | ------- | ------------------------------------------------------------ | -------------- |
275 | | `--columns` | string | The columns in the table. Can be `LCR`, `LR`, etc. | `LR` |
276 | | `--target` | string | The target column. Can be `"left"`, `"center"`, or `"right"`. | |
277 | | `--rows` | integer | The number of rows. | `1` |
278 | | `--trials` | integer | The number of trials to run and then average. | `100` |
279 | | `--recipe` | string | Filename of the recipe file in the `recipes/` directory. | `default.json` |
280 |
281 | ### `chapter`
282 |
283 | Define the chapter header style.
284 |
285 | | Field | Type | Description | Required? |
286 | | ------------ | ------- | ------------------------------------------------------------ | --------- |
287 | | `definition` | string | The TeX definition of the command used to define the font. Sorry this is a bit of a mess. The parameter after `\newcommand` must match `\command` (see `default.json`). | ✔ |
288 | | `command` | string | The command used when creating a chapter. | ✔ |
289 | | `numbering` | boolean | If true, chapter headers will start with numbers. | ✔ |
290 |
291 | ### `colors`
292 |
293 | Define colors for the preamble. The key is the name of the color, and the value is the HTML hex code. This can be left empty if you don't need any extra colors.
294 |
295 | ### `misc_definitions`
296 |
297 | Anything else you'd like to include in the preamble. `default.json` includes the following:
298 |
299 | - Definitions for left and right citation font commands; note how they include a color found in `colors`.
300 | - Definition for `\flowerfont`, referred to in `recipe["fonts"]["left"]["substitutions"]`.
301 |
302 | ## 7. How it works
303 |
304 | #### Style
305 |
306 | Talmudifier converts markdown text into LaTeX text and then outputs a PDF:
307 |
308 | | Markdown | LaTeX | Output |
309 | | ----------------------------------------- | ----------------------------------------------------- | --------------------------------------- |
310 | | `_**Rabbi Yonatan said to Rabbi Rivka**_` | `\textit{\textbf{Rabbi Yonatan said to Rabbi Rivka}}` | _**Rabbi Yonatan said to Rabbi Rivka**_ |
311 |
312 | You can apply different styles and options to each column with a _recipe_ file (a default file is included in this repo).
313 |
314 | #### Layout
315 |
316 | Columns are generated using the following process:
317 |
318 | 1. Create four rows of the left and right columns of half width in a `paracol` environment.
319 | 2. Create an additional row on the left and right of one-third width.
320 | 3. Find the shortest column. For each column that still has text, add it to the `paracol` environment up to that number of rows.
321 |
322 | How do we know how many rows a column will be? _By repeatedly generating test pdfs._ Talmudifier outputs a column pdf with line numbers (using the `lineno` package), and then extracts plaintext from the pdf. This is ponderous and very hacky. If you know a better way, let me know. Right now, I think the most obvious improvement would be to catch the bytestream of the pdf before it is written to disk and extract the plaintext from that, but as far as I know that's not possible either.
323 |
324 | **This script will take a while to run.** Expect the entire process to require approximately 5 minutes per page.
325 |
326 | ## 8. Typesetting notes
327 |
328 | Talmudifier is meant to generate Talmud-esque pages rather than Talmud pages. The actual traditional page layouts of the Talmud are far more varied and complicated. However, the algorithm is inspired by the actual layout "rules" and typesetting techniques. Because it took me a year to track down enough errant URLs and rare books to write this Python script, I'll summarize my notes for you here. Most of this information can be found in Printing the Talmud : a history of the earliest printed editions of the Talmud by Martin Heller.
329 |
330 | - The left and right columns of a Talmud page always encapsulate the center column at the top. They are always (when possible) four rows, followed by one "gap" row to give the center column some breathing space:
331 |
332 | 
333 |
334 | - After this, the columns extend downward for a length equal to the _shortest_ column. Whenever a column terminates, the remaining columns go on for another "gap" row.
335 |
336 | 
337 |
338 | - Columns expand or contract to fill the page width depending on which columns still have text. The following options are possible:
339 |
340 | 1. `████████ ████████`
341 | 2. `█████ █████ █████`
342 | 3. `█████ ███████████`
343 | 4. `███████████ █████`
344 | 5. `█████████████████`
345 |
346 | (The Talmud will sometimes vary this formula. And, the columns don't have such uniforms widths; see the above image.)
347 |
348 | - Additional marginalia is always in-line with the text it comments on.
349 |
350 | 
351 |
352 | - The Talmud uses different typefaces to indicate the author, most famously Rashi (for this reason, I decided that [XeLaTeX](https://www.overleaf.com/learn/latex/XeLaTeX) is a better tool than LaTeX for this project).
353 |
354 | 
355 |
356 | I found very little information on how typesetters knew how long any given column would be (information that Talmudifier requires) other than that it was hard to do. From this, I deduced that an experienced typesetter would simply have an eye for how to fit blocks of text on a page. I simulated this learned knowledge by adding expected row sizes to the recipe file derived from hundreds of simulated columns.
357 |
358 | ## 9. Known Limitations
359 |
360 | - The algorithm really struggles with different font sizes. I'm not sure how to handle this, other than requiring all columns to be the same font size. If you have a suggestion, please [email me](subalterngames@gmail.com).
361 | - The regex used is probably sub-optimal. Style markers _must_ be at the start and end of a word. Talmudifier is `ok with _this._` But `_not this_.`
362 |
363 | ## 10. Changelog
364 |
365 | ### v1.1.0
366 |
367 | - Replaced `sys.platform` with `platform.system()` in `PDFWriter` (the return value is more predictable).
368 | - Fixed: PDFWriter doesn't work on OS X.
369 | - Fixed: The example code in this README has an error.
--------------------------------------------------------------------------------
/talmudifier/talmudifier.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 | from typing import List
3 | from json import load
4 | from talmudifier.column import Column
5 | from talmudifier.util import to_camelcase
6 | import io
7 | from typing import Optional
8 | from talmudifier.pdf_writer import PDFWriter
9 | from talmudifier.citation import Citation
10 | from talmudifier.word import Word
11 | from talmudifier.style import Style
12 | from talmudifier.row_maker import RowMaker
13 | from talmudifier.paracol import Paracol
14 | import pkg_resources
15 |
16 |
17 | class Talmudifier:
18 | """
19 | Generate Talmud-esque page layouts, given markdown plaintext and a recipe JSON file.
20 | """
21 |
22 | def __init__(self, text_left: str, text_center: str, text_right: str, recipe_filename="default.json"):
23 | """
24 | :param text_left: The markdown text of the left column.
25 | :param text_center: The markdown text of the center column.
26 | :param text_right: The markdown text of the right column.
27 | :param recipe_filename: The filename of the recipe, located in recipes/
28 | """
29 |
30 | # Read the recipe.
31 | recipe_path = Path(f"recipes/{recipe_filename}")
32 | if not recipe_path.exists():
33 | print(f"Couldn't find recipe: {recipe_filename}; using default.json instead.")
34 | with io.open(str(recipe_path.resolve()), "rt", encoding="utf-8") as f:
35 | self.recipe = load(f)
36 |
37 | header_file = pkg_resources.resource_filename(__name__, "header.txt")
38 |
39 | # Read the preamble.
40 | assert Path(header_file).exists()
41 | with io.open(header_file, "rt", encoding="utf-8") as f:
42 | self.preamble = f.read()
43 |
44 | # Append font declarations.
45 | for col_name in ["left", "center", "right"]:
46 | self.preamble += "\n" + self._get_font_declaration(col_name)
47 |
48 | # Append a citation font declaration, if any.
49 | citation_declaration = self._get_citation_font_declaration(col_name)
50 | if citation_declaration is not None:
51 | self.preamble += "\n" + citation_declaration
52 |
53 | # Append color declarations.
54 | if "colors" in self.recipe:
55 | for color in self.recipe["colors"]:
56 | self.preamble += "\n\\definecolor{" + color + "}{HTML}{" + self.recipe["colors"][color] + "}"
57 |
58 | # Append the chapter command.
59 | assert "chapter" in self.recipe, "Chapter not found in recipe."
60 | assert "definition" in self.recipe["chapter"], "Chapter definition not found."
61 | self.preamble += "\n" + self.recipe["chapter"]["definition"]
62 |
63 | # Append additional definitions.
64 | if "misc_definitions" in self.recipe:
65 | for d in self.recipe["misc_definitions"]:
66 | self.preamble += "\n" + d
67 |
68 | # Create the PDF writer.
69 | self.writer = PDFWriter(self.preamble)
70 |
71 | self.left = self._get_column(text_left, "left")
72 | self.center = self._get_column(text_center, "center")
73 | self.right = self._get_column(text_right, "right")
74 |
75 | def _get_font_declaration(self, column_name: str) -> str:
76 | """
77 | Returns the font declaration in the recipe associated with the column.
78 |
79 | :param column_name: The name of the column (left, center, right).
80 | """
81 |
82 | # Check that all required keys are present.
83 | assert "fonts" in self.recipe, "No fonts found in recipe!"
84 | assert column_name in self.recipe["fonts"], f"No fonts found for: {column_name}"
85 | font_data = self.recipe["fonts"][column_name]
86 | for required_key in ["path", "ligatures", "regular_font"]:
87 | assert required_key in font_data, f"Required key in {column_name} not found: {required_key}"
88 |
89 | # Append a / to the path if needed.
90 | path = font_data['path']
91 | if not path.endswith("/"):
92 | path += "/"
93 |
94 | declaration = f"\\newfontfamily\\{column_name}font[Path={path}, Ligatures={font_data['ligatures']}"
95 |
96 | # Add styles to the declaration.
97 | for style_key in ["italic_font", "bold_font", "bold_italic_font"]:
98 | if style_key in font_data:
99 | declaration += f", {to_camelcase(style_key)}={font_data[style_key]}"
100 |
101 | # Finish the declaration and add the regular font.
102 | declaration += "]{" + font_data["regular_font"] + "}"
103 | return declaration
104 |
105 | def _get_citation_font_declaration(self, column_name: str) -> Optional[str]:
106 | """
107 | Returns the font declaration for a citation. May be none.
108 |
109 | :param column_name: The name of the column associated with the citation.
110 | """
111 |
112 | # Check that all required keys are present.
113 | assert "fonts" in self.recipe, "No fonts found in recipe!"
114 | assert column_name in self.recipe["fonts"], f"No fonts found for: {column_name}"
115 | font_data = self.recipe["fonts"][column_name]
116 |
117 | # Check if there is a citation.
118 | if "citation" not in font_data:
119 | return None
120 |
121 | citation_data = font_data["citation"]
122 | for required_key in ["path", "font", "font_command", "pattern"]:
123 | assert required_key in citation_data, f"Required key in citation {column_name} not found: {required_key}"
124 |
125 | # Append a / to the path if needed.
126 | path = citation_data['path']
127 | if not path.endswith("/"):
128 | path += "/"
129 |
130 | return "\\newfontfamily" + citation_data["font_command"] + "[Path=" + path + "]{" + citation_data["font"] + "}"
131 |
132 | def _get_column(self, text: str, column_name: str) -> Column:
133 | """
134 | Returns a column of words and font commands.
135 |
136 | :param text: The raw markdown text.
137 | :param column_name: The name of the column.
138 | """
139 |
140 | # Check that all required keys are present.
141 | assert "fonts" in self.recipe, "No fonts found in recipe!"
142 | assert column_name in self.recipe["fonts"], f"No fonts found for: {column_name}"
143 | font_data = self.recipe["fonts"][column_name]
144 |
145 | # Get the font-size command.
146 | if "skip" not in font_data or "size" not in font_data:
147 | font_size = -1
148 | font_skip = -1
149 | else:
150 | font_size = font_data["size"]
151 | font_skip = font_data["skip"]
152 |
153 | # Get the citation.
154 | if "citation" in font_data:
155 | citation = Citation(font_data["citation"])
156 | else:
157 | citation = None
158 |
159 | # Get the substitutions.
160 | if "substitutions" in font_data:
161 | substitutions = font_data["substitutions"]
162 | else:
163 | substitutions = None
164 |
165 | word_str = text.split(" ")
166 | words = []
167 |
168 | style = Style(False, False, False)
169 |
170 | for w in word_str:
171 | if w.startswith("**"):
172 | style.bold = True
173 | elif w.endswith("**"):
174 | style.bold = True
175 |
176 | if w.startswith("_"):
177 | style.italic = True
178 | elif w.endswith("_"):
179 | style.italic = True
180 |
181 | if w.startswith("_**") or w.startswith("**_"):
182 | style.bold = True
183 | style.italic = True
184 | elif w.endswith("_**") or w.endswith("**_"):
185 | style.bold = True
186 | style.italic = True
187 |
188 | if "" in w:
189 | style.underline = True
190 | elif "" in w:
191 | style.underline = True
192 |
193 | w_str = w.replace("*", "").replace("_", "").replace("", "").replace("", "")
194 |
195 | # Append the new word.
196 | words.append(Word(w_str, Style(style.bold, style.italic, style.underline), substitutions, citation))
197 |
198 | # Check if this was one word, e.g. **this** and apply styles again.
199 | if w.endswith("**"):
200 | style.bold = False
201 | if w.endswith("_"):
202 | style.italic = False
203 | if w.endswith("_**") or w.endswith("**_"):
204 | style.bold = False
205 | style.italic = False
206 | if "" in w:
207 | style.underline = False
208 |
209 | return Column(words, "\\" + column_name + "font", font_size, font_skip)
210 |
211 | def _get_expected_length(self, column_name: str, width: str, num_rows: int) -> int:
212 | """
213 | Get the expected length of a given number of rows of a given column of a given width.
214 |
215 | :param column_name: The name of the column.
216 | :param width: The width, e.g. half.
217 | :param num_rows: The number of rows.
218 | """
219 |
220 | # Check that all required keys are present.
221 | assert "fonts" in self.recipe, "No fonts found in recipe!"
222 | assert column_name in self.recipe["fonts"], f"No fonts found for: {column_name}"
223 |
224 | if "character_counts" not in self.recipe:
225 | return -1
226 | if width not in self.recipe["character_counts"]:
227 | return -1
228 |
229 | if column_name not in self.recipe["character_counts"][width]:
230 | return -1
231 |
232 | counts = self.recipe["character_counts"][width][column_name]
233 | if str(num_rows) in counts:
234 | return counts[str(num_rows)]
235 | else:
236 | if "1" in counts:
237 | return counts["1"] * num_rows
238 | else:
239 | return -1
240 |
241 | def _get_four_rows_left_right(self, column: Column, column_name: str) -> (str, Column):
242 | """
243 | Build four rows on the left or right.
244 |
245 | :param column: The column.
246 | :param column_name: The name of the column.
247 | :return:
248 | """
249 |
250 | # Build 4 rows of the left and right columns.
251 | rowmaker = RowMaker(True, False, True, column_name, self.writer)
252 | return rowmaker.get_text_of_length(column, 4, self._get_expected_length(column_name, "half", 4))
253 |
254 | def _get_one_row_left_right(self, column: Column, column_name: str) -> (str, Column):
255 | # Build 1 row of the left and right columns.
256 | rowmaker = RowMaker(True, True, True, column_name, self.writer)
257 | return rowmaker.get_text_of_length(column, 1, self._get_expected_length(column_name, "one_third", 1))
258 |
259 | def _get_column_width(self, target: str) -> str:
260 | if len(self.left.words) > 0:
261 | if len(self.center.words) > 0:
262 | if len(self.right.words) > 0:
263 | return "one_third"
264 | else:
265 | if target == "left":
266 | return "one_third"
267 | else:
268 | return "two_thirds"
269 | elif len(self.right.words) > 0:
270 | return "half"
271 | else:
272 | return ""
273 | elif len(self.center.words) > 0:
274 | if len(self.right.words) > 0:
275 | if target == "center":
276 | return "two_thirds"
277 | else:
278 | return "one_third"
279 | else:
280 | return ""
281 | else:
282 | return ""
283 |
284 | def _get_column_name(self, col: Column) -> str:
285 | """
286 | Returns the name of the column.
287 |
288 | :param col: The column.
289 | """
290 |
291 | if col == self.left:
292 | return "left"
293 | elif col == self.center:
294 | return "center"
295 | elif col == self.right:
296 | return "right"
297 | else:
298 | raise Exception("Couldn't match column.")
299 |
300 | def _get_columns_with_words(self) -> List[Column]:
301 | """
302 | Returns a list of columns that have words.
303 | """
304 |
305 | return [c for c in [self.left, self.center, self.right] if len(c.words) > 0]
306 |
307 | def _get_shortest(self) -> (Column, str, int, bool):
308 | """
309 | Returns which of my columns is the shortest, its name, the number of lines, and whether any has any lines.
310 | """
311 |
312 | # Check if any columns have any words.
313 | cols = self._get_columns_with_words()
314 | if len(cols) == 0:
315 | return None, "", 0, False
316 | elif len(cols) == 1:
317 | return cols[0], "", -1, True
318 |
319 | min_col = None
320 | min_lines = 10000000
321 | min_column_name = ""
322 |
323 | for col in cols:
324 | column_name = self._get_column_name(col)
325 |
326 | # Create the row maker.
327 | rowmaker = RowMaker(self.left in cols, self.center in cols, self.right in cols, column_name, self.writer)
328 |
329 | # Get the number of lines.
330 | num_lines = rowmaker.get_num_rows(col.get_tex(True))
331 |
332 | # Get the number of lines relative to the left column's font size.
333 | num_lines = int((col.font_size / self.left.font_size) * num_lines)
334 |
335 | if num_lines < min_lines:
336 | min_col = col
337 | min_lines = num_lines
338 | min_column_name = column_name
339 |
340 | return min_col, min_column_name, min_lines, True
341 |
342 | def get_tex(self) -> str:
343 | """
344 | Generate the body of text.
345 |
346 | 1. Create 4 rows on the left and right (width = one half).
347 | 2. Create 1 row on the left and right (width = one third).
348 | 3. Until the columns are all done: Find the shortest column and add it. Add other columns up to that length.
349 | """
350 |
351 | tex = ""
352 |
353 | # Get four row on the left and on the right.
354 | left_tex, self.left = self._get_four_rows_left_right(self.left, "left")
355 | right_tex, self.right = self._get_four_rows_left_right(self.right, "right")
356 |
357 | # Add the paracol environment.
358 | tex += "\n\\columnratio{0.5,0.5}\\begin{paracol}{2}\n\n" + left_tex + "\\switchcolumn" + right_tex + "\n\n\\end{paracol}\n\n"
359 |
360 | # Get four row on the left and on the right.
361 | left_tex, self.left = self._get_one_row_left_right(self.left, "left")
362 | right_tex, self.right = self._get_one_row_left_right(self.right, "right")
363 |
364 | # Add the paracol environment.
365 | three_col_begin = r"\columnratio{" + f"{Paracol.ONE_THIRD},{Paracol.ONE_THIRD},{Paracol.ONE_THIRD}" + "}" + r"\begin{paracol}{3}"
366 | tex += "\n" + three_col_begin + "\n\n" + left_tex + "\\switchcolumn[2]" + right_tex + "\n\n\\end{paracol}\n\n"
367 |
368 | done = False
369 | while not done:
370 | shortest_col, shortest_col_name, num_lines, any_lines = self._get_shortest()
371 | done = not any_lines
372 | if done:
373 | continue
374 |
375 | # Just fill the page with the last column's words.
376 | if num_lines == -1:
377 | tex += "\n\n\\columnratio{1}\\begin{paracol}{1}\n\n" + shortest_col.get_tex(True) + "\n\n\\end{paracol}\n\n"
378 | done = True
379 | continue
380 |
381 | # Start building the table.
382 | table = {shortest_col_name: shortest_col.get_tex(True)}
383 |
384 | assert self.left == shortest_col or self.center == shortest_col or self.right == shortest_col
385 |
386 | paracol = Paracol.get_paracol_header(len(self.left.words) > 0, len(self.center.words) > 0, len(self.right.words) > 0)
387 |
388 | # Fill the other columns, if possible.
389 | cols = self._get_columns_with_words()
390 | has_left = self.left in cols
391 | has_center = self.center in cols
392 | has_right = self.right in cols
393 |
394 | for i in range(len(cols)):
395 | if cols[i] == shortest_col:
396 | continue
397 | col_name = self._get_column_name(cols[i])
398 |
399 | # Build the column.
400 | rm = RowMaker(has_left, has_center, has_right, col_name, self.writer)
401 |
402 | # Set the target number of lines based on the font size relative to the left column.
403 | target_num_lines = int((self.left.font_size / cols[i].font_size) * num_lines + 1)
404 |
405 | col_tex, col = rm.get_text_of_length(cols[i],
406 | target_num_lines,
407 | self._get_expected_length(col_name,
408 | self._get_column_width(col_name),
409 | target_num_lines))
410 |
411 | # Update the table.
412 | table.update({col_name: col_tex})
413 |
414 | # Update my columns.
415 | if col_name == "left":
416 | self.left = col
417 | elif col_name == "center":
418 | self.center = col
419 | elif col_name == "right":
420 | self.right = col
421 | else:
422 | raise Exception()
423 |
424 | # Empty the shortest column.
425 | shortest_col.words = []
426 |
427 | # Build the paracol.
428 | for col_key in ["left", "center", "right"]:
429 | if col_key not in table:
430 | continue
431 | paracol += table[col_key]
432 | if col_key != "right":
433 | paracol += "\n\n\\switchcolumn\n\n"
434 |
435 | # End the paracol.
436 | paracol += "\n\n\\end{paracol}\n\n"
437 |
438 | # Add the paracol.
439 | tex += paracol
440 |
441 | return tex
442 |
443 | def get_chapter(self, title: str) -> str:
444 | """
445 | Returns the chapter command.
446 |
447 | :param title: The title of the chapter.
448 | """
449 |
450 | assert "chapter" in self.recipe, "Chapter not found in recipe."
451 | assert "command" in self.recipe["chapter"], "Chapter command not found in recipe."
452 | assert "numbering" in self.recipe["chapter"], "Chapter numbering not found in recipe."
453 |
454 | chapter = "\\chapter"
455 | if not self.recipe["chapter"]["numbering"]:
456 | chapter += "*"
457 | chapter += "{" + self.recipe["chapter"]["command"] + "{" + title + "}}"
458 | return chapter
459 |
460 | def create_pdf(self, chapter="", output_filename="output", print_tex=False) -> str:
461 | """
462 | Create a PDF. Generate the chapter and the body, and append them to the preamble. Returns the LaTeX string.
463 |
464 | :param chapter: If not empty, create the header here.
465 | :param output_filename: The name of the output file.
466 | :param print_tex: If true, print the LaTeX string to the console.
467 | """
468 |
469 | # Create the title.
470 | tex = self.get_chapter(chapter) + "\n" if chapter != "" else ""
471 | # Append the body.
472 | tex += self.get_tex()
473 |
474 | # Create the PDF.
475 | # Get the full LaTeX string, including the preamble.
476 | tex = self.writer.write(tex, output_filename)
477 | if print_tex:
478 | print(tex)
479 | return tex
480 |
--------------------------------------------------------------------------------