changeset 0:5b6f8024e177

initial commit
author Jeff Hammel <k0scist@gmail.com>
date Tue, 03 Nov 2020 13:01:49 -0800
parents
children dacffabe885f
files PKG-INFO TODO markup/__init__.py markup/form.py markup/markup.py setup.cfg setup.py
diffstat 7 files changed, 457 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/PKG-INFO	Tue Nov 03 13:01:49 2020 -0800
@@ -0,0 +1,10 @@
+Metadata-Version: 1.0
+Name: markup
+Version: 0.2
+Summary: generate HTML markup programmatically
+Home-page: http://www.openplans.org/people/k0s/
+Author: Jeff Hammel
+Author-email: jhammel@openplans.org
+License: GPL
+Description: UNKNOWN
+Platform: UNKNOWN
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/TODO	Tue Nov 03 13:01:49 2020 -0800
@@ -0,0 +1,16 @@
+Block level elements should be distinguished from non-block level elements:
+For instance, div('foo') should render:
+
+<div>
+foo
+</div>
+
+Versus b('foo') should result in 
+
+<b>foo</b>
+
+Quoting -  smart quoting should be implemented:
+
+"foo" -> &#8220;foo&#8221;
+
+'foo' -> &#8216;foo&#8217;
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markup/__init__.py	Tue Nov 03 13:01:49 2020 -0800
@@ -0,0 +1,1 @@
+from markup import *
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markup/form.py	Tue Nov 03 13:01:49 2020 -0800
@@ -0,0 +1,232 @@
+import markup
+from cStringIO import StringIO
+
+class Form(object):
+    """a simple class for HTML forms"""
+
+    type_validators = {} # validators inherit to type
+
+    def __init__(self, method='post', action=None, submit='Submit', post_html=''):
+        """
+        * post_html: html to include before the submit button
+        """
+        self.method = method
+        self.action = action
+        self.submit = submit
+        self.post_html = post_html
+        self.elements = []
+        self.validators = {} # keys=tuples of names; values=validators
+
+    def __call__(self, errors=None):
+        """render the form"""
+        retval = StringIO()
+        print >> retval
+
+        def name_field(element):
+            if not element['name']:
+                return ''
+            title={}
+            help = element.get('help')
+            if help:
+                title['title'] = help
+            return markup.span(markup.strong('%s:' % element['name']), **title)
+
+        # print the form as a table
+        table = [ [ name_field(element), element['html'] ]
+                  for element in self.elements ]
+        if errors:
+            for row, element in zip(table, self.elements):
+                error = errors.get(element['name'], '')
+                if error:
+                    if not isinstance(error, basestring):
+                        if len(error) == 1:
+                            error = error[0]
+                        else:
+                            error = markup.listify(error)
+                    error = markup.div(error, **{ 'class': 'error' })
+                row.append(error)
+            
+        print >> retval, markup.tablify(table)
+        print >> retval, self.post_html
+
+        # each form has a submit button
+        # XXX this should probably be more customizable
+        print >> retval, markup.input(None, type='submit', name='submit',
+                                      value=self.submit),
+
+        args = { 'method': self.method,
+                 'enctype': 'multipart/form-data'}
+        if self.action is not None:
+            args['action'] = self.action
+        return markup.form(retval.getvalue(), **args)
+
+    ### functions for validation
+
+    def validate(self, post):
+        """validate the form from the (POST) data
+        post should be a dictionary (MultiDict)
+        returns a dictionary of errors (empty dict == success)
+        validator functions can denote failure in three ways:
+        * raise an exception (currently any[!])
+        * return a string -- assumed to be an error string
+        * return False -- the error
+        """
+
+        errors = {}
+
+        def add_error(name, error):
+            if not errors.has_key(name):
+                errors[name] = []
+            errors[name].append(error)
+
+        for names, validators in self.validators.items():
+            args = [ post.get(arg) for arg in names ]
+            error = None
+
+            for validator in validators:
+                try:
+                    validation = validator(*args)
+                except Exception, e: # horrible!
+                    error = str(e)
+                else:
+                    if isinstance(validation, basestring):
+                        error = validation # error string
+                    elif validation == False:
+                        error = "Illegal value for %s" % name
+                if error:
+                    for name in names:
+                        add_error(name, error)
+                    
+        return errors
+
+    ### functions to add form elements
+    # each of these should be decorated to have:
+    # * a name/label
+    # * a validator (optional)
+    # (depending on type, an additional validator may be implied)
+
+    def textfield(self, name, value=None, **kw):
+        kwargs = dict(name=name, type='text')
+        if value is not None:
+            kwargs['value'] = value
+            kwargs['size'] = '%d' % len(value)
+        kwargs.update(kw)
+        return markup.input(None, **kwargs)
+
+    def password(self, name):
+        return markup.input(None, type='password', name=name)
+
+    def hidden(self, name, value):
+        return markup.input(None, type='hidden', name=name, value=value)
+
+    def file_upload(self, name):
+        return markup.input(None, type='file', name=name)
+
+    def menu(self, name, items):
+        # first item is selected by default
+        retval = '\n'.join([ markup.option(item) for item in items])
+        return markup.select('\n%s\n' % retval, name=name)
+
+    def checkboxes(self, name, items, checked=set()):
+        retval = []
+        checked = set(checked)
+        for item in items:
+            kw = { 'name': name, 'value': item, 'type': 'checkbox' }
+            if item in checked:
+                kw['checked'] = 'checked'
+
+            # XXX hacky;  ideally, one would use a list with no bullets
+            retval.append('%s%s<br/>' % (markup.input(None, **kw), item))
+            
+        return '\n'.join(retval)
+
+    def radiobuttons(self, name, items, checked=None, joiner='<br/>'):
+        if checked is None:
+            checked = items[0]
+        joiner = "%s\n" % joiner
+        retval = []
+        for item in items:
+            title = None
+            if hasattr(item, '__iter__'):
+                item, title = item
+            kw = dict(type='radio', name=name, value=item)
+            if item == checked:
+                kw['checked'] = None
+
+            # display contextual help
+            if title:
+                title = dict(title=title)
+            else:
+                title = {}
+                
+            retval.append(markup.span('%s%s' % (item, markup.input(**kw)),
+                          **title))
+            
+        return joiner.join(retval)
+
+    def add_password_confirmation(self, password='Password',
+                                  confirmation='Confirm password',
+                                  validators=None):
+        """add password and confirm password fields"""
+        if validators is None:
+            validators = []
+        validators.append(lambda x: bool(x.strip()) or "Please provide a password")
+        self.add_element('password', password)
+        self.add_element('password', confirmation)
+        self.validators[(password,)] = validators
+        match = lambda x, y: (x == y) or "Fields don't match"
+        self.validators[(password, confirmation)] = [ match ]
+
+    
+    def add_element(self, func, name, *args, **kwargs):
+
+        if isinstance(func, basestring):
+            func_name = func
+            func = getattr(self, func)
+        else:
+            func_name = func.func_name
+
+        # form validators
+        validators = self.type_validators.get(func_name, [])
+        validators.extend(kwargs.pop('validators', []))
+        self.validators[(name, )] = validators
+
+        # don't diplay hidden elements
+        if func_name == 'hidden':
+            self.elements.append(dict(name='', 
+                                      html=func(name, *args, **kwargs)))
+            return
+
+        # allow contextual help
+        help = kwargs.pop('help', None)
+
+        # allow alternate input names
+        # XXX breaks validation?
+        input_name = kwargs.pop('input_name', name)
+
+        self.elements.append(dict(name=name, 
+                                  html=func(input_name, *args, **kwargs),
+                                  help=help))
+
+    # TODO: add validation
+    def add_elements(self, name, funcs, args, kwargs, help=None):
+        """add multiple elements to the form
+        * name: master name; element names will be derived from this
+        * funcs: list of html-returning functions to add
+        * args: list of positional args: [ [ args1 ], [ args2], ... ]
+        * kwargs: dictionary of kwargs: [ { kwargs1 }, { kwargs2 }, ... ]
+        """
+        html = StringIO()
+        
+        for func, index in zip(funcs, range(len(funcs))):
+            # no validation (yet)
+            if isinstance(func, basestring):
+                func = getattr(self, func)
+            print >> html, func('%s-%d' % (name, index), *args[index],
+                                **kwargs[index])
+        self.elements.append(dict(name=name,
+                                  html=html.getvalue(),
+                                  help=help))
+
+
+    
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/markup/markup.py	Tue Nov 03 13:01:49 2020 -0800
@@ -0,0 +1,167 @@
+from cStringIO import StringIO
+
+def HTMLmarkup(tag, text=None, **attributes):
+    """
+    markups the text with the tag and
+    attributes
+    """
+
+    # ideally, this woulod be cached for cascading calls
+    s = StringIO()
+    
+    s.write('<%s' % tag)
+    for attribute, value in attributes.items():
+        if value is None:
+            s.write(' %s' % attribute)
+        else:
+            s.write(' %s="%s"' % (attribute, value))
+
+    if text:
+        if tag in block_level:
+            s.write('>\n%s\n</%s>' % (text, tag))
+        else:
+            s.write('>%s</%s>' % (text, tag))
+    else:
+        s.write('/>')
+    return s.getvalue()
+
+tags = [ 'a',
+         'b', 'body', 'br',
+         'center',
+         'dd', 'div', 'dl', 'dt', 'em',
+         'form',
+         'h1', 'h2', 'h3', 'head', 'html',
+         'i', 'img', 'input',
+         'li', 'lh',
+         'ol', 'option',
+         'p',
+         'select', 'span', 'strong',
+         'table', 'td', 'textarea', 'th', 'title', 'tr',
+         'ul',
+         ]
+
+for _i in tags:
+    globals()[_i] = lambda x=None, _i=_i, **y: HTMLmarkup(_i, x, **y)
+
+# block-level elements
+# from http://htmlhelp.com/reference/html40/block.html
+block_level = set(['address',
+                   'blockquote',
+                   'center',
+                   'dir', 'div', 'dl',
+                   'fieldset', 'form',
+                   'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr',
+                   'isindex',
+                   'menu',
+                   'noframes', 'noscript',
+                   'ol',
+                   'p', 'pre',
+                   'table',
+                   'ul',
+                   # not really block level, but act like it is
+                   'body',
+                   'dd', 'dt',
+                   'frameset',
+                   'head', 'html',
+                   'iframe',
+                   'tbody', 'tfoot', 'th', 'thead', 'tr'])
+
+### front ends to tags to make our lives easier
+### these don't stomp on tags -- they're just front ends
+### (these should go in a separate file)
+
+def image(src, **attributes):
+    attributes['src'] = src
+    return img(**attributes)
+
+def link(location, description=None, **attributes):
+    if description is None:
+        description = location
+    attributes['href'] = location
+    return a(description, **attributes)
+
+def listify(items, ordered=False, **attributes):
+    """
+    return a HTML list of iterable items
+    * ordered: whether the list is a <ol> (True) or an <ul> (False)
+    * item_attributes: attributes applied to each list item
+    """
+
+    # type of list
+    if ordered:
+        func = ol
+    else:
+        func = ul
+
+    item_attributes = attributes.pop('item_attributes', {})
+    listitems = [ li(item, **item_attributes) for item in items ]
+    return func('\n'.join(listitems), **attributes)
+
+def definition_list(items, header=None, **attributes):
+    """definition list
+    items can be a dictionary or a list of 2-tuples"""
+    # XXX no attributes for header, dts, dds (yet)
+
+    if header is None:
+        header = '',
+    else:
+        header = '%s\n' % lh(header)
+
+    # convert dicts to lists of 2-tuples
+    if hasattr(items, 'items'):
+        items = items.items() 
+
+    items = [ dt(term) + dd(definition) for term, definition in items ]
+    return dl(('\n%s%s\n' % ( header, '\n'.join(items))), **attributes)
+
+def tablify(rows, header=False, item_attributes=None, 
+            **attributes):
+    """return an HTML table from a iterable of iterable rows"""
+    
+    if item_attributes is None:
+        item_attributes = {}
+
+    retval = []
+    if header:
+        markup = th
+    else:
+        markup = td
+
+    for row in rows:
+        retval.append('\n'.join([markup(str(item)) for item in row]))
+        markup = td
+
+    return table('\n\n'.join([tr(row) for row in retval]))
+
+def wrap(string, pagetitle=None, stylesheets=(), icon=None, head_markup=()):
+    """wrap a string in a webpage"""
+
+    _head = ''
+    if pagetitle:
+        _head += title(pagetitle)
+    rel = 'stylesheet'
+    for i in stylesheets:
+        attributes = dict(rel=rel,
+                          type='text/css')        
+        if hasattr(i, '__iter__'):
+            # assume a 2-tuple
+            attributes['href'] = i[0]
+            attributes['title'] = i[1]
+        else:
+            attributes['href'] = i
+        _head += '\n' + HTMLmarkup('link', None, **attributes)
+        rel = 'alternate stylesheet' # first stylesheet is default
+    if icon:
+        _head += '\n' + HTMLmarkup('link', None, href=icon)
+
+    if head_markup:
+        # additional markup for <head>
+        if isinstance(head_markup, basestring):
+            _head += '\n' + head_markup
+        else:
+            for item in head_markup:
+                _head += '\n' + item
+    if _head:
+        _head = head(_head)        
+
+    return html('%s\n\n%s' % ( _head, body(string) ) )
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.cfg	Tue Nov 03 13:01:49 2020 -0800
@@ -0,0 +1,5 @@
+[egg_info]
+tag_build = 
+tag_date = 0
+tag_svn_revision = 0
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/setup.py	Tue Nov 03 13:01:49 2020 -0800
@@ -0,0 +1,26 @@
+from setuptools import setup, find_packages
+import sys, os
+
+version = '0.2'
+
+setup(name='markup',
+      version=version,
+      description="generate HTML markup programmatically",
+      long_description="""\
+""",
+      classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
+      keywords='',
+      author='Jeff Hammel',
+      author_email='jhammel@openplans.org',
+      url='http://www.openplans.org/people/k0s/',
+      license='GPL',
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      """,
+      )