mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Ability to output GM sheets as EPUB files (without CSS for now).
This commit is contained in:
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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> </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> </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 %]
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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 %]
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -4,3 +4,4 @@ npyscreen
|
|||||||
jinja2
|
jinja2
|
||||||
sphinx
|
sphinx
|
||||||
pdfrw
|
pdfrw
|
||||||
|
EbookLib
|
||||||
|
|||||||
@@ -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': [
|
||||||
|
|||||||
@@ -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"
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user