Ability to output GM sheets as EPUB files (without CSS for now).

This commit is contained in:
Mark Wolfman
2021-07-04 23:19:44 -05:00
parent aed864b093
commit 22dd8894bc
15 changed files with 1029 additions and 41 deletions
+1
View File
@@ -24,6 +24,7 @@ script:
- cd examples/ - cd examples/
- makesheets --debug - makesheets --debug
- makesheets --debug --fancy - makesheets --debug --fancy
- makesheets --debug --output-format=epub
- cd ../ - cd ../
after_success: after_success:
- coveralls - coveralls
+1 -1
View File
@@ -1 +1 @@
0.15.1 0.16.0
+139
View File
@@ -0,0 +1,139 @@
from typing import Mapping
from ebooklib import epub
from docutils import core
from sphinx.util.docstrings import prepare_docstring
from docutils.writers.html5_polyglot import Writer as HTMLWriter
from dungeonsheets.latex import dice_re
def create_epub(
chapters: Mapping,
title: str,
basename: str,
use_dnd_decorations: bool = False
):
"""Prepare an EPUB file from the list of chapters.
Parameters
==========
chapters
A mapping where the keys are chapter names (spines) and the
values are strings of HTML to be rendered as the chapter
contents.
basename
The basename for saving files (PDFs, etc). The resulting epub
file will be "{basename}.epub".
use_dnd_decorations
If true, style sheets will be included to produce D&D stylized
stat blocks, etc.
"""
# Create a new epub book
book = epub.EpubBook()
book.set_identifier('id123456')
book.set_title(title)
book.set_language('en')
# Create the separate chapters
html_chapters = []
for chap_title, content in chapters.items():
chap_fname = "{}.html".format(chap_title.replace(" ", "_").lower())
chapter = epub.EpubHtml(title=chap_title, file_name=chap_fname, lang="en")
chapter.set_content(content)
book.add_item(chapter)
html_chapters.append(chapter)
# Add the table of contents
book.toc = html_chapters
book.spine = ("nav", *html_chapters)
# add default NCX and Nav file
book.add_item(epub.EpubNcx())
book.add_item(epub.EpubNav())
# Save the file
epub_fname = f"{basename}.epub"
epub.write_epub(epub_fname, book)
def html_parts(
input_string,
source_path=None,
destination_path=None,
input_encoding="unicode",
doctitle=True,
initial_header_level=1,
):
"""
Given an input string, returns a dictionary of HTML document parts.
Dictionary keys are the names of parts, and values are Unicode strings;
encoding is up to the client.
Parameters:
- `input_string`: A multi-line text string; required.
- `source_path`: Path to the source file or object. Optional, but useful
for diagnostic output (system messages).
- `destination_path`: Path to the file or object which will receive the
output; optional. Used for determining relative paths (stylesheets,
source links, etc.).
- `input_encoding`: The encoding of `input_string`. If it is an encoded
8-bit string, provide the correct encoding. If it is a Unicode string,
use "unicode", the default.
- `doctitle`: Disable the promotion of a lone top-level section title to
document title (and subsequent section title to document subtitle
promotion); enabled by default.
- `initial_header_level`: The initial level for header elements (e.g. 1
for "<h1>").
"""
# Remove indentation, etc
input_string = "\n".join(prepare_docstring(input_string))
# Parse from rst to TeX
overrides = {
"input_encoding": input_encoding,
"doctitle_xform": doctitle,
"initial_header_level": initial_header_level,
}
writer = HTMLWriter()
parts = core.publish_parts(
source=input_string,
source_path=source_path,
destination_path=destination_path,
writer=writer,
settings_overrides=overrides,
)
return parts
def rst_to_html(rst, top_heading_level=0):
"""Basic markup of reST to HTML code.
The translation between reST headings and LaTeX headings is
modified by the *top_heading_level* parameter. A value of 0
(default) translates "# Heading" -> "<h1>{Heading}</h1>". A value
of 1 translates "# Heading" -> "<h2>{Heading}</h2>", etc.
Note: heading translation is currently broken.
Parameters
==========
rst
reStructured text input to be parsed.
top_heading_level : optional
The highest level heading that will be added to the HTML as
described above.
Returns
=======
html : str
The reST text parsed into HTML markup.
"""
if rst is None:
# No reST, so return an empty string
html = ""
else:
# Mark hit dice in monospace font
rst = dice_re.sub(r"``\1``", rst)
_html_parts = html_parts(rst)
html = _html_parts["body"]
return html
+3
View File
@@ -24,3 +24,6 @@ class JSONFormatError(RuntimeError):
class UnknownFileType(RuntimeError): class UnknownFileType(RuntimeError):
"""The input file does not match one of the known formats.""" """The input file does not match one of the known formats."""
class UnknownOutputFormat(RuntimeError):
"""The output format requested is not one of the known outputs."""
@@ -0,0 +1,70 @@
<h1 id="gm-monsters">Monsters</h1>
[% for monster in monsters|sort(attribute='name') %]
<h1 id="gm-monsters-[[ monster.name ]]">[[ monster.name ]]</h1>
[% if monster.description %]
<h2>[[ monster.description ]]</h2>
[% endif %]
<!-- Basic properties -->
<table>
<tr>
<th>Armor Class</th>
<th>Hit Points</th>
<th>Speed</th>
</tr>
<tr>
<td>[[ monster.armor_class ]]</td>
<td>[[ monster.hp_max ]] ([[ monster.hit_dice ]])</td>
<td>[[ monster.speed ]][% if monster.swim_speed %],
[[ monster.swim_speed ]] swim[% endif %][% if monster.fly_speed %],
[[ monster.fly_speed ]] fly[% endif %][% if monster.burrow_speed %],
[[ monster.burrow_speed ]] burrow[% endif %]</td>
</tr>
</table>
<!-- Attributes -->
<table>
<tr>
<th>STR</th>
<th>DEX</th>
<th>CON</th>
<th>INT</th>
<th>WIS</th>
<th>CHA</th>
</tr>
<tr>
<td>[[ monster.strength.value ]]</td>
<td>[[ monster.dexterity.value ]]</td>
<td>[[ monster.constitution.value ]]</td>
<td>[[ monster.intelligence.value ]]</td>
<td>[[ monster.wisdom.value ]]</td>
<td>[[ monster.charisma.value ]]</td>
</tr>
<tr>
<td>([[ monster.strength.modifier|mod_str ]])</td>
<td>([[ monster.dexterity.modifier|mod_str ]])</td>
<td>([[ monster.constitution.modifier|mod_str ]])</td>
<td>([[ monster.intelligence.modifier|mod_str ]])</td>
<td>([[ monster.wisdom.modifier|mod_str ]])</td>
<td>([[ monster.charisma.modifier|mod_str ]])</td>
</tr>
</table>
<dl>
[% if monster.skills != "" %]<dt>Skills:</dt><dd>[[ monster.skills ]]</dd>[% endif %]
<dt>Senses:</dt><dd>[% if monster.senses != "" %][[ monster.senses ]][% else %]--[% endif %]</dd>
<dt>Languages:</dt><dd>[% if monster.languages != "" %][[ monster.languages ]][% else %]--[% endif %]</dd>
[% if monster.damage_resistances != "" %]<dt>Damage Resistances:</dt><dd>[[ monster.damage_resistances ]]</dd>[% endif %]
[% if monster.damage_immunities != "" %]<dt>Damage Immunities:</dt><dd>[[ monster.damage_immunities ]]</dd>[% endif %]
[% if monster.damage_vulnerabilities != "" %]<dt>Damage Vulnerabilities:</dt><dd>[[ monster.damage_vulnerabilities ]]</dd>[% endif %]
[% if monster.condition_immunities != "" %]<dt>Condition Immunuties:</dt><dd>[[ monster.condition_immunities ]]</dd>[% endif %]
[% if monster.saving_throws != "" %]<dt>Saving Throws:</dt><dd>[[ monster.saving_throws ]]</dd>[% endif %]
<dt>Challenge:<dd>[[ monster.challenge_rating ]]</dd>
</dl>
[[ monster.__doc__ | rst_to_html(top_heading_level=2) ]]
[% endfor %]
@@ -0,0 +1,56 @@
[% if summary %]
<h1 id="gm-summary">Summary</h1>
[[ summary | rst_to_html ]]
[% endif %]
[% if party %]
<h1 id="gm-party">Party</h1>
<table>
<tr>
<th>&nbsp;</th>
<th>Str</th>
<th>Dex</th>
<th>Con</th>
<th>Int</th>
<th>Wis</th>
<th>Cha</th>
</tr>
[% for member in party %]
<tr>
<td>[[ member.name ]]</td>
<td>[[ member.strength.modifier | mod_str ]]</td>
<td>[[ member.dexterity.modifier | mod_str ]]</td>
<td>[[ member.constitution.modifier | mod_str ]]</td>
<td>[[ member.intelligence.modifier | mod_str ]]</td>
<td>[[ member.wisdom.modifier | mod_str ]]</td>
<td>[[ member.charisma.modifier | mod_str ]]</td>
</tr>
[% endfor %]
</table>
<table>
<tr>
<th>&nbsp;</th>
<th>AC</th>
<th>Pass. Per.</th>
<th>Spell DC</th>
</tr>
[% for member in party %]
<tr>
<td>[[ member.name[:28] ]]</td>
<td>[[ member.armor_class ]]</td>
<td>[[ member.perception + 10 ]]</td>
<td>[% for class in member.class_list %]
[% if class.spellcasting_ability %] [[ member.spell_save_dc(class) ]], [% endif %]
[% endfor %]
</td>
</tr>
[% endfor %]
</table>
[% endif %]
+3
View File
@@ -0,0 +1,3 @@
</body>
</html>
+6
View File
@@ -0,0 +1,6 @@
<!DOCTYPE html>
<html>
<head>
<title>My title</title>
</head>
<body>
@@ -0,0 +1,539 @@
<h1 id="gm-random-tables">Random Tables</h1>
[% if conjure_animals %]
<!-- https://the-azure-triskele.obsidianportal.com/wikis/conjure-animals-table -->
<h2 id="gm-random-tables-conjure-animals">Conjure Animals</h2>
<!-- Which category of beasts to summon -->
<table>
<tr>
<th>1d4</th>
<th>Number of Beasts</th>
</tr>
<tr>
<td>1</td>
<td>One beast of challenge rating 2</td>
</tr>
<tr>
<td>2</td>
<td>Two beasts of challenge rating 1</td>
</tr>
<tr>
<td>3</td>
<td>Four beasts of challenge rating 1/2</td>
</tr>
<tr>
<td>4</td>
<td>Eight beasts of challenge rating 1/4 or lower</td>
</tr>
</table>
<!-- CR2 Beasts -->
<table>
<tr>
<th>1d20</th>
<th>CR2 Beasts</th>
</tr>
<tr>
<td>1-2</td>
<td>Allosaurus</td>
</tr>
<tr>
<td>3-4</td>
<td>Giant Boar</td>
</tr>
<tr>
<td>5-6</td>
<td>Giant Constrictor Snake</td>
</tr>
<tr>
<td>7-8</td>
<td>Giant Elk</td>
</tr>
<tr>
<td>9-10</td>
<td>Hunter Shark</td>
</tr>
<tr>
<td>11</td>
<td>Plesiosaurus</td>
</tr>
<tr>
<td>12-13</td>
<td>Polar Bear</td>
</tr>
<tr>
<td>14-15</td>
<td>Rhinoceros</td>
</tr>
<tr>
<td>16-17</td>
<td>Saber-toothed Tiger</td>
</tr>
<tr>
<td>18-19</td>
<td>Swarm of Poisonous Snakes</td>
</tr>
<tr>
<td>20</td>
<td>Roll on CR 1 Beast Table</td>
</tr>
</table>
<!-- CR1 Beasts -->
<table>
<tr>
<th>1d12</th>
<th>Challenge Rating 1 Beasts</th>
</tr>
<tr>
<td>1</td>
<td>Brown Bear</td>
</tr>
<tr>
<td>2</td>
<td>Dire Wolf</td>
</tr>
<tr>
<td>3</td>
<td>Fire Snake</td>
</tr>
<tr>
<td>4</td>
<td>Giant Eagle</td>
</tr>
<tr>
<td>5</td>
<td>Giant Hyena</td>
</tr>
<tr>
<td>6</td>
<td>Giant Octopus</td>
</tr>
<tr>
<td>7</td>
<td>Giant Spider</td>
</tr>
<tr>
<td>8</td>
<td>Giant Toad</td>
</tr>
<tr>
<td>9</td>
<td>Giant Vulture</td>
</tr>
<tr>
<td>10</td>
<td>Lion</td>
</tr>
<tr>
<td>11</td>
<td>Tiger</td>
</tr>
<tr>
<td>12</td>
<td>Roll on CR ½ Beast Table</td>
</tr>
</table>
<table>
<tr>
<th>1d20</th>
<th>Challenge Rating ½ Beasts</th>
</tr>
<tr>
<td>1-2</td>
<td>Ape</td>
</tr>
<tr>
<td>3-4</td>
<td>Black Bear</td>
</tr>
<tr>
<td>5-6</td>
<td>Crocodile</td>
</tr>
<tr>
<td>7-8</td>
<td>Giant Goat</td>
</tr>
<tr>
<td>9-10</td>
<td>Giant Sea Horse</td>
</tr>
<tr>
<td>11-12</td>
<td>Giant Wasp</td>
</tr>
<tr>
<td>13-14</td>
<td>Reef Shark</td>
</tr>
<tr>
<td>15-16</td>
<td>Swarm of Insects (below)</td>
</tr>
<tr>
<td>17-18</td>
<td>Warhorse</td>
</tr>
<tr>
<td>19</td>
<td>Worg</td>
</tr>
<tr>
<td>20</td>
<td>Roll on Lesser Beast Menu Table</td>
</tr>
</table>
<!-- Swarm of insects (mostly for flavor) -->
<table>
<tr>
<th>1d6</th>
<th>Swarm of Insects</th>
</tr>
<tr>
<td>1</td>
<td>Ant</td>
</tr>
<tr>
<td>2</td>
<td>Beatles</td>
</tr>
<tr>
<td>3</td>
<td>Centipedes</td>
</tr>
<tr>
<td>4</td>
<td>Locusts</td>
</tr>
<tr>
<td>5</td>
<td>Spiders</td>
</tr>
<tr>
<td>6</td>
<td>Wasps</td>
</tr>
</table>
<!-- Challenge Rating 1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d6</th>
<th>CR ¼ and Lesser Beast Menu</th>
</tr>
<tr>
<td>1-2</td>
<td>Menu A</td>
</tr>
<tr>
<td>3-4</td>
<td>Menu B</td>
</tr>
<tr>
<td>5-6</td>
<td>Menu C</td>
</tr>
</table>
<!-- CR1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d20</th>
<th>Lesser Beast Menu A</th>
</tr>
<tr>
<td>1</td>
<td>Axe Beak</td>
</tr>
<tr>
<td>2</td>
<td>Baboon</td>
</tr>
<tr>
<td>3</td>
<td>Badger</td>
</tr>
<tr>
<td>4</td>
<td>Bat</td>
</tr>
<tr>
<td>5</td>
<td>Blood Hawk</td>
</tr>
<tr>
<td>6</td>
<td>Boar</td>
</tr>
<tr>
<td>7</td>
<td>Camel</td>
</tr>
<tr>
<td>8</td>
<td>Cat</td>
</tr>
<tr>
<td>9</td>
<td>Chicken*</td>
</tr>
<tr>
<td>10</td>
<td>Constrictor Snake</td>
</tr>
<tr>
<td>11</td>
<td>Crab</td>
</tr>
<tr>
<td>12</td>
<td>Deer</td>
</tr>
<tr>
<td>13</td>
<td>Draft Horse</td>
</tr>
<tr>
<td>14</td>
<td>Eagle</td>
</tr>
<tr>
<td>15</td>
<td>Elk</td>
</tr>
<tr>
<td>16</td>
<td>Flying Snake</td>
</tr>
<tr>
<td>17</td>
<td>Frog</td>
</tr>
<tr>
<td>18</td>
<td>Giant Badger</td>
</tr>
<tr>
<td>19</td>
<td>Giant Bat</td>
</tr>
<tr>
<td>20</td>
<td>Giant Centipede</td>
</tr>
</table>
<dl>
<dt>*Chicken:</dt>
<dd>Raven stats with Advantage on checks to wake
up targets instead of mimicry</dd>
</dl>
<!-- CR1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d20</th>
<th>Lesser Beast Menu B</th>
</tr>
<tr>
<td>1</td>
<td>Giant Crab </td>
</tr>
<tr>
<td>2</td>
<td>Giant Fire Beetle </td>
</tr>
<tr>
<td>3</td>
<td>Giant Frog</td>
</tr>
<tr>
<td>4</td>
<td>Giant Lizard</td>
</tr>
<tr>
<td>5</td>
<td>Giant Owl</td>
</tr>
<tr>
<td>6</td>
<td>Giant Poisonous Snake</td>
</tr>
<tr>
<td>7</td>
<td>Giant Rat</td>
</tr>
<tr>
<td>8</td>
<td>Giant Weasel</td>
</tr>
<tr>
<td>9</td>
<td>Giant Wolf Spider</td>
</tr>
<tr>
<td>10</td>
<td>Goat</td>
</tr>
<tr>
<td>11</td>
<td>Hawk</td>
</tr>
<tr>
<td>12</td>
<td>Hyena</td>
</tr>
<tr>
<td>13</td>
<td>Jackal</td>
</tr>
<tr>
<td>14</td>
<td>Lemur*</td>
</tr>
<tr>
<td>15</td>
<td>Lizard</td>
</tr>
<tr>
<td>16</td>
<td>Mastiff</td>
</tr>
<tr>
<td>17</td>
<td>Mule</td>
</tr>
<tr>
<td>18</td>
<td>Newt**</td>
</tr>
<tr>
<td>19</td>
<td>Octopus</td>
</tr>
<tr>
<td>20</td>
<td>Octopus, Cascadian Tree***</td>
</tr>
</table>
<dl>
<dt>*Lemur</dt>
<dd>Weasel stats with a common Climb speed instead of a
bite attack</dd>
<dt>**Newt:</dt>
<dd>Lizard stats with Amphibious instead of a bite
attack</dd>
<dt>***Octopus, Cascadian Tree:</dt>
<dd>Octopus stats with Amphibious
and a 10 ft land speed instead of camouflage</dd>
</dl>
<!-- CR1/4 and Lesser Beasts -->
<table>
<tr>
<th>1d20</th>
<th>Lesser Beast Menu C</th>
</tr>
<tr>
<td>1</td>
<td>Owl</td>
</tr>
<tr>
<td>2</td>
<td>Panther</td>
</tr>
<tr>
<td>3</td>
<td>Poisonous Snake</td>
</tr>
<tr>
<td>4</td>
<td>Pony</td>
</tr>
<tr>
<td>5</td>
<td>Pteranodon</td>
</tr>
<tr>
<td>6</td>
<td>Quipper</td>
</tr>
<tr>
<td>7</td>
<td>Rat</td>
</tr>
<tr>
<td>8</td>
<td>Raven</td>
</tr>
<tr>
<td>9</td>
<td>Riding Horse</td>
</tr>
<tr>
<td>10</td>
<td>Scorpion</td>
</tr>
<tr>
<td>11</td>
<td>Sea Horse</td>
</tr>
<tr>
<td>12</td>
<td>Shocker Lizard*</td>
</tr>
<tr>
<td>13</td>
<td>Spider</td>
</tr>
<tr>
<td>14</td>
<td>Swarm of Bats</td>
</tr>
<tr>
<td>15</td>
<td>Swarm of Rats</td>
</tr>
<tr>
<td>16</td>
<td>Swarm of Ravens</td>
</tr>
<tr>
<td>17</td>
<td>Turtle**</td>
</tr>
<tr>
<td>18</td>
<td>Vulture</td>
</tr>
<tr>
<td>19</td>
<td>Weasel</td>
</tr>
<tr>
<td>20</td>
<td>Wolf</td>
</tr>
</table>
<dl>
<dt>*Shocker Lizard:</dt>
<dd>Lizard stats with Static Electricity ranged attack of 1d6
Electricity damage Close/Medium.</dd>
<dt>**Turtle:</dt>
<dd>Lizard stats with 14 natural armor and no climb speed.</dd>
</dl>
[% endif %]
+1 -1
View File
@@ -46,7 +46,7 @@ def _remove_temp_files(basename_):
filename.unlink() filename.unlink()
def create_latex_pdf(tex, basename, keep_temp_files=False, use_dnd_decorations=False): def create_latex_pdf(tex: str, basename: str, keep_temp_files: bool=False, use_dnd_decorations: bool=False):
# Create tex document # Create tex document
tex_file = f"{basename}.tex" tex_file = f"{basename}.tex"
with open(tex_file, mode="w", encoding="utf-8") as f: with open(tex_file, mode="w", encoding="utf-8") as f:
+69 -30
View File
@@ -13,7 +13,7 @@ from typing import Union, Sequence, Optional
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
from dungeonsheets import character as _char, exceptions, readers, latex, monsters from dungeonsheets import character as _char, exceptions, readers, latex, epub, monsters
from dungeonsheets.stats import mod_str from dungeonsheets.stats import mod_str
from dungeonsheets.content_registry import find_content from dungeonsheets.content_registry import find_content
from dungeonsheets.fill_pdf_template import ( from dungeonsheets.fill_pdf_template import (
@@ -49,6 +49,7 @@ jinja_env = Environment(
variable_end_string="]]", variable_end_string="]]",
) )
jinja_env.filters["rst_to_latex"] = latex.rst_to_latex jinja_env.filters["rst_to_latex"] = latex.rst_to_latex
jinja_env.filters["rst_to_html"] = epub.rst_to_html
jinja_env.filters["mod_str"] = mod_str jinja_env.filters["mod_str"] = mod_str
@@ -83,22 +84,24 @@ def create_magic_items_tex(
return template.render(character=character, use_dnd_decorations=use_dnd_decorations) return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_monsters_tex( def create_monsters_content(
monsters: Sequence[Union[monsters.Monster, str]], monsters: Sequence[Union[monsters.Monster, str]],
suffix: str,
use_dnd_decorations: bool = False, use_dnd_decorations: bool = False,
) -> str: ) -> str:
# Convert strings to Monster objects # Convert strings to Monster objects
template = jinja_env.get_template("monsters_template.tex") template = jinja_env.get_template(f"monsters_template.{suffix}")
return template.render(monsters=monsters, use_dnd_decorations=use_dnd_decorations) return template.render(monsters=monsters, use_dnd_decorations=use_dnd_decorations)
def create_party_summary_tex( def create_party_summary_content(
party: Sequence[Entity], party: Sequence[Entity],
summary_rst: str, summary_rst: str,
suffix: str,
use_dnd_decorations: bool = False, use_dnd_decorations: bool = False,
) -> str: ) -> str:
log.debug("Preparing summary table for party: %s", party) log.debug("Preparing summary table for party: %s", party)
template = jinja_env.get_template("party_summary_template.tex") template = jinja_env.get_template(f"party_summary_template.{suffix}")
return template.render( return template.render(
party=party, summary=summary_rst, use_dnd_decorations=use_dnd_decorations party=party, summary=summary_rst, use_dnd_decorations=use_dnd_decorations
) )
@@ -130,11 +133,12 @@ def create_druid_shapes_tex(
return template.render(character=character, use_dnd_decorations=use_dnd_decorations) return template.render(character=character, use_dnd_decorations=use_dnd_decorations)
def create_random_tables_tex( def create_random_tables_content(
conjure_animals: bool, conjure_animals: bool,
suffix: str,
use_dnd_decorations: bool = False, use_dnd_decorations: bool = False,
) -> str: ) -> str:
template = jinja_env.get_template("random_tables_template.tex") template = jinja_env.get_template(f"random_tables_template.{suffix}")
return template.render(conjure_animals=conjure_animals, return template.render(conjure_animals=conjure_animals,
use_dnd_decorations=use_dnd_decorations) use_dnd_decorations=use_dnd_decorations)
@@ -142,6 +146,7 @@ def create_random_tables_tex(
def make_sheet( def make_sheet(
sheet_file: File, sheet_file: File,
flatten: bool = False, flatten: bool = False,
output_format: str = "pdf",
fancy_decorations: bool = False, fancy_decorations: bool = False,
debug: bool = False, debug: bool = False,
): ):
@@ -153,6 +158,8 @@ def make_sheet(
flatten : bool, optional flatten : bool, optional
If true, the resulting PDF will look better and won't be If true, the resulting PDF will look better and won't be
fillable form. fillable form.
output_format
Either "pdf" or "epub" to generate a PDF file or an EPUB file.
fancy_decorations : bool, optional fancy_decorations : bool, optional
Use fancy page layout and decorations for extra sheets, namely Use fancy page layout and decorations for extra sheets, namely
the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template. the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template.
@@ -167,6 +174,7 @@ def make_sheet(
if sheet_props.get("sheet_type", "") == "gm": if sheet_props.get("sheet_type", "") == "gm":
ret = make_gm_sheet( ret = make_gm_sheet(
gm_file=sheet_file, gm_file=sheet_file,
output_format=output_format,
fancy_decorations=fancy_decorations, fancy_decorations=fancy_decorations,
debug=debug, debug=debug,
) )
@@ -180,8 +188,15 @@ def make_sheet(
return ret return ret
format_suffixes = {
"pdf": "tex",
"epub": "html",
}
def make_gm_sheet( def make_gm_sheet(
gm_file: Union[str, Path], gm_file: Union[str, Path],
output_format: str = "pdf",
fancy_decorations: bool = False, fancy_decorations: bool = False,
debug: bool = False, debug: bool = False,
): ):
@@ -191,6 +206,8 @@ def make_gm_sheet(
---------- ----------
gm_file gm_file
The file with the gm_sheet definitions. The file with the gm_sheet definitions.
output_format
Either "pdf" or "epub" to generate a PDF file or an EPUB file.
fancy_decorations fancy_decorations
Use fancy page layout and decorations for extra sheets, namely Use fancy page layout and decorations for extra sheets, namely
the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template. the dnd style file: https://github.com/rpgtex/DND-5e-LaTeX-Template.
@@ -203,8 +220,9 @@ def make_gm_sheet(
basename = gm_file.stem basename = gm_file.stem
gm_props = readers.read_sheet_file(gm_file) gm_props = readers.read_sheet_file(gm_file)
# Create the intro tex # Create the intro tex
tex = [ content_suffix = format_suffixes[output_format]
jinja_env.get_template("preamble.tex").render( content = [
jinja_env.get_template(f"preamble.{content_suffix}").render(
use_dnd_decorations=fancy_decorations, use_dnd_decorations=fancy_decorations,
title=gm_props.pop("session_title", "GM Session Notes"), title=gm_props.pop("session_title", "GM Session Notes"),
) )
@@ -222,9 +240,9 @@ def make_gm_sheet(
member = _char.Character.load(character_props) member = _char.Character.load(character_props)
party.append(member) party.append(member)
summary = gm_props.pop("summary", "") summary = gm_props.pop("summary", "")
tex.append( content.append(
create_party_summary_tex( create_party_summary_content(
party, summary_rst=summary, use_dnd_decorations=fancy_decorations party, summary_rst=summary, suffix=content_suffix, use_dnd_decorations=fancy_decorations
) )
) )
# Add the monsters # Add the monsters
@@ -244,20 +262,21 @@ def make_gm_sheet(
new_monster = MyMonster() new_monster = MyMonster()
monsters_.append(new_monster) monsters_.append(new_monster)
if len(monsters_) > 0: if len(monsters_) > 0:
tex.append( content.append(
create_monsters_tex(monsters_, use_dnd_decorations=fancy_decorations) create_monsters_content(monsters_, suffix=content_suffix, use_dnd_decorations=fancy_decorations)
) )
# Add the random tables # Add the random tables
random_tables = [s.replace(" ", "_").lower() for s in gm_props.pop("random_tables", [])] random_tables = [s.replace(" ", "_").lower() for s in gm_props.pop("random_tables", [])]
tex.append( content.append(
create_random_tables_tex( create_random_tables_content(
conjure_animals=("conjure_animals" in random_tables), conjure_animals=("conjure_animals" in random_tables),
suffix=content_suffix,
use_dnd_decorations=fancy_decorations, use_dnd_decorations=fancy_decorations,
) )
) )
# Add the closing TeX # Add the closing TeX
tex.append( content.append(
jinja_env.get_template("postamble.tex").render( jinja_env.get_template(f"postamble.{format_suffixes[output_format]}").render(
use_dnd_decorations=fancy_decorations use_dnd_decorations=fancy_decorations
) )
) )
@@ -268,17 +287,30 @@ def make_gm_sheet(
msg = f"Unhandled attributes in '{str(gm_file)}': {','.join(gm_props.keys())}" msg = f"Unhandled attributes in '{str(gm_file)}': {','.join(gm_props.keys())}"
log.warn(msg) log.warn(msg)
warnings.warn(msg) warnings.warn(msg)
# Typeset combined LaTeX file # Produce the combined output depending on the format requested
try: if output_format == "pdf":
if len(tex) > 2: # Typeset combined LaTeX file
latex.create_latex_pdf( try:
tex="".join(tex), if len(content) > 2:
basename=basename, latex.create_latex_pdf(
keep_temp_files=debug, tex="".join(content),
use_dnd_decorations=fancy_decorations, basename=basename,
) keep_temp_files=debug,
except exceptions.LatexNotFoundError: use_dnd_decorations=fancy_decorations,
log.warning(f"``pdflatex`` not available. Skipping {basename}") )
except exceptions.LatexNotFoundError:
log.warning(f"``pdflatex`` not available. Skipping {basename}")
elif output_format == "epub":
epub.create_epub(
chapters={"GM Sheet": "".join(content)},
basename=basename,
title=gm_props.get("session_title", f"GM Notes: {basename}"),
use_dnd_decorations=fancy_decorations,
)
else:
raise exceptions.UnknownOutputFormat(
f"Unknown output format requested: {output_format}. Valid options are: 'pdf', 'epub'"
)
def make_character_sheet( def make_character_sheet(
@@ -435,6 +467,7 @@ def _build(filename, args) -> int:
make_sheet( make_sheet(
sheet_file=filename, sheet_file=filename,
flatten=(not args.editable), flatten=(not args.editable),
output_format=args.output_format,
debug=args.debug, debug=args.debug,
fancy_decorations=args.fancy_decorations, fancy_decorations=args.fancy_decorations,
) )
@@ -484,6 +517,12 @@ def main(args=None):
"(experimental, requires https://github.com/rpgtex/DND-5e-LaTeX-Template)" "(experimental, requires https://github.com/rpgtex/DND-5e-LaTeX-Template)"
), ),
) )
parser.add_argument(
"--output-format", "-o",
help="Specify the output format for the sheets.",
choices=["pdf", "epub"],
default="pdf",
)
parser.add_argument( parser.add_argument(
"--debug", "--debug",
"-d", "-d",
@@ -529,7 +568,7 @@ def main(args=None):
# Process the requested files # Process the requested files
if args.debug: if args.debug:
for filename in filenames: for filename in filenames:
print("building") log.debug("building")
_build(filename, args) _build(filename, args)
else: else:
with Pool(cpu_count()) as p: with Pool(cpu_count()) as p:
+1
View File
@@ -4,3 +4,4 @@ npyscreen
jinja2 jinja2
sphinx sphinx
pdfrw pdfrw
EbookLib
+1 -1
View File
@@ -25,7 +25,7 @@ setup(name='dungeonsheets',
'../VERSION'] '../VERSION']
}, },
install_requires=[ install_requires=[
'fdfgen', 'npyscreen', 'jinja2', 'pdfrw', 'sphinx', 'fdfgen', 'npyscreen', 'jinja2', 'pdfrw', 'sphinx', 'EbookLib',
], ],
entry_points={ entry_points={
'console_scripts': [ 'console_scripts': [
+114
View File
@@ -0,0 +1,114 @@
import unittest
from dungeonsheets import spells, features, epub
class MarkdownTestCase(unittest.TestCase):
"""Check that conversion of markdown formats to LaTeX code works
correctly."""
def test_rst_bold(self):
text = epub.rst_to_html("**hello**")
self.assertEqual(text, "<p><strong>hello</strong></p>\n")
def test_hit_dice(self):
text = epub.rst_to_html("1d6+3")
self.assertEqual(text.strip("\n"), '<p><span class="docutils literal">1d6+3</span></p>')
def test_no_text(self):
text = epub.rst_to_html(None)
self.assertEqual(text, "")
def test_verbatim(self):
text = epub.rst_to_html("``hello, world``")
self.assertIn('<p><span class="docutils literal">hello, world</span></p>', text)
def test_literal_backslash(self):
text = epub.rst_to_html(r"\\")
self.assertEqual(r"<p>\</p>", text.strip("\n"))
@unittest.skip(
"Headings are all screwed up because it treats them as the document title"
)
def test_headings(self):
# Simple heading by itself
text = epub.rst_to_html("Hello, world\n------------\n\nGoodbye, world")
self.assertEqual("\\section*{Hello, world}\n", text)
# Simple heading with leading whitespace
text = epub.rst_to_html(" Hello, world\n ============\n")
self.assertEqual("\\section*{Hello, world}\n", text)
# Heading with text after it
text = epub.rst_to_html("Hello, world\n============\n\nThis is some text")
self.assertEqual("\\section*{Hello, world}\n\nThis is some text", text)
# Heading with text before it
text = epub.rst_to_html("This is a paragraph\n\nHello, world\n============\n")
self.assertEqual("This is a paragraph\n\n\\section*{Hello, world}\n", text)
# Check that levels of headings are parsed appropriately
text = epub.rst_to_html("Hello, world\n^^^^^^^^^^^^\n")
self.assertEqual("\\subsubsection*{Hello, world}\n", text)
text = epub.rst_to_html("Hello, world\n^^^^^^^^^^^^\n", top_heading_level=3)
self.assertEqual("\\subparagraph*{Hello, world}\n", text)
# This is a bad heading missing with all the underline on one line
text = epub.rst_to_html("Hello, world^^^^^^^^^^^^\n")
self.assertEqual("Hello, world\\^\\^\\^\\^\\^\\^\\^\\^\\^\\^\\^\\^\n", text)
def test_bullet_list(self):
tex = epub.rst_to_html("\n- Hello\n- World\n\n")
expected_tex = '<ul class="simple">\n<li><p>Hello</p></li>\n<li><p>World</p></li>\n</ul>'
self.assertEqual(expected_tex, tex.strip("\n"))
# Other bullet characters
tex = epub.rst_to_html("\n* Hello\n* World\n\n")
self.assertEqual(expected_tex, tex.strip("\n"))
tex = epub.rst_to_html("\n+ Hello\n+ World\n\n")
self.assertEqual(expected_tex, tex.strip("\n"))
# A real list taken from a docstring
real_list = """
- Secondhand (you have heard of the target) - +5
- Firsthand (you have met the target) - +0
- Familiar (you know the target well) - -5
"""
tex = epub.rst_to_html(real_list)
self.assertIn('<ul class="simple">', tex)
def test_multiline_bullet_list(self):
md_list = """
- Secondhand (you have heard
of the target) - +5
- Firsthand (you have met
the target) - +0
- Familiar (you know the target
well) - -5
"""
tex = epub.rst_to_html(md_list)
self.assertIn('<ul class="simple">', tex)
def test_simple_table(self):
table_rst = """
===== ===== =======
A B A and B
===== ===== =======
False False False
True False False
False True False
True True True
===== ===== =======
"""
tex = epub.rst_to_html(table_rst)
# Check begin/end environment is fixed
self.assertIn("<table>", tex)
def test_rst_all_spells(self):
for spell in spells.all_spells():
tex = epub.rst_to_html(spell.__doc__)
self.assertNotIn(
"DUadmonition", tex, f"spell {spell} is not valid reStructured text"
)
def test_rst_all_features(self):
for feature in features.all_features():
tex = epub.rst_to_html(feature.__doc__)
self.assertNotIn(
"DUadmonition", tex, f"feature {feature} is not valid reStructured text"
)
+25 -8
View File
@@ -52,6 +52,21 @@ class MakeSheetsTestCase(unittest.TestCase):
f"GM PDF ({self.gm_pdf.resolve()}) not created.") f"GM PDF ({self.gm_pdf.resolve()}) not created.")
class EpubOutputTestCase(unittest.TestCase):
gm_epub = Path(f"{GMFILE.stem}.epub").resolve()
def tearDown(self):
for f in [self.gm_epub]:
if f.exists():
f.unlink()
def test_file_created(self):
# Check that a file is created once the function is run
# self.assertFalse(os.path.exists(pdf_name), f'{pdf_name} already exists.')
make_sheets.make_gm_sheet(gm_file=GMFILE, output_format="epub")
self.assertTrue(self.gm_epub.exists(), f"{self.gm_epub} not created.")
class PdfOutputTestCase(unittest.TestCase): class PdfOutputTestCase(unittest.TestCase):
basename = "clara" basename = "clara"
@@ -142,11 +157,11 @@ class TexCreatorTestCase(unittest.TestCase):
def test_create_monsters_tex(self): def test_create_monsters_tex(self):
monsters_ = [monsters.GiantEagle()] monsters_ = [monsters.GiantEagle()]
tex = make_sheets.create_monsters_tex(monsters=monsters_) tex = make_sheets.create_monsters_content(monsters=monsters_, suffix="tex")
self.assertIn(r"Giant Eagle", tex) self.assertIn(r"Giant Eagle", tex)
# Check extended properties # Check extended properties
monsters_ = [VashtaNerada()] monsters_ = [VashtaNerada()]
tex = make_sheets.create_monsters_tex(monsters=monsters_) tex = make_sheets.create_monsters_content(monsters=monsters_, suffix="tex")
self.assertIn(r"Vashta Nerada", tex) self.assertIn(r"Vashta Nerada", tex)
self.assertIn(r"35", tex) self.assertIn(r"35", tex)
self.assertIn(r"45 fly", tex) self.assertIn(r"45 fly", tex)
@@ -162,8 +177,9 @@ class TexCreatorTestCase(unittest.TestCase):
self.assertIn(r"Languages:", tex) self.assertIn(r"Languages:", tex)
self.assertIn(r"Skills:", tex) self.assertIn(r"Skills:", tex)
# Check fancy extended properties # Check fancy extended properties
tex = make_sheets.create_monsters_tex(monsters=monsters_, tex = make_sheets.create_monsters_content(monsters=monsters_,
use_dnd_decorations=True) suffix="tex",
use_dnd_decorations=True)
self.assertIn(r"Vashta Nerada", tex) self.assertIn(r"Vashta Nerada", tex)
self.assertIn(r"35 ft.", tex) self.assertIn(r"35 ft.", tex)
self.assertIn(r"45 ft. fly", tex) self.assertIn(r"45 ft. fly", tex)
@@ -171,22 +187,23 @@ class TexCreatorTestCase(unittest.TestCase):
self.assertIn(r"65 ft. burrow", tex) self.assertIn(r"65 ft. burrow", tex)
self.assertIn(r"petrified", tex) self.assertIn(r"petrified", tex)
self.assertIn(r"saving-throws = {Dex +8}", tex) self.assertIn(r"saving-throws = {Dex +8}", tex)
def test_create_party_summary_tex(self): def test_create_party_summary_tex(self):
char = self.new_character() char = self.new_character()
tex = make_sheets.create_party_summary_tex(party=[char], summary_rst="") tex = make_sheets.create_party_summary_content(party=[char], suffix="tex", summary_rst="")
self.assertIn(r"\section*{Party}", tex) self.assertIn(r"\section*{Party}", tex)
self.assertIn(char.name, tex) self.assertIn(char.name, tex)
def test_create_summary_tex(self): def test_create_summary_tex(self):
rst = "The party's create *adventure*." rst = "The party's create *adventure*."
tex = make_sheets.create_party_summary_tex(party=[], summary_rst=rst) tex = make_sheets.create_party_summary_content(party=[], suffix="tex", summary_rst=rst)
self.assertIn(r"\section*{Summary}", tex) self.assertIn(r"\section*{Summary}", tex)
# Check that the RST is parsed # Check that the RST is parsed
self.assertIn(r"\emph{adventure}", tex) self.assertIn(r"\emph{adventure}", tex)
def test_random_tables_tex(self): def test_random_tables_tex(self):
tex = make_sheets.create_random_tables_tex( tex = make_sheets.create_random_tables_content(
suffix="tex",
conjure_animals=True, conjure_animals=True,
) )
self.assertIn(r"\subsection*{Conjure Animals}", tex) self.assertIn(r"\subsection*{Conjure Animals}", tex)