Parsing of reST headings into LaTeX \section*{}, etc.

This commit is contained in:
Mark Wolfman
2020-05-14 11:52:32 -05:00
parent a32d1e7142
commit a3bf048e70
9 changed files with 159 additions and 29 deletions
+34
View File
@@ -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 %]
+1 -1
View File
@@ -25,7 +25,7 @@
\subsection*{Subclass: [[ sc.name ]]}
[[ sc.__doc__|rst_to_latex ]]
[[ sc.__doc__ | rst_to_latex(top_heading_level=2) ]]
[% endfor %]
+1 -1
View File
@@ -34,7 +34,7 @@
[% endif %]%
[[ inf.__doc__|rst_to_latex ]]
[[ inf.__doc__ | rst_to_latex(top_heading_level=2) ]]
[% endfor %]
+1 -1
View File
@@ -47,7 +47,7 @@
\end{description}
\vspace{\zerosep}
[[ spl.__doc__|rst_to_latex ]]
[[ spl.__doc__ | rst_to_latex(top_heading_level=1) ]]
[% endfor %]
+72 -2
View File
@@ -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)
+15 -15
View File
@@ -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
+8 -8
View File
@@ -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.
+26
View File
@@ -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)