gdritter repos dndchar / master charsheet.py
master

Tree @master (Download .tar.gz)

charsheet.py @masterraw · history · blame

#!/usr/bin/env python3

import sys
import yaml
import yattag

TEMPLATE = '''<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-type"
          content="application/html; charset=utf-8"/>
    <style type="text/css">
      body {{ font-family: Fira Sans; }}
      ul {{ list-style-type: '—'; margin-top: 2px; margin-bottom: 2px; }}
      .main {{ width: 600px; margin-left: auto; margin-right: auto; }}
      .key {{ color: #888; }}
      .key:after {{ content: ": " }}
      .val {{ font-weight: bold; }}
      .section {{ padding-left: 30px; padding-right: 30px;
                  padding-top: 15px; padding-bottom: 15px;
                  margin: 20px; border: 1px solid; }}
      .note {{ color: #888; }}
       li {{ padding-left: 10px; margin-left: 5px; }}
       .subhed {{ font-weight: bold; }}
    </style>
  </head>
  <body><div class="main">{0}</div></body>
</html>
'''

def main(source):
    '''Load a character YAML file and then HTMLify it'''
    if source == '-':
        char = yaml.load(sys.stdin)
    else:
        with open(source) as f:
            char = yaml.load(f)
    print(TEMPLATE.format(render(char)))

def stat_mod(n):
    '''Get the modifier corresponding to a stat'''
    mod = (n // 2) - 5
    if mod < 0:
        return str(mod)
    else:
        return '+' + str(mod)

STATS = [
    'strength',
    'dexterity',
    'constitution',
    'intelligence',
    'wisdom',
    'charisma',
    'sanity'
]

def render(char):
    '''
    Turn a character into a nice HTML page. This uses a lot of
    helper functions for common structures.
    '''
    doc, tag, text, line = yattag.Doc().ttl()

    def field(name, source=char):
        '''
        Look up a field in `source` and print it nicely as a key/value
        pair of some kind. This does a bit of inspection of the value,
        and in the case of a list of dictionary, tries to do the right
        thing and lay it out as an HTML unordered list.
        '''
        if isinstance(source[name], list):
            line('span', name.title(), klass='subhed')
            with tag('ul', id=name):
                for elem in source[name]:
                    maybe_li(elem)
        elif isinstance(source[name], dict):
            line('span', name.title(), klass='subhed')
            with tag('ul', id=name):
                for k, elem in source[name].items():
                    maybe_li('{0}: {1}'.format(k, elem).capitalize())
        else:
            print_kv(name, source[name])
            doc.stag('br')

    def print_kv(name, val):
        '''
        Print a simple key/value pair nicely.
        '''
        with tag('span', id=name):
            line('span', name.capitalize(), klass='key')
            line('span', val, klass='val')

    def stat_field(name, source=char['stats']):
        '''
        For fields representing a D&D stat, we also want to print the
        modifier afterwards, for convenience.
        '''
        with tag('span', id=name):
            line('span', name.title(), klass='key')
            line('span', source[name], klass='val')
            mod = stat_mod(source[name])
            line('span', ' ({0})'.format(mod), klass='note')
        doc.stag('br')

    def maybe_li(val):
        '''
        Sometimes a list item is going to be just a string, but it
        might have more structure than that, in which case we want
        to print the tags on the structure, as well.
        '''
        if isinstance(val, str):
            line('li', val.capitalize())
        else:
            print_li_with_note(val)

    def print_li_with_note(val):
        '''
        Right now, I'm assuming all "rich" fields are dictionaries,
        but this might not turn out to be true eventually. This
        handles fields of the form `"item": {"note": "blah"}` by
        formatting the note separately, and fields of the form
        "foo": "bar" by printing them normally, because that lets
        us pretend in the source text that we can just have single-line
        fields with colons in them. (YAML doesn't interpret it like that!)
        '''
        for k, v in val.items():
            with tag('li'):
                text(k.capitalize())
                if isinstance(v, dict):
                    for cl, tx in v.items():
                        doc.stag('br')
                        line('span', cl+': '+tx, klass='note')
                else:
                    text(': ' + v)

    # Now that we have that our of the way, everything else
    # is just printing out the right pieces of data in order!

    # basic character data
    with tag('div', id='chardata', klass='section'):
        field('name')
        field('class')
        field('level')
        field('background')
        field('race')
        field('alignment')

    # stat block
    with tag('div', id='stats', klass='section'):
        for stat in STATS:
            stat_field(stat)

    # saving throw proficiencies
    with tag('div', id='saving_throws', klass='section'):
        field('saving throws')

    # combat stuff
    with tag('div', id='combat', klass='section'):
        field('ac')
        field('speed')
        field('hit point maximum')
        field('hit die')

    # spells and spell slots
    with tag('div', id='spells', klass='section'):
        field('spell slots')
        field('spells')

    # skill proficiencies
    with tag('div', id='skills', klass='section'):
        field('skills')

    # equipment section
    with tag('div', id='equipment', klass='section'):
        field('equipment')

    # personality section
    with tag('div', id='personality', klass='section'):
        field('personality')
        field('ideals')
        field('bonds')
        field('flaw')

    # features and traits
    with tag('div', id='features_traits', klass='section'):
        field('features & traits')

    # other proficiencies
    with tag('div', id='other_proficiencies', klass='section'):
        field('languages')
        field('proficiencies')

    # and we're done!
    return doc.getvalue()

if __name__ == '__main__':
    main(sys.argv[1])