mirror of
https://github.com/Threnklyn/dungeon-sheets.git
synced 2026-05-18 20:23:27 +02:00
Parsing of reST headings into LaTeX \section*{}, etc.
This commit is contained in:
+34
@@ -83,3 +83,37 @@ https://github.com/rpgtex/DND-5e-LaTeX-Template
|
||||
If you'd like a **step-by-step walkthrough** for creating a new
|
||||
character, just run ``create-character`` from a command line and a
|
||||
helpful menu system will take care of the basics for you.
|
||||
|
||||
|
||||
Content Descriptions
|
||||
====================
|
||||
|
||||
The descriptions of content elements (e.g. classes, spells, etc.) are
|
||||
included in docstrings. The descriptions should ideally conform to
|
||||
reStructured text. This allows certain formatting elements to be
|
||||
properly parsed and rendered into LaTeX::
|
||||
|
||||
class Scrying(Spell):
|
||||
"""You can see and hear a particular creature you choose that is on
|
||||
the same plane of existence as you. The target must make a W isdom
|
||||
saving throw, which is modified by how well you know the target
|
||||
and the sort of physical connection you have to it. If a target
|
||||
knows you're casting this spell, it can fail the saving throw
|
||||
voluntarily if it wants to be observed.
|
||||
|
||||
Knowledge - Save Modifier
|
||||
-------------------------
|
||||
- Secondhand (you have heard of the target) - +5
|
||||
- Firsthand (you have met the target) - +0
|
||||
- Familiar (you know the target well) - -5
|
||||
|
||||
Connection - Save Modifier
|
||||
--------------------------
|
||||
- Likeness or picture - -2
|
||||
- Possession or garment - -4
|
||||
- Body part, lock of hair, bit of nail, or the like - -10
|
||||
|
||||
"""
|
||||
name = "Scrying"
|
||||
level = 5
|
||||
...
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
\vspace{0.2cm}
|
||||
|
||||
[[ shape.__doc__ | rst_to_latex ]]
|
||||
[[ shape.__doc__ | rst_to_latex(top_heading_level=2) ]]
|
||||
|
||||
} %\color
|
||||
[% endfor %]
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
|
||||
\subsection*{Subclass: [[ sc.name ]]}
|
||||
|
||||
[[ sc.__doc__|rst_to_latex ]]
|
||||
[[ sc.__doc__ | rst_to_latex(top_heading_level=2) ]]
|
||||
|
||||
[% endfor %]
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
|
||||
[% endif %]%
|
||||
|
||||
[[ inf.__doc__|rst_to_latex ]]
|
||||
[[ inf.__doc__ | rst_to_latex(top_heading_level=2) ]]
|
||||
|
||||
[% endfor %]
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
\end{description}
|
||||
\vspace{\zerosep}
|
||||
|
||||
[[ spl.__doc__|rst_to_latex ]]
|
||||
[[ spl.__doc__ | rst_to_latex(top_heading_level=1) ]]
|
||||
|
||||
[% endfor %]
|
||||
|
||||
|
||||
@@ -25,22 +25,92 @@ character sheet."""
|
||||
bold_re = re.compile(r'\*\*([^*]+)\*\*')
|
||||
it_re = re.compile(r'\*([^*]+)\*')
|
||||
verb_re = re.compile(r'``([^`]+)``')
|
||||
heading_re = re.compile(r'^[ \t\r\f\v]*(.+)\n\s*([-=\^]+)$', flags=re.MULTILINE)
|
||||
# A dice string, with optinal backticks: ``1d6 + 3``
|
||||
dice_re = re.compile(r'`*(\d+d\d+(?:\s*\+\s*\d+)?)`*')
|
||||
|
||||
|
||||
def rst_to_latex(rst):
|
||||
"""Basic markup of RST to LaTeX code."""
|
||||
def _parse_rst_headings(rst):
|
||||
"""Read headings in reST and iterate.
|
||||
|
||||
Yields
|
||||
======
|
||||
heading_rst : str
|
||||
The matching reST heading found in the input text.
|
||||
heading : str
|
||||
The text of the heading with underlining removed.
|
||||
level : int
|
||||
How deep the heading is: 0 is top-level, 1 is next level down,
|
||||
etc.
|
||||
|
||||
"""
|
||||
heading_levels = {
|
||||
'=': 0,
|
||||
'-': 1,
|
||||
'^': 2,
|
||||
}
|
||||
for match in heading_re.finditer(rst):
|
||||
heading_rst = match.group(0)
|
||||
heading, underline = match.groups()
|
||||
# Check for valid heading
|
||||
if len(underline) < len(heading):
|
||||
log.debug("Skipping malformed reST heading: '%s\n%s'", heading, underline)
|
||||
continue
|
||||
if len(set(underline)) > 1:
|
||||
log.debug("Skipping malformed reST heading: '%s\n%s'", heading, underline)
|
||||
continue
|
||||
# Valid heading, so determine how many levels deep it is
|
||||
level = heading_levels[underline[0]]
|
||||
yield heading_rst, heading, level
|
||||
|
||||
|
||||
def rst_to_latex(rst, top_heading_level=0):
|
||||
"""Basic markup of reST to LaTeX code.
|
||||
|
||||
The translation between reST headings and LaTeX headings is
|
||||
modified by the *top_heading_level* parameter. A value of 0
|
||||
(default) translates "# Heading" -> "\section{Heading}". A value
|
||||
of 1 translates "# Heading" -> "\subsection{Heading}", etc.
|
||||
|
||||
Parameters
|
||||
==========
|
||||
rst
|
||||
reStructured text input to be parsed.
|
||||
top_heading_level : optional
|
||||
The highest level heading that will be added to the LaTeX as
|
||||
described above.
|
||||
|
||||
Returns
|
||||
=======
|
||||
tex : str
|
||||
The reST text parsed into LaTeX markup.
|
||||
|
||||
"""
|
||||
heading_latex = {
|
||||
0: 'section*',
|
||||
1: 'subsection*',
|
||||
2: 'subsubsection*',
|
||||
3: 'paragraph*',
|
||||
4: 'subparagraph*',
|
||||
}
|
||||
if rst is None:
|
||||
# No reST, so return an empty string
|
||||
tex = ""
|
||||
else:
|
||||
tex = rst
|
||||
# Allow literal backslashes
|
||||
for c in ['\\']:
|
||||
tex = tex.replace(c, '\\' + c)
|
||||
# Inline text formatting
|
||||
tex = bold_re.sub(r'\\textbf{\1}', tex)
|
||||
tex = it_re.sub(r'\\textit{\1}', tex)
|
||||
tex = verb_re.sub(r'\\begin{verbatim}{\1}\\end{verbatim}', tex)
|
||||
tex = dice_re.sub(r'\\texttt{\1}', tex)
|
||||
# Headings
|
||||
for heading_rst, heading, heading_level in _parse_rst_headings(tex):
|
||||
heading_level = min(heading_level + top_heading_level, max(heading_latex.keys()))
|
||||
tex = tex.replace(heading_rst, f"\\{heading_latex[heading_level]}{{{heading}}}")
|
||||
# Escape any remaining characters that have meaning in LaTeX
|
||||
for c in ['#', '$', '%', '&', '~', '_', '^']:
|
||||
tex = tex.replace(c, '\\' + c)
|
||||
|
||||
|
||||
@@ -73,14 +73,14 @@ class AcidArrow(Spell):
|
||||
|
||||
|
||||
class AcidSplash(Spell):
|
||||
"""You hurl a bubble of acid.
|
||||
Choose one creature within range, or choose two
|
||||
creatures within range that are within 5 feet of each other. A target must
|
||||
succeed on a Dexterity saving throw or take 1d6 acid damage.
|
||||
"""You hurl a bubble of acid. Choose one creature within range, or
|
||||
choose two creatures within range that are within 5 feet of each
|
||||
other. A target must succeed on a Dexterity saving throw or take
|
||||
1d6 acid damage.
|
||||
|
||||
**At Higher Levels:** This spell's damage increases by 1d6 when
|
||||
you reach 5th level (2d6), 11th level (3d6), and 17th level (4d6).
|
||||
|
||||
At Higher Levels:
|
||||
This spell's damage increases by 1d6 when you reach 5th level (2d6), 11th level
|
||||
(3d6), and 17th level (4d6).
|
||||
"""
|
||||
name = "Acid Splash"
|
||||
level = 0
|
||||
@@ -95,15 +95,15 @@ class AcidSplash(Spell):
|
||||
|
||||
|
||||
class AganazzarsScorcher(Spell):
|
||||
"""A line of roaring flame 30 feet long and 5 feet wide emanates from you in a
|
||||
direction you choose.
|
||||
Each creature in the line must make a Dexterity saving
|
||||
throw. A creature takes 3d8 fire damage on a failed save, or half as much damage
|
||||
on a successful one.
|
||||
"""A line of roaring flame 30 feet long and 5 feet wide emanates from
|
||||
you in a direction you choose. Each creature in the line must
|
||||
make a Dexterity saving throw. A creature takes 3d8 fire damage on
|
||||
a failed save, or half as much damage on a successful one.
|
||||
|
||||
**At Higher Levels:** When you cast this spell using a spell slot
|
||||
of 3rd level or higher, the damage increases by 1d8 for each slot
|
||||
level above 2nd.
|
||||
|
||||
At Higher Levels: When you cast this spell using a spell
|
||||
slot of 3rd level or higher, the damage increases by 1d8 for each slot level
|
||||
above 2nd.
|
||||
"""
|
||||
name = "Aganazzars Scorcher"
|
||||
level = 2
|
||||
|
||||
@@ -95,16 +95,16 @@ class Scrying(Spell):
|
||||
voluntarily if it wants to be observed.
|
||||
|
||||
Knowledge - Save Modifier
|
||||
=========================
|
||||
Secondhand (you have heard of the target) - +5
|
||||
Firsthand (you have met the target) - +0
|
||||
Familiar (you know the target well) - -5
|
||||
-------------------------
|
||||
- Secondhand (you have heard of the target) - +5
|
||||
- Firsthand (you have met the target) - +0
|
||||
- Familiar (you know the target well) - -5
|
||||
|
||||
Connection - Save Modifier
|
||||
==========================
|
||||
Likeness or picture - -2
|
||||
Possession or garment - -4
|
||||
Body part, lock of hair, bit of nail, or the like - -10
|
||||
--------------------------
|
||||
- Likeness or picture - -2
|
||||
- Possession or garment - -4
|
||||
- Body part, lock of hair, bit of nail, or the like - -10
|
||||
|
||||
On a successful save, the target isn't affected, and you can't use
|
||||
this spell against it again for 24 hours.
|
||||
|
||||
@@ -51,3 +51,29 @@ class MarkdownTestCase(unittest.TestCase):
|
||||
def test_verbatim(self):
|
||||
text = make_sheets.rst_to_latex('``hello, world``')
|
||||
self.assertIn(r'\begin{verbatim}', text)
|
||||
|
||||
def test_literal_backslash(self):
|
||||
text = make_sheets.rst_to_latex('\\')
|
||||
self.assertEqual(r'\\', text)
|
||||
|
||||
def test_headings(self):
|
||||
# Simple heading by itself
|
||||
text = make_sheets.rst_to_latex('Hello, world\n============\n')
|
||||
self.assertEqual('\\section*{Hello, world}\n', text)
|
||||
# Simple heading with leading whitespace
|
||||
text = make_sheets.rst_to_latex(' Hello, world\n ============\n')
|
||||
self.assertEqual('\\section*{Hello, world}\n', text)
|
||||
# Heading with text after it
|
||||
text = make_sheets.rst_to_latex('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 = make_sheets.rst_to_latex('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 = make_sheets.rst_to_latex('Hello, world\n^^^^^^^^^^^^\n')
|
||||
self.assertEqual('\\subsubsection*{Hello, world}\n', text)
|
||||
text = make_sheets.rst_to_latex('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 = make_sheets.rst_to_latex('Hello, world^^^^^^^^^^^^\n')
|
||||
self.assertEqual('Hello, world\\^\\^\\^\\^\\^\\^\\^\\^\\^\\^\\^\\^\n', text)
|
||||
|
||||
Reference in New Issue
Block a user