Anonymous avatar Anonymous committed 89b77b0

[svn] relocating RailsHelpers into new WebHelpers

Comments (0)

Files changed (34)

+RailsHelpers
+++++++++++++
+
+RailsHelpers is a direct port of the Rails helper functions.
+
+These functions are intended to ease web development with template languages by removing common view logic and
+encapsulating it in re-usable modules.
+
+*Requirements:* RailsHelpers requires `Routes <http://routes.groovie.org/>`_ to be active in the framework
+for a variety of functions. Currently `Pylons <http://pylons.groovie.org/>`_, `TurboGears <http://trac.turbogears.org/turbogears/wiki/RoutesIntegration>`_,
+and `Aquarium <http://aquarium.sourceforge.net/>`_ support Routes.

railshelpers/__init__.py

+from helpers.url import *
+from helpers.javascript import *
+from helpers.tag import *
+from helpers.prototype import *
+from helpers.scriptaculous import *
+from helpers.form_tag import *
+from helpers.text import *
+from helpers.form_options import *
+from helpers.date import *
+from helpers.number import *
+from routes import url_for, redirect_to

railshelpers/commands.py

+from paste.script.command import Command
+class ScriptaculousCommand(Command):
+    summary = "Unfinshed scriptaculous command"

railshelpers/escapes.py

+# $Id: escapes.py 2013 2005-12-31 03:19:39Z zzzeek $
+# escapes.py - string escaping functions for Myghty
+# Copyright (C) 2004, 2005 Michael Bayer mike_mp@zzzcomputing.com
+# Original Perl code and documentation copyright (c) 1998-2003 by Jonathan Swartz. 
+#
+# This module is part of Myghty and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+#
+
+
+import cgi
+
+def html_escape(string):
+    return cgi.escape(string, True)

railshelpers/helpers/__init__.py

+#

railshelpers/helpers/date.py

+"""
+Date/Time Helpers
+"""
+
+from datetime import datetime
+import time
+
+def distance_of_time_in_words(from_time, to_time=0, include_seconds=False):
+    """
+    Reports the approximate distance in time between two datetime objects or integers. 
+    
+    For example, if the distance is 47 minutes, it'll return
+    "about 1 hour". See the source for the complete wording list.
+    
+    Integers are interpreted as seconds from now. So,
+    ``distance_of_time_in_words(50)`` returns "less than a minute".
+    
+    Set ``include_seconds`` to True if you want more detailed approximations if distance < 1 minute
+    """
+    if isinstance(from_time, int):
+        from_time = time.time()+from_time
+    else:
+        from_time = time.mktime(from_time.timetuple())
+    if isinstance(to_time, int):
+        to_time = time.time()+to_time
+    else:
+        to_time = time.mktime(to_time.timetuple())
+    
+    distance_in_minutes = int(round(abs(to_time-from_time)/60))
+    distance_in_seconds = int(round(abs(to_time-from_time)))
+    
+    if distance_in_minutes <= 1:
+        if include_seconds:
+            for remainder in [5, 10, 20]:
+                if distance_in_seconds < remainder:
+                    return "less than %s seconds" % remainder
+            if distance_in_seconds < 40:
+                return "half a minute"
+            elif distance_in_seconds < 60:
+                return "less than a minute"
+            else:
+                return "1 minute"
+        else:
+            if distance_in_minutes == 0:
+                return "less than a minute"
+            else:
+                return "1 minute"
+    elif distance_in_minutes <= 45:
+        return "%s minutes" % distance_in_minutes
+    elif distance_in_minutes <= 90:
+        return "about 1 hour"
+    elif distance_in_minutes <= 1440:
+        return "about %d hours" % (round(distance_in_minutes / 60.0))
+    elif distance_in_minutes <= 2880:
+        return "1 day"
+    else:
+        return "%s days" % (distance_in_minutes / 1440)
+
+def time_ago_in_words(from_time, include_seconds=False):
+    """
+    Like distance_of_time_in_words, but where ``to_time`` is fixed to ``datetime.now()``.
+    """
+    return distance_of_time_in_words(from_time, datetime.now(), include_seconds)
+
+distance_of_time_in_words_to_now = time_ago_in_words
+
+
+__all__ = ['distance_of_time_in_words', 'time_ago_in_words', 'distance_of_time_in_words_to_now']

railshelpers/helpers/form_options.py

+"""
+Form Options Helpers
+"""
+
+from railshelpers.escapes import html_escape
+
+def options_for_select(container, selected = None):
+    """
+    Creates select options from a container (list, tuple, dict)
+    
+    Accepts a container (list, tuple, dict) and returns a string of option tags. Given a container where the 
+    elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
+    the "firsts" as option text. Dicts are turned into this form automatically, so the keys become "firsts" and values
+    become lasts. If ``selected`` is specified, the matching "last" or element will get the selected option-tag.
+    ``Selected`` may also be an array of values to be selected when using a multiple select.
+    
+    Examples (call, result)::
+    
+        >>> options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
+        <option value="$">Dollar</option>\\n<option value="DKK">Kroner</option>
+        >>> options_for_select([ "VISA", "MasterCard" ], "MasterCard")
+        <option value="VISA">VISA</option>\\n<option value="MasterCard" selected="selected">MasterCard</option>
+        >>> options_for_select(dict(Basic="$20", Plus="$40"), "$40")
+        <option value="$20">Basic</option>\\n<option value="$40" selected="selected">Plus</option>
+        >>> options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
+        <option value="VISA" selected="selected">VISA</option>\\n<option value="MasterCard">MasterCard</option>\\n
+        <option value="Discover" selected="selected">Discover</option>
+
+    Note: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+    """
+    if hasattr(container, 'values'):
+        container = container.items()
+    
+    if not isinstance(selected, (list,tuple)):
+        selected = (selected,)
+    
+    options = []
+    
+    for elem in container:
+        if isinstance(elem, (list, tuple)):
+            name, value = elem
+            n = html_escape(name)
+            v = html_escape(value)
+        else :
+            name = value = elem
+            n = v = html_escape(elem)
+        
+        #TODO: run timeit for this against content_tag('option', n, value=v, selected=value in selected)
+        if value in selected:
+            options.append('<option value="%s" selected="selected">%s</option>' % (v, n))
+        else :
+            options.append('<option value="%s">%s</option>' % (v, n))
+    return "\n".join(options)
+
+def options_for_select_from_objects(container, name_attr, value_attr = None, selected = None):
+    """
+    Create select options from objects in a container
+    
+    Returns a string of option tags that have been compiled by iterating over the ``container`` and assigning the
+    the result of a call to the ``value_attr`` as the option value and the ``name_attr`` as the option text.
+    If ``selected`` is specified, the element returning a match on ``value_attr`` will get the selected option tag.
+    
+    NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+    """
+    if value_attr:
+        def make_elem(elem):
+            return getattr(elem, name_attr), getattr(elem, value_attr)
+    else :
+        def make_elem(elem):
+            return getattr(elem, name_attr)
+    
+    return options_for_select([make_elem(x) for x in container], selected)
+
+def options_for_select_from_dicts(container, name_key, value_key = None, selected = None):
+    """
+    Create select options from dicts in a container
+    
+    Returns a string of option tags that have been compiled by iterating over the ``container`` and assigning the
+    the result of a call to the ``value_key`` as the option value and the ``name_attr`` as the option text.
+    If ``selected`` is specified, the element returning a match on ``value_key`` will get the selected option tag.
+    
+    NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+    """
+    if value_key:
+        def make_elem(elem):
+            return elem[name_key], elem[value_key]
+    else :
+        def make_elem(elem):
+            return elem[name_key]
+
+    return options_for_select([make_elem(x) for x in container], selected)
+
+__all__ = ['options_for_select', 'options_for_select_from_objects', 'options_for_select_from_dicts']

railshelpers/helpers/form_tag.py

+"""
+Form Tag Helpers
+"""
+
+from tag import *
+from railshelpers.escapes import html_escape
+
+def form(url, **options):
+    """
+    Starts a form tag that points the action to an url. 
+    
+    The url options should be given either as a string, or as a ``url()`` function. The
+    method for the form defaults to POST.
+    
+    Options:
+
+    * ``multipart`` - If set to True, the enctype is set to "multipart/form-data".
+    * ``method`` - The method to use when submitting the form, usually either "get" or "post".
+    """
+    o = { "method": "post" }
+    o.update(options)
+    if 'multipart' in o:
+        o["enctype"] = "multipart/form-data"
+        del o['multipart']
+    if callable(url):
+        url = url()
+    else:
+        url = html_escape(url)
+    o["action"] = url
+    return tag("form", True, **o)
+
+start_form = form
+
+def end_form():
+    """
+    Outputs "</form>"
+    """
+    return "</form>"
+
+def select(name, option_tags='', **options):
+    """
+    Creates a dropdown selection box
+    
+    ``option_tags`` is a string containing the option tags for the select box::
+
+        >>> select("people", "<option>George</option>")
+        <select id="people" name="people"><option>George</option></select>
+    
+    Options:
+    
+    * ``multiple`` - If set to true the selection will allow multiple choices.
+    """
+    o = { 'name_': name, 'id': name }
+    o.update(options)
+    return content_tag("select", option_tags, **o)
+
+def text_field(name, value=None, **options):
+    """
+    Creates a standard text field.
+    
+    ``value`` is a string that will the contents of the text field will be set to
+    
+    Options:
+    
+    * ``disabled`` - If set to True, the user will not be able to use this input.
+    * ``size`` - The number of visible characters that will fit in the input.
+    * ``maxlength`` - The maximum number of characters that the browser will allow the user to enter.
+    
+    Remaining keyword options will be standard HTML options for the tag.
+    """
+    o = {'type': 'text', 'name_': name, 'id': name, 'value': value}
+    o.update(options)
+    return tag("input", **o)
+
+def hidden_field(name, value=None, **options):
+    """
+    Creates a hidden field.
+    
+    Takes the same options as text_field
+    """
+    return text_field(name, value, type="hidden", **options)
+
+def file_field(name, value=None, **options):
+    """
+    Creates a file upload field.
+    
+    If you are using file uploads then you will also need to set the multipart option for the form.
+    """
+    return text_field(name, value=value, type="file", **options)
+
+def password_field(name="password", value=None, **options):
+    """
+    Creates a password field
+    
+    Takes the same options as text_field
+    """
+    return text_field(name, value, type="password", **options)
+
+def text_area(name, content='', **options):
+    """
+    Creates a text input area.
+    
+    Options:
+    
+    * ``size`` - A string specifying the dimensions of the textarea.
+    
+    Example::
+    
+        >>> text_area("body", '', size="25x10")
+        <textarea name="body" id="body" cols="25" rows="10"></textarea>
+    """
+    if 'size' in options:
+        options["cols"], options["rows"] = options["size"].split("x")
+        del options['size']
+    o = {'name_': name, 'id': name}
+    o.update(options)
+    return content_tag("textarea", content, **o)
+
+def check_box(name, value="1", checked=False, **options):
+    """
+    Creates a check box.
+    """
+    o = {'type': 'checkbox', 'name_': name, 'id': name, 'value': value}
+    o.update(options)
+    if checked:
+        o["checked"] = "checked"
+    return tag("input", **o)
+
+def radio_button(name, value, checked=False, **options):
+    """
+    Creates a radio button.
+    """
+    o = {'type': 'radio', 'name_': name, 'id': name, 'value': value}
+    o.update(options)
+    if checked:
+        o["checked"] = "checked"
+    return tag("input", **o)
+
+def submit(value="Save changes", name='commit', **options):
+    """
+    Creates a submit button with the text ``value`` as the caption.
+    
+    If options contains a keyword pair with the key of "disable_with", then the value will
+    be used to rename a disabled version of the submit button.
+    """
+    if options.has_key('disable_with'):
+        options["onclick"] = "this.disabled=true;this.value='%s';this.form.submit();%s" % (options['disable_with'], options.get("onclick", ''))
+    o = {'type': 'submit', 'name_': name, 'value': value }
+    o.update(options)
+    return tag("input", **o)
+      
+#def image_submit(source, **options):
+#    """Displays an image which when clicked will submit the form"""
+#    o = {'type': 'image', 'src': image_path_source) }
+#    o.update(options)
+#    return tag("input", **o)
+
+__all__ = ['form', 'start_form', 'end_form', 'select', 'text_field', 'hidden_field', 'file_field',
+           'password_field', 'text_area', 'check_box', 'radio_button', 'submit']

railshelpers/helpers/javascript.py

+"""
+Javascript Helpers
+
+Provides functionality for working with JavaScript in your views.
+
+Ajax, controls and visual effects
+---------------------------------
+
+* For information on using Ajax, see `Prototype Helpers <module-railshelpers.helpers.prototype.html>`_.
+* For information on using controls and visual effects, see `Scriptaculous Helpers <module-railshelpers.helpers.scriptaculous.html>`_.
+"""
+from tag import *
+import re
+
+def link_to_function(name, function, **html_options):
+    """
+    Returns a link that'll trigger a JavaScript ``function`` using the 
+    onclick handler and return false after the fact.
+    
+    Example::
+    
+        link_to_function("Greeting", "alert('Hello world!')")
+    """
+    options = dict(href="#", onclick="%s; return false;" % function)
+    options.update(html_options)
+    return content_tag("a", name, **options)
+
+def button_to_function(name, function, **html_options):
+    """
+    Returns a link that'll trigger a JavaScript ``function`` using the 
+    onclick handler and return false after the fact.
+    
+    Example::
+    
+        button_to_function("Greeting", "alert('Hello world!')")
+    """
+    options = dict(type_="button", value=name, onclick="%s; " % function)
+    options.update(html_options)
+    return content_tag("input", name, **options)
+
+def escape_javascript(javascript):
+    """
+    Escape carriage returns and single and double quotes for JavaScript segments.
+    """
+    javascript = re.sub(r'\r\n|\n|\r', r'\\n', (javascript or ''))
+    javascript = re.sub(r'(["\'])', r'\\\1', javascript)
+    return javascript
+
+def javascript_tag(content):
+    """
+    Returns a JavaScript tag with the ``content`` inside.
+    
+    Example::
+    
+        >>> javascript_tag("alert('All is good')"
+        '<script type="text/javascript">alert('All is good')</script>'
+    """
+    return content_tag("script", javascript_cdata_section(content), type="text/javascript")
+
+def javascript_cdata_section(content):
+    return "\n//%s\n" % cdata_section("\n%s\n//" % content)
+
+def options_for_javascript(options):
+    optionlist = []
+    for key, value in options.iteritems():
+        if isinstance(value, bool):
+            value = str(value).lower()
+        optionlist.append('%s:%s' % (key, value))
+    optionlist.sort()
+    return '{' + ', '.join(optionlist) + '}'
+
+def array_or_string_for_javascript(option):
+    jsoption = None
+    if isinstance(option, list):
+        jsoption = "['%s']" % '\',\''.join(option)
+    elif isinstance(option, bool):
+        jsoption = str(option).lower()
+    else:
+        jsoption = "'%s'" % option
+    return jsoption
+
+__all__ = ['button_to_function', 'javascript_tag', 'escape_javascript', 'link_to_function']

railshelpers/helpers/number.py

+"""
+Number Helpers
+"""
+import re
+
+def number_to_phone(number, area_code=False, delimiter="-", extension=""):
+    """
+    Formats a ``number`` into a US phone number string.
+    
+    The area code can be surrounded by parentheses by setting ``area_code`` to True; default is False
+    The delimiter can be set using ``delimiter`` default is "-"
+    
+    Examples::
+    
+        >>> number_to_phone(1235551234)
+        123-555-1234
+        >>> number_to_phone(1235551234, area_code=True)
+        (123) 555-1234
+        >>> number_to_phone(1235551234, delimiter=" ")
+        123 555 1234
+        >>> number_to_phone(1235551234, area_code=True, extension=555)
+        (123) 555-1234 x 555
+    """
+    if area_code:
+        number = re.sub(r'([0-9]{3})([0-9]{3})([0-9]{4})', r'(\1) \2%s\3' % delimiter, str(number))
+    else:
+        number = re.sub(r'([0-9]{3})([0-9]{3})([0-9]{4})', r'\1%s\2%s\3' % (delimiter, delimiter), str(number))
+    if extension:
+        number += " x %s" % extension
+    return number
+
+def number_to_currency(number, unit="$", precision=2, separator=".", delimiter=","):
+    """
+    Formats a ``number`` into a currency string. 
+    
+    ``number``
+        Indicates the level of precision
+    ``unit``
+        Sets the currency type
+    ``separator``
+        Used to set what the unit separation should be
+    ``delimiter``
+        The delimiter can be set using the +delimiter+ key; default is ","
+    
+    Examples::
+    
+        >>> number_to_currency(1234567890.50)
+        $1,234,567,890.50
+        >>> number_to_currency(1234567890.506)
+        $1,234,567,890.51
+        >>> number_to_currency(1234567890.50, unit="&pound;", separator=",", delimiter="")
+        &pound;1234567890,50
+    """
+    if precision < 1:
+        separator = ""
+    parts = number_with_precision(number, precision).split('.')
+    return unit + number_with_delimiter(parts[0], delimiter) + separator + parts[1]
+
+def number_to_percentage(number, precision=3, separator="."):
+    """
+    Formats a ``number`` as into a percentage string. 
+    
+    ``number``
+        Contains the level of precision
+    ``separator``
+        The unit separator to be used
+    
+    Examples::
+    
+        >>> number_to_percentage(100)
+        100.000%
+        >>> number_to_percentage(100, precision=0)
+        100%
+        >>> number_to_percentage(302.0574, precision=2)
+        302.06%
+    """
+    number = number_with_precision(number, precision)
+    parts = number.split('.')
+    if len(parts) < 2:
+        return parts[0] + "%"
+    else:
+        return parts[0] + separator + parts[1] + "%"
+
+def number_to_human_size(size):
+    """
+    Returns a formatted-for-humans file size.
+    
+    Examples::
+    
+        >>> number_to_human_size(123)
+        123 Bytes
+        >>> number_to_human_size(1234)
+        1.2 KB
+        >>> number_to_human_size(12345)
+        12.1 KB
+        >>> number_to_human_size(1234567)
+        1.2 MB
+        >>> number_to_human_size(1234567890)
+        1.1 GB
+    """
+    if size < 1024:
+        return "%d Bytes" % size
+    elif size < (1024**2):
+        return "%.1f KB" % (size / 1024.00)
+    elif size < (1024**3):
+        return "%.1f MB" % (size / 1024.00**2)
+    elif size < (1024**4):
+        return "%.1f GB" % (size / 1024.00**3)
+    elif size < (1024**5):
+        return "%.1f TB" % (size / 1024.00**4)
+    else:
+        return ""
+
+def number_with_delimiter(number, delimiter=","):
+    """
+    Formats a ``number`` with a ``delimiter``.
+    
+    Example::
+    
+        >>> number_with_delimiter(12345678)
+        12,345,678
+    """
+    return re.sub(r'(\d)(?=(\d\d\d)+(?!\d))', r'\1%s' % delimiter, str(number))
+
+def number_with_precision(number, precision=3):
+    """
+    Formats a ``number`` with a level of ``precision``.
+    
+    Example::
+    
+        >>> number_with_precision(111.2345)
+        111.235
+    """
+    formstr = '%01.' + str(precision) + 'f'
+    return formstr % number
+
+__all__ = ['number_to_phone', 'number_to_currency', 'number_to_percentage','number_with_delimiter', 
+           'number_with_precision', 'number_to_human_size']

railshelpers/helpers/prototype.py

+"""
+Prototype Helpers
+
+Provides a set of helpers for calling Prototype JavaScript functions, 
+including functionality to call remote methods using 
+`Ajax <http://www.adaptivepath.com/publications/essays/archives/000385.php>`_. 
+This means that you can call actions in your controllers without
+reloading the page, but still update certain parts of it using
+injections into the DOM. The common use case is having a form that adds
+a new element to a list without reloading the page.
+
+To be able to use these helpers, you must include the Prototype 
+JavaScript framework in your pages.
+
+See `link_to_remote <module-railshelpers.helpers.javascript.html#link_to_function>`_ 
+for documentation of options common to all Ajax helpers.
+
+See also `Scriptaculous <module-railshelpers.helpers.scriptaculous.html>`_ for
+helpers which work with the Scriptaculous controls and visual effects library.
+"""
+import sys
+if sys.version < '2.4':
+    from sets import ImmutableSet as frozenset
+
+from javascript import *
+from javascript import options_for_javascript
+from tag import tag, camelize
+from url import get_url
+
+CALLBACKS = frozenset(['uninitialized','loading','loaded',
+                       'interactive','complete','failure','success'] + [str(x) for x in range(100,599)])
+AJAX_OPTIONS = frozenset(['before','after','condition','url',
+                          'asynchronous','method','insertion','position',
+                          'form','with','update','script'] + list(CALLBACKS))
+
+def link_to_remote(name, options={}, **html_options):
+    """
+    Links to a remote function
+    
+    Returns a link to a remote action defined ``dict(url=url())``
+    (using the url() format) that's called in the background using 
+    XMLHttpRequest. The result of that request can then be inserted into a
+    DOM object whose id can be specified with the ``update`` keyword. 
+    Usually, the result would be a partial prepared by the controller with
+    either render_partial or render_partial_collection.
+    
+    Any keywords given after the second dict argument are considered html options
+    and assigned as html attributes/values for the element.
+    
+    Example::
+    
+        link_to_remote("Delete this post", dict(update="posts", 
+                       url=url(action="destroy", id=post.id)))
+    
+    You can also specify a dict for ``update`` to allow for easy redirection
+    of output to an other DOM element if a server-side error occurs:
+    
+    Example::
+
+        link_to_remote("Delete this post",
+                dict(url=url(action="destroy", id=post.id),
+                     update=dict(success="posts", failure="error")))
+    
+    Optionally, you can use the ``position`` parameter to influence how the
+    target DOM element is updated. It must be one of 'before', 'top', 'bottom',
+    or 'after'.
+    
+    By default, these remote requests are processed asynchronous during 
+    which various JavaScript callbacks can be triggered (for progress 
+    indicators and the likes). All callbacks get access to the 
+    ``request`` object, which holds the underlying XMLHttpRequest. 
+    
+    To access the server response, use ``request.responseText``, to
+    find out the HTTP status, use ``request.status``.
+    
+    Example::
+
+        link_to_remote(word,
+                dict(url=url(action="undo", n=word_counter),
+                     complete="undoRequestCompleted(request)"))
+    
+    The callbacks that may be specified are (in order):
+    
+    ``loading``
+        Called when the remote document is being loaded with data by the browser.
+    ``loaded``
+        Called when the browser has finished loading the remote document.
+    ``interactive``
+        Called when the user can interact with the remote document, even
+        though it has not finished loading.
+    ``success``
+        Called when the XMLHttpRequest is completed, and the HTTP status
+        code is in the 2XX range.
+    ``failure``
+        Called when the XMLHttpRequest is completed, and the HTTP status code is
+        not in the 2XX range.
+    ``complete``
+        Called when the XMLHttpRequest is complete (fires after success/failure
+        if they are present).
+                        
+    You can further refine ``success`` and ``failure`` by 
+    adding additional callbacks for specific status codes.
+    
+    Example::
+    
+        link_to_remote(word,
+                dict(url=url(action="action"),
+                     404="alert('Not found...? Wrong URL...?')",
+                     failure="alert('HTTP Error ' + request.status + '!')"))
+    
+    A status code callback overrides the success/failure handlers if 
+    present.
+    
+    If you for some reason or another need synchronous processing (that'll
+    block the browser while the request is happening), you can specify 
+    ``type='synchronous'``.
+    
+    You can customize further browser side call logic by passing in
+    JavaScript code snippets via some optional parameters. In their order 
+    of use these are:
+    
+    ``confirm``
+        Adds confirmation dialog.
+    ``condition``
+        Perform remote request conditionally by this expression. Use this to
+        describe browser-side conditions when request should not be initiated.
+    ``before``
+        Called before request is initiated.
+    ``after``
+        Called immediately after request was initiated and before ``loading``.
+    ``submit``
+        Specifies the DOM element ID that's used as the parent of the form
+        elements. By default this is the current form, but it could just as
+        well be the ID of a table row or any other DOM element.    
+    """
+    return link_to_function(name, remote_function(**options), **html_options)
+
+def periodically_call_remote(**options):
+    """
+    Periodically calls a remote function
+    
+    Periodically calls the specified ``url`` every ``frequency`` seconds
+    (default is 10). Usually used to update a specified div ``update``
+    with the results of the remote call. The options for specifying the
+    target with ``url`` and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.    
+    """
+    frequency = options.get('frequency') or 10
+    code = "new PeriodicalExecuter(function() {%s}, %s)" % (remote_function(**options), frequency)
+    return javascript_tag(code)
+
+def form_remote_tag(**options):
+    """
+    Create a form tag using a remote function to submit the request
+    
+    Returns a form tag that will submit using XMLHttpRequest in the 
+    background instead of the regular reloading POST arrangement. Even 
+    though it's using JavaScript to serialize the form elements, the form
+    submission will work just like a regular submission as viewed by the
+    receiving side. The options for specifying the target with ``url``
+    and defining callbacks is the same as `link_to_remote <#link_to_remote>`_.
+    
+    A "fall-through" target for browsers that doesn't do JavaScript can be
+    specified with the ``action/method`` options on ``html``.
+    
+    Example::
+
+        form_remote_tag(html=dict(action=url(
+                                    controller="some", action="place")))
+    
+    By default the fall-through action is the same as the one specified in 
+    the ``url`` (and the default method is ``post``).
+    """
+    options['form'] = True
+    options['html'] = options.get('html') or {}
+    options['html']['onsubmit'] = "%s; return false;" % remote_function(**options)
+    options['html']['action'] = options['html'].get('action') or get_url(options['url'])
+    options['html']['method'] = options['html'].get('method') or 'post'
+    
+    return tag("form", open=True, **options['html'])
+
+def submit_to_remote(name, value, **options):
+    """
+    A submit button that submits via an XMLHttpRequest call
+    
+    Returns a button input tag that will submit form using XMLHttpRequest 
+    in the background instead of regular reloading POST arrangement. 
+    Keyword args are the same as in ``form_remote_tag``.    
+    """
+    options['with'] = options.get('form') or 'Form.serialize(this.form)'
+    
+    options['html'] = options.get('html') or {}
+    options['html']['type'] = 'button'
+    options['html']['onclick'] = "%s; return false;" % remote_function(**options)
+    options['html']['name_'] = name
+    options['html']['value'] = str(value)
+    
+    return tag("input", open=False, **options['html'])
+
+def update_element_function(element_id, **options):
+    """
+    Returns a JavaScript function (or expression) that'll update a DOM 
+    element.
+    
+    ``content``
+        The content to use for updating.
+    ``action``
+        Valid options are 'update' (assumed by default), 'empty', 'remove'
+    ``position``
+        If the ``action`` is 'update', you can optionally specify one of the
+        following positions: 'before', 'top', 'bottom', 'after'.
+    
+    Example::
+    
+        <% javascript_tag(update_element_function("products", 
+            position='bottom', content="<p>New product!</p>")) %>
+    
+    This method can also be used in combination with remote method call 
+    where the result is evaluated afterwards to cause multiple updates on
+    a page. Example::
+    
+        # Calling view
+        <% form_remote_tag(url=url(action="buy"), 
+                complete=evaluate_remote_response()) %>
+            all the inputs here...
+    
+        # Controller action
+        def buy(self, **params):
+            c.product = Product.find(1)
+            m.subexec('/buy.myt')
+    
+        # Returning view
+        <% update_element_function(
+                "cart", action='update', position='bottom', 
+                content="<p>New Product: %s</p>" % c.product.name) %>
+        <% update_element_function("status", binding='binding',
+                content="You've bought a new product!") %>
+    """
+    content = escape_javascript(options.get('content', ''))
+    opval = options.get('action', 'update')
+    if opval == 'update':
+        if options.get('position'):
+            jsf = "new Insertion.%s('%s','%s')" % (camelize(options['position']), element_id, content)
+        else:
+            jsf = "$('%s').innerHTML = '%s'" % (element_id, content)
+    elif opval == 'empty':
+        jsf = "$('%s').innerHTML = ''" % element_id
+    elif opval == 'remove':
+        jsf = "Element.remove('%s')" % element_id
+    else:
+        raise "Invalid action, choose one of update, remove, or empty"
+    
+    jsf += ";\n"
+    if options.get('binding'):
+        return jsf + options['binding']
+    else:
+        return jsf
+
+def evaluate_remote_response():
+    """
+    Returns a Javascript function that evals a request response
+    
+    Returns 'eval(request.responseText)' which is the JavaScript function
+    that ``form_remote_tag`` can call in *complete* to evaluate a multiple
+    update return document using ``update_element_function`` calls.    
+    """
+    return "eval(request.responseText)"
+
+def remote_function(**options):
+    """
+    Returns the JavaScript needed for a remote function.
+    
+    Takes the same arguments as `link_to_remote <#link_to_remote>`_.
+    
+    Example::
+    
+        <select id="options" onchange="<% remote_function(update="options", 
+                url=url(action='update_options')) %>">
+            <option value="0">Hello</option>
+            <option value="1">World</option>
+        </select>    
+    """
+    javascript_options = options_for_ajax(options)
+    
+    update = ''
+    if options.get('update') and isinstance(options['update'], dict):
+        update = []
+        if options['update'].has_key('success'): 
+            update.append("success:'%s'" % options['update']['success'])
+        if options['update'].has_key('failure'):
+            update.append("failure:'%s'" % options['update']['failure'])
+        update = '{' + ','.join(update) + '}'
+    elif options.get('update'):
+        update += "'%s'" % options['update']
+    
+    function = "new Ajax.Request("
+    if update: function = "new Ajax.Updater(%s, " % update
+    
+    function += "'%s'" % get_url(options['url'])
+    function += ", %s)" % javascript_options
+    
+    if options.get('before'):
+        function = "%s; %s" % (options['before'], function)
+    if options.get('after'):
+        function = "%s; %s" % (function, options['after'])
+    if options.get('condition'):
+        function = "if (%s) { %s; }" % (options['condition'], function)
+    if options.get('confirm'):
+        function = "if (confirm('%s')) { %s; }" % (escape_javascript(options['confirm']), function)
+    
+    return function
+
+def observe_field(field_id, **options):
+    """
+    Observes the field with the DOM ID specified by ``field_id`` and makes
+    an Ajax call when its contents have changed.
+    
+    Required keyword args are:
+    
+    ``url``
+        ``url()``-style options for the action to call when the
+        field has changed.
+    
+    Additional keyword args are:
+    
+    ``frequency``
+        The frequency (in seconds) at which changes to this field will be
+        detected. Not setting this option at all or to a value equal to or
+        less than zero will use event based observation instead of time
+        based observation.
+    ``update``
+        Specifies the DOM ID of the element whose innerHTML should be
+        updated with the XMLHttpRequest response text.
+    ``with``
+        A JavaScript expression specifying the parameters for the
+        XMLHttpRequest. This defaults to 'value', which in the evaluated
+        context refers to the new field value.
+    ``on``
+        Specifies which event handler to observe. By default, it's set to
+        "changed" for text fields and areas and "click" for radio buttons
+        and checkboxes. With this, you can specify it instead to be "blur"
+        or "focus" or any other event.
+    
+    Additionally, you may specify any of the options documented in
+    `link_to_remote <#link_to_remote>`_.
+    """
+    if options.get('frequency') > 0:
+        return build_observer('Form.Element.Observer', field_id, **options)
+    else:
+        return build_observer('Form.Element.EventObserver', field_id, **options)
+
+def observe_form(form_id, **options):
+    """
+    Like `observe_field <#observe_field>`_, but operates on an entire form
+    identified by the DOM ID ``form_id``.
+    
+    Keyword args are the same as observe_field, except the default value of
+    the ``with`` keyword evaluates to the serialized (request string) value
+    of the form.
+    """
+    if options.get('frequency'):
+        return build_observer('Form.Observer', form_id, **options)
+    else:
+        return build_observer('Form.EventObserver', form_id, **options)
+
+def options_for_ajax(options):
+    js_options = build_callbacks(options)
+    
+    js_options['asynchronous'] = str(options.get('type') != 'synchronous').lower()
+    if options.get('method'):
+        if isinstance(options['method'], str) and options['method'].startswith("'"):
+            js_options['method'] = options['method']
+        else:
+            js_options['method'] = "'%s'" % options['method']
+    if options.get('position'):
+        js_options['insertion'] = "Insertion.%s" % camelize(options['position'])
+    js_options['evalScripts'] = str(options.get('script') is None or options['script']).lower()
+    
+    if options.get('form'):
+        js_options['parameters'] = 'Form.serialize(this)'
+    elif options.get('submit'):
+        js_options['parameters'] = "Form.serialize('%s')" % options['submit']
+    elif options.get('with'):
+        js_options['parameters'] = options['with']
+    
+    return options_for_javascript(js_options)
+
+def build_observer(cls, name, **options):
+    if options.get('update') is True:
+        options['with'] = options.get('with', 'value')
+    callback = remote_function(**options)
+    javascript = "new %s('%s', " % (cls, name)
+    if options.get('frequency'): 
+        javascript += "%s, " % options['frequency']
+    javascript += "function(element, value) {%s})" % callback
+    return javascript_tag(javascript)
+
+def build_callbacks(options):
+    callbacks = {}
+    for callback, code in options.iteritems():
+        if callback in CALLBACKS:
+            name = 'on' + callback.title()
+            callbacks[name] = "function(request){%s}" % code
+    return callbacks
+
+__all__ = ['link_to_remote', 'periodically_call_remote', 'form_remote_tag', 'submit_to_remote', 'update_element_function',
+           'evaluate_remote_response', 'remote_function', 'observe_field', 'observe_form']

railshelpers/helpers/scriptaculous.py

+"""
+Scriptaculous Helpers
+
+Provides a set of helpers for calling Scriptaculous JavaScript 
+functions, including those which create Ajax controls and visual effects.
+
+To be able to use these helpers, you must include the Prototype 
+JavaScript framework and the Scriptaculous JavaScript library in your 
+pages.
+
+The Scriptaculous helpers' behavior can be tweaked with various options.
+See the documentation at http://script.aculo.us for more information on
+using these helpers in your application.
+"""
+from prototype import *
+from javascript import options_for_javascript, array_or_string_for_javascript
+from prototype import AJAX_OPTIONS, javascript_tag
+from tag import camelize
+
+def visual_effect(name, element_id=False, **js_options):
+    """
+    Returns a JavaScript snippet to be used on the Ajax callbacks for
+    starting visual effects.
+    
+    Example::
+    
+        <% link_to_remote("Reload",  
+                dict(url=url(action="reload"),
+                     update="posts",
+                     complete=visual_effect('highlight', "posts", duration=0.5))) %>
+    
+    If no element_id is given, it assumes "element" which should be a local
+    variable in the generated JavaScript execution context. This can be 
+    used for example with drop_receiving_element::
+    
+        <% drop_receving_element('some_element', loading=visual_effect('fade')) %>
+    
+    This would fade the element that was dropped on the drop receiving 
+    element.
+    
+    For toggling visual effects, you can use ``toggle_appear``, ``toggle_slide``, and
+    ``toggle_blind`` which will alternate between appear/fade, slidedown/slideup, and
+    blinddown/blindup respectively.
+    
+    You can change the behaviour with various options, see
+    http://script.aculo.us for more documentation.
+    """
+    element = (element_id and "'%s'" % element_id) or "element"
+    if js_options.has_key('queue'):
+        js_options['queue'] = "'%s'" % js_options['queue']
+    if 'toggle' in name:
+        return "Effect.toggle(%s,'%s',%s);" % (element, name.replace('toggle_',''), options_for_javascript(js_options))
+    return "new Effect.%s(%s,%s);" % (camelize(name), element, options_for_javascript(js_options))
+
+def parallel_effects(effects, **js_options):
+    """
+    Wraps visual effects so they occur in parallel
+    
+    Example::
+    
+        parallel_effects(
+            visual_effect('highlight, 'dom_id'),
+            visual_effect('fade', 'dom_id'),
+        )
+    """
+    return "new Effect.Parallel([%s], %s)" % (effects.join(''), options_for_javascript(js_options))
+
+def sortable_element(element_id, **options):
+    """
+    Makes the element with the DOM ID specified by ``element_id`` sortable.
+    
+    Uses drag-and-drop and makes an Ajax call whenever the sort order has
+    changed. By default, the action called gets the serialized sortable
+    element as parameters.
+    
+    Example::
+
+        <% sortable_element("my_list", url=url(action="order")) %>
+    
+    In the example, the action gets a "my_list" array parameter 
+    containing the values of the ids of elements the sortable consists 
+    of, in the current order.
+    
+    You can change the behaviour with various options, see
+    http://script.aculo.us for more documentation.
+    """
+    options.setdefault('with', "Sortable.serialize('%s')" % element_id)
+    options.setdefault('onUpdate', "function(){%s}" % remote_function(**options))
+    for k in options.keys():
+        if k in AJAX_OPTIONS: del options[k]
+    
+    for option in ['tag', 'overlap', 'constraint', 'handle']:
+        if options.has_key(option) and options[option]:
+            options[option] = "'%s'" % options[option]
+    
+    if options.has_key('containment'):
+        options['containment'] = array_or_string_for_javascript(options['containment'])
+    if options.has_key('only'):
+        options['only'] = array_or_string_for_javascript(options['only'])
+    
+    return javascript_tag("Sortable.create('%s', %s)" % (element_id, options_for_javascript(options)))
+
+def draggable_element(element_id, **options):
+    """
+    Makes the element with the DOM ID specified by ``element_id`` draggable.
+    
+    Example::
+
+        <% draggable_element("my_image", revert=True)
+    
+    You can change the behaviour with various options, see
+    http://script.aculo.us for more documentation.
+    """
+    return javascript_tag("new Draggable('%s', %s)" % (element_id, options_for_javascript(options)))
+
+def drop_receiving_element(element_id, **options):
+    """
+    Makes an element able to recieve dropped draggable elements
+    
+    Makes the element with the DOM ID specified by ``element_id`` receive
+    dropped draggable elements (created by draggable_element) and make an
+    AJAX call  By default, the action called gets the DOM ID of the element
+    as parameter.
+    
+    Example::
+    
+        <% drop_receiving_element("my_cart", url=(controller="cart", action="add" )) %>
+    
+    You can change the behaviour with various options, see
+    http://script.aculo.us for more documentation.    
+    """
+    options.setdefault('with', "'id=' + encodeURIComponent(element.id)")
+    options.setdefault('onDrop', "function(element){%s}" % remote_function(**options))
+    for k in options.keys():
+        if k in AJAX_OPTIONS: del options[k]
+    
+    if options.has_key('accept'):
+        options['accept'] = array_or_string_for_javascript(options['accept'])
+    if options.has_key('hoverclass'):
+        options['hoverclass'] = "'%s'" % options['hoverclass']
+    
+    return javascript_tag("Droppables.add('%s', %s)" % (element_id, options_for_javascript(options)))
+
+__all__ = ['visual_effect', 'parallel_effects', 'sortable_element', 'draggable_element', 'drop_receiving_element']

railshelpers/helpers/tag.py

+"""
+Tag Helpers
+"""
+from railshelpers.escapes import html_escape
+import re
+
+def camelize(name):
+    """
+    Camelize a ``name``
+    """
+    def upcase(matchobj):
+        return getattr(matchobj.group(0)[1:], 'upper')()
+    name = re.sub(r'(_[a-zA-Z])', upcase, name)
+    name = name[0].upper() + name[1:]
+    return name
+
+def strip_unders(options):
+    for x,y in options.iteritems():
+        if x.endswith('_'):
+            options[x[:-1]] = y
+            del options[x]
+
+def tag(name, open=False, **options):
+    """
+    Create a HTML tag of type ``name``
+    
+    ``open``
+        Set to True if the tag should remain open
+    
+    All additional keyword args become attribute/value's for the tag. To pass in Python
+    reserved words, append _ to the name of the key.
+    
+    Examples::
+    
+        >>> tag("br")
+        <br />
+        >>> tag("input", type="text")
+        <input type="text" />
+    """
+    tag = '<%s%s%s' % (name, (options and tag_options(**options)) or '', (open and '>') or ' />')
+    return tag
+
+def content_tag(name, content, **options):
+    """
+    Create a tag with content
+    
+    Takes the same keyword args as ``tag``
+    
+    Examples::
+    
+        >>> content_tag("p", "Hello world!")
+        <p>Hello world!</p>
+        >>> content_tag("div", content_tag("p", "Hello world!"), class_="strong")
+        <div class="strong"><p>Hello world!</p></div>
+    """
+    tag = '<%s%s>%s</%s>' % (name, (options and tag_options(**options)) or '', content, name)
+    return tag
+
+def cdata_section(content):
+    """
+    Returns a CDATA section for the given ``content``.
+    
+    CDATA sections are used to escape blocks of text containing characters which would
+    otherwise be recognized as markup. CDATA sections begin with the string
+    ``<![CDATA[`` and end with (and may not contain) the string 
+    ``]]>``. 
+    """
+    return "<![CDATA[%s]]>" % content
+
+def tag_options(**options):
+    strip_unders(options)
+    cleaned_options = convert_booleans(dict([(x, y) for x,y in options.iteritems() if y is not None]))
+    optionlist = ['%s="%s"' % (x, html_escape(str(y))) for x,y in cleaned_options.iteritems()]
+    optionlist.sort()
+    if optionlist:
+        return ' ' + ' '.join(optionlist)
+    else:
+        return ''
+
+def convert_booleans(options):
+    for attr in ['disabled', 'readonly', 'multiple']:
+        boolean_attribute(options, attr)
+    return options
+
+def boolean_attribute(options, attribute):
+    if options.get(attribute):
+        options[attribute] = attribute
+    elif options.has_key(attribute):
+        del options[attribute]
+
+__all__ = ['tag', 'content_tag', 'cdata_section', 'camelize']

railshelpers/helpers/text.py

+"""
+Text Helpers
+"""
+from routes import request_config
+from tag import content_tag, tag_options
+import itertools, re
+
+AUTO_LINK_RE = re.compile("""(<\w+.*?>|[^=!:'"\/]|^)((?:http[s]?:\/\/)|(?:www\.))(([\w]+:?[=?&\/.-]?)*\w+[\/]?(?:\#\w*)?)([\.,"'?!;:]|\s|<|$)""")
+    
+def iterdict(items):
+    return dict(items=items, iter=itertools.cycle(items))
+
+def cycle(*args, **kargs):
+    """
+    Returns the next cycle of the given list
+    
+    Everytime ``cycle`` is called, the value returned will be the next item
+    in the list passed to it. This list is reset on every request, but can
+    also be reset by calling ``reset_cycle()``.
+    
+    You may specify the list as either arguments, or as a single list argument.
+    
+    This can be used to alternate classes for table rows::
+    
+        # In Myghty...
+        % for item in items:
+        <tr class="<% cycle("even", "odd") %>">
+            ... use item ...
+        </tr>
+        % #endfor
+    
+    You can use named cycles to prevent clashes in nested loops. You'll
+    have to reset the inner cycle, manually::
+    
+        % for item in items:
+        <tr class="<% cycle("even", "odd", name="row_class") %>
+            <td>
+        %     for value in item.values:
+                <span style="color:'<% cycle("red", "green", "blue",
+                                             name="colors") %>'">
+                            item
+                </span>
+        %     #endfor
+            <% reset_cycle("colors") %>
+            </td>
+        </tr>
+        % #endfor
+    """
+    if len(args) > 1:
+        items = args
+    else:
+        items = args[0]
+    name = kargs.get('name', 'default')
+    cycles = request_config().environ.setdefault('railshelpers.cycles', {})
+    
+    cycle = cycles.setdefault(name, iterdict(items))
+    
+    if cycles[name].get('items') != items:
+        cycle = cycles[name] = iterdict(items)
+    return cycle['iter'].next()
+
+def reset_cycle(name='default'):
+    """
+    Resets a cycle
+    
+    Resets the cycle so that it starts from the first element in the array
+    the next time it is used.
+    """
+    del request_config().environ['railshelpers.cycles'][name]
+
+def truncate(text, length=30, truncate_string='...'):
+    """
+    Truncates ``text`` with replacement characters
+    
+    ``length``
+        The maximum length of ``text`` before replacement
+    ``truncate_string``
+        If ``text`` exceeds the ``length``, this string will replace
+        the end of the string
+    """
+    if not text: return ''
+    
+    new_len = length-len(truncate_string)
+    if len(text) > length:
+        return text[:new_len] + truncate_string
+    else:
+        return text
+
+def highlight(text, phrase, hilighter='<strong class="hilight">\\1</strong>'):
+    """
+    Highlights the ``phrase`` where it is found in the ``text``
+    
+    The highlighted phrase will be surrounded by the hilighter, by default::
+    
+        <strong class="highlight">I'm a highlight phrase</strong>
+    
+    ``highlighter``
+        Defines the highlighting phrase. This argument should be a single-quoted string
+        with ``\\1`` where the phrase is supposed to be inserted.
+        
+    Note: The ``phrase`` is sanitized to include only letters, digits, and spaces before use.
+    """
+    if not phrase or not text:
+        return text
+    return re.sub(re.compile('(%s)' % re.escape(phrase)), hilighter, text, re.I)
+
+def excerpt(text, phrase, radius=100, excerpt_string="..."):
+    """
+    Extracts an excerpt from the ``text``
+    
+    ``phrase``
+        Phrase to excerpt from ``text``
+    ``radius``
+        How many surrounding characters to include
+    ``excerpt_string``
+        Characters surrounding entire excerpt
+    
+    Example::
+    
+        >>> excerpt("hello my world", "my", 3)
+        "...lo my wo..."
+    """
+    if not text or not phrase:
+        return text
+    
+    pat = re.compile('(.{0,%s}%s.{0,%s})' % (radius, re.escape(phrase), radius))
+    match = pat.search(text)
+    if not match:
+        return ""
+    return excerpt_string + match.expand(r'\1') + excerpt_string
+
+def word_wrap(text, line_width=80):
+    """
+    Word wrap long lines to ``line_width``
+    """
+    text = re.sub(r'\n', '\n\n', text)
+    return re.sub(r'(.{1,%s})(\s+|$)' % line_width, r'\1\n', text).strip()
+
+def simple_format(text):
+    """
+    Returns ``text`` transformed into HTML using very simple formatting rules
+    
+    Surrounds paragraphs with ``<p>`` tags, and converts line breaks into ``<br />``
+    Two consecutive newlines(``\\n\\n``) are considered as a paragraph, one newline (``\\n``) is
+    considered a linebreak, three or more consecutive newlines are turned into two newlines.
+    """
+    text = re.sub(r'(\r\n|\n|\r)', r'\n', text)
+    text = re.sub(r'\n\n+', r'\n\n', text)
+    text = re.sub(r'(\n\n)', r'</p>\1<p>', text)
+    text = re.sub(r'([^\n])(\n)([^\n])', r'\1\2<br />\3', text)
+    text = content_tag("p", text).replace('</p><p></p>', '</p>')
+    text = re.sub(r'</p><p>', r'</p>\n<p>', text)
+    return text
+
+def auto_link(text, link="all", **href_options):
+    """
+    Turns all urls and email addresses into clickable links. 
+    
+    ``link``
+        Used to determine what to link. Options are "all", "email_addresses", or "urls"
+    
+    Example::
+    
+        >>> auto_link("Go to http://www.planetpython.com and say hello to guido@python.org")
+        'Go to <a href="http://www.planetpython.com">http://www.planetpython.com</a> and say
+        hello to <a href="mailto:guido@python.org">guido@python.org</a>'
+    """
+    if not text:
+        return ""
+    if link == "all":
+        return auto_link_urls(auto_link_email_addresses(text), **href_options)
+    elif link == "email_addresses":
+        return auto_link_email_addresses(text)
+    else:
+        return auto_link_urls(text, **href_options)
+
+def auto_link_urls(text, **href_options):
+    extra_options = tag_options(**href_options)
+    def handle_match(matchobj):
+        all = matchobj.group()
+        a, b, c, d = matchobj.group(1,2,3,5)
+        if re.match(r'<a\s', a, re.I):
+            return all
+        text = b + c
+        if b == "www.":
+            b = "http://www."
+        return '%s<a href="%s%s"%s>%s</a>%s' % (a, b, c, extra_options, text, d)
+    return re.sub(AUTO_LINK_RE, handle_match, text)
+
+def auto_link_email_addresses(text):
+    def fix_email(match):
+        text = matchobj.group()
+        return '<a href="mailto:%s>%s</a>' % (text, text)
+    return re.sub(r'([\w\.!#\$%\-+.]+@[A-Za-z0-9\-]+(\.[A-Za-z0-9\-]+)+)', r'<a href="mailto:\1">\1</a>', text)
+
+def strip_links(text):
+    """
+    Turns all links into words
+    
+    Example::
+    
+        >>> strip_links("<a href="something">else</a>")
+        "else"
+    """
+    return re.sub(r'<a.*?>(.*)</a>', r'\1', text, re.M)
+
+__all__ = ['cycle', 'reset_cycle', 'truncate', 'highlight', 'excerpt', 'word_wrap', 'simple_format',
+           'auto_link', 'strip_links']

railshelpers/helpers/url.py

+"""
+URL Helpers
+"""
+import cgi
+
+from railshelpers.escapes import html_escape
+
+from routes import url_for, request_config
+from javascript import *
+from tag import *
+from tag import tag_options
+
+def get_url(url):
+    if callable(url):
+        return url()
+    else:
+        return url
+
+def url(*args, **kargs):
+    """
+    Lazily evaluates url_for() arguments
+    
+    Used instead of url_for() for functions so that the function will be evaluated
+    in a lazy manner rather than at initial function call.
+    """
+    args = args
+    kargs = kargs
+    def call():
+        return url_for(*args, **kargs)
+    return call
+
+def link_to(name, url='', **html_options):
+    """
+    Creates a link tag of the given ``name`` using an URL created by the set of ``options``.
+    
+    See the valid options in the documentation for Routes url_for.
+    
+    The html_options has three special features. One for creating javascript confirm alerts where if you pass
+    ``confirm='Are you sure?'`` , the link will be guarded with a JS popup asking that question. If the user
+    accepts, the link is processed, otherwise not.
+    
+    Another for creating a popup window, which is done by either passing ``popup`` with True or the options
+    of the window in Javascript form.
+    
+    And a third for making the link do a POST request (instead of the regular GET) through a dynamically added
+    form element that is instantly submitted. Note that if the user has turned off Javascript, the request will
+    fall back on the GET. So its your responsibility to determine what the action should be once it arrives at
+    the controller. The POST form is turned on by passing ``post`` as True. Note, it's not possible to use POST
+    requests and popup targets at the same time (an exception will be thrown).
+    
+    Examples::
+    
+        >>> link_to("Delete this page", url(action="destroy", id=4), confirm="Are you sure?")
+        >>> link_to("Help", url(action="help"), popup=True)
+        >>> link_to("Busy loop", url(action="busy"), popup=['new_window', 'height=300,width=600'])
+        >>> link_to("Destroy account", url(action="destroy"), confirm="Are you sure?", post => True)
+    """
+    if html_options:
+        html_options = convert_options_to_javascript(**html_options)
+        tag_op = tag_options(**html_options)
+    else:
+        tag_op = ''
+    if callable(url):
+        url = url()
+    else:
+        url = html_escape(url)
+    return "<a href=\"%s\"%s>%s</a>" % (url, tag_op, name or url)
+
+def button_to(name, url='', **html_options):
+    """
+    Generates a form containing a sole button that submits to the
+    URL given by ``url``.  
+    
+    Use this method instead of ``link_to`` for actions that do not have the safe HTTP GET semantics
+    implied by using a hypertext link.
+    
+    The parameters are the same as for ``link_to``.  Any ``html_options`` that you pass will be
+    applied to the inner ``input`` element.
+    In particular, pass
+    
+        disabled = True/False
+    
+    as part of ``html_options`` to control whether the button is
+    disabled.  The generated form element is given the class
+    'button-to', to which you can attach CSS styles for display
+    purposes.
+    
+    Example 1::
+    
+        # inside of controller for "feeds"
+        >>> button_to("Edit", url(action='edit', id=3))
+        <form method="post" action="/feeds/edit/3" class="button-to">
+        <div><input value="Edit" type="submit" /></div>
+        </form>
+    
+    Example 2::
+    
+        >> button_to("Destroy", url(action='destroy', id=3), confirm="Are you sure?")
+        <form method="post" action="/feeds/destroy/3" class="button-to">
+        <div><input onclick="return confirm('Are you sure?');" value="Destroy" type="submit" />
+        </div>
+        </form>
+    
+    *NOTE*: This method generates HTML code that represents a form.
+    Forms are "block" content, which means that you should not try to
+    insert them into your HTML where only inline content is expected.
+    For example, you can legally insert a form inside of a ``div`` or
+    ``td`` element or in between ``p`` elements, but not in the middle of
+    a run of text, nor can you place a form within another form.
+    (Bottom line: Always validate your HTML before going public.)    
+    """
+    if html_options:
+        convert_boolean_attributes(html_options, ['disabled'])
+    
+    confirm = html_options.get('confirm')
+    if confirm:
+        del html_options['confirm']
+        html_options['onclick'] = "return %s;" % confirm_javascript_function(confirm)
+    
+    if callable(url):
+        ur = url()
+        url, name = ur, name or html_escape(ur)
+    else:
+        url, name = url, name or url
+    
+    html_options.update(dict(type='submit', value=name))
+    
+    return """<form method="post" action="%s" class="button-to"><div>""" % html_escape(url) + \
+           tag("input", **html_options) + "</div></form>"
+
+def link_to_unless_current(name, url, **html_options):
+    """
+    Conditionally create a link tag of the given ``name`` using the ``url``
+    
+    If the current request uri is the same as the link's only the name is returned. This is useful
+    for creating link bars where you don't want to link to the page currently being viewed.
+    """
+    return link_to_unless(current_page(url), name, url, **html_options)
+
+def link_to_unless(condition, name, url, **html_options):
+    """
+    Conditionally create a link tag of the given ``name`` using the ``url``
+    
+    If ``condition`` is false only the name is returned.
+    """
+    if condition:
+        return name
+    else:
+        return link_to(name, url, **html_options)
+
+def link_to_if(condition, name, url, **html_options):
+    """
+    Conditionally create a link tag of the given ``name`` using the ``url`` 
+    
+    If ``condition`` is True only the name is returned.
+    """
+    link_to_unless(not condition, name, url, **html_options)
+
+def parse_querystring(environ):
+    source = environ.get('QUERY_STRING', '')
+    parsed = cgi.parse_qsl(source, keep_blank_values=True,
+                           strict_parsing=False)
+    return parsed
+
+def current_page(url):
+    """
+    Returns true if the current page uri is equivilant to ``url``
+    """
+    config = request_config()
+    environ = config.environ
+    curopts = config.mapper_dict.copy()
+    if environ.get('REQUEST_METHOD', 'GET') == 'GET':
+        if environ.has_key('QUERY_STRING'):
+            curopts.update(parse_querystring(environ))
+    currl = url_for(**curopts)
+    if callable(url):
+        return url() == currl
+    else:
+        return url == currl
+
+def convert_options_to_javascript(confirm=None, popup=None, post=None, **html_options):
+    if popup and post:
+        raise "You can't use popup and post in the same link"
+    elif confirm and popup:
+        oc = "if (%s) { %s };return false;" % (confirm_javascript_function(confirm), 
+                                               popup_javascript_function(popup))
+    elif confirm and post:
+        oc = "if (%s) { %s };return false;" % (confirm_javascript_function(confirm),
+                                               post_javascript_function())
+    elif confirm:
+        oc = "return %s;" % confirm_javascript_function(confirm)
+    elif post:
+        oc = "%sreturn false;" % post_javascript_function()
+    elif popup:
+        oc = popup_javascript_function(popup) + 'return false;'
+    else:
+        oc = html_options.get('onclick')
+    html_options['onclick'] = oc
+    return html_options
+    
+def convert_boolean_attributes(html_options, bool_attrs):
+    for attr in bool_attrs:
+        if html_options.has_key(attr) and html_options[attr]:
+            html_options[attr] = attr
+        elif html_options.has_key(attr):
+            del html_options[attr]
+
+def confirm_javascript_function(confirm):
+    return "confirm('%s')" % escape_javascript(confirm)
+
+def popup_javascript_function(popup):
+    if isinstance(popup, list):
+        return "window.open(this.href,'%s','%s');" % (popup[0], popup[-1])
+    else:
+        return "window.open(this.href);"
+
+def post_javascript_function():
+    return "f = document.createElement('form'); document.body.appendChild(f); f.method = 'POST'; f.action = this.href; f.submit();"
+
+__all__ = ['url', 'link_to', 'button_to', 'link_to_unless_current', 'link_to_unless', 'link_to_if',
+           'current_page']

railshelpers/javascripts/builder.js

+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//
+// See scriptaculous.js for full license.
+
+var Builder = {
+  NODEMAP: {
+    AREA: 'map',
+    CAPTION: 'table',
+    COL: 'table',
+    COLGROUP: 'table',
+    LEGEND: 'fieldset',
+    OPTGROUP: 'select',
+    OPTION: 'select',
+    PARAM: 'object',
+    TBODY: 'table',
+    TD: 'table',
+    TFOOT: 'table',
+    TH: 'table',
+    THEAD: 'table',
+    TR: 'table'
+  },
+  // note: For Firefox < 1.5, OPTION and OPTGROUP tags are currently broken,
+  //       due to a Firefox bug
+  node: function(elementName) {
+    elementName = elementName.toUpperCase();
+    
+    // try innerHTML approach
+    var parentTag = this.NODEMAP[elementName] || 'div';
+    var parentElement = document.createElement(parentTag);
+    try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+      parentElement.innerHTML = "<" + elementName + "></" + elementName + ">";
+    } catch(e) {}
+    var element = parentElement.firstChild || null;
+      
+    // see if browser added wrapping tags
+    if(element && (element.tagName != elementName))
+      element = element.getElementsByTagName(elementName)[0];
+    
+    // fallback to createElement approach
+    if(!element) element = document.createElement(elementName);
+    
+    // abort if nothing could be created
+    if(!element) return;
+
+    // attributes (or text)
+    if(arguments[1])
+      if(this._isStringOrNumber(arguments[1]) ||
+        (arguments[1] instanceof Array)) {
+          this._children(element, arguments[1]);
+        } else {
+          var attrs = this._attributes(arguments[1]);
+          if(attrs.length) {
+            try { // prevent IE "feature": http://dev.rubyonrails.org/ticket/2707
+              parentElement.innerHTML = "<" +elementName + " " +
+                attrs + "></" + elementName + ">";
+            } catch(e) {}
+            element = parentElement.firstChild || null;
+            // workaround firefox 1.0.X bug
+            if(!element) {
+              element = document.createElement(elementName);
+              for(attr in arguments[1]) 
+                element[attr == 'class' ? 'className' : attr] = arguments[1][attr];
+            }
+            if(element.tagName != elementName)
+              element = parentElement.getElementsByTagName(elementName)[0];
+            }
+        } 
+
+    // text, or array of children
+    if(arguments[2])
+      this._children(element, arguments[2]);
+
+     return element;
+  },
+  _text: function(text) {
+     return document.createTextNode(text);
+  },
+  _attributes: function(attributes) {
+    var attrs = [];
+    for(attribute in attributes)
+      attrs.push((attribute=='className' ? 'class' : attribute) +
+          '="' + attributes[attribute].toString().escapeHTML() + '"');
+    return attrs.join(" ");
+  },
+  _children: function(element, children) {
+    if(typeof children=='object') { // array can hold nodes and text
+      children.flatten().each( function(e) {
+        if(typeof e=='object')
+          element.appendChild(e)
+        else
+          if(Builder._isStringOrNumber(e))
+            element.appendChild(Builder._text(e));
+      });
+    } else
+      if(Builder._isStringOrNumber(children)) 
+         element.appendChild(Builder._text(children));
+  },
+  _isStringOrNumber: function(param) {
+    return(typeof param=='string' || typeof param=='number');
+  }
+}

railshelpers/javascripts/controls.js

+// Copyright (c) 2005 Thomas Fuchs (http://script.aculo.us, http://mir.aculo.us)
+//           (c) 2005 Ivan Krstic (http://blogs.law.harvard.edu/ivan)
+//           (c) 2005 Jon Tirsen (http://www.tirsen.com)
+// Contributors:
+//  Richard Livsey
+//  Rahul Bhargava
+//  Rob Wills
+// 
+// See scriptaculous.js for full license.
+
+// Autocompleter.Base handles all the autocompletion functionality 
+// that's independent of the data source for autocompletion. This
+// includes drawing the autocompletion menu, observing keyboard
+// and mouse events, and similar.
+//
+// Specific autocompleters need to provide, at the very least, 
+// a getUpdatedChoices function that will be invoked every time
+// the text inside the monitored textbox changes. This method 
+// should get the text for which to provide autocompletion by
+// invoking this.getToken(), NOT by directly accessing
+// this.element.value. This is to allow incremental tokenized
+// autocompletion. Specific auto-completion logic (AJAX, etc)
+// belongs in getUpdatedChoices.
+//
+// Tokenized incremental autocompletion is enabled automatically
+// when an autocompleter is instantiated with the 'tokens' option
+// in the options parameter, e.g.:
+// new Ajax.Autocompleter('id','upd', '/url/', { tokens: ',' });
+// will incrementally autocomplete with a comma as the token.
+// Additionally, ',' in the above example can be replaced with
+// a token array, e.g. { tokens: [',', '\n'] } which
+// enables autocompletion on multiple tokens. This is most 
+// useful when one of the tokens is \n (a newline), as it 
+// allows smart autocompletion after linebreaks.
+
+var Autocompleter = {}
+Autocompleter.Base = function() {};
+Autocompleter.Base.prototype = {
+  baseInitialize: function(element, update, options) {
+    this.element     = $(element); 
+    this.update      = $(update);  
+    this.hasFocus    = false; 
+    this.changed     = false; 
+    this.active      = false; 
+    this.index       = 0;     
+    this.entryCount  = 0;
+
+    if (this.setOptions)
+      this.setOptions(options);
+    else
+      this.options = options || {};
+
+    this.options.paramName    = this.options.paramName || this.element.name;
+    this.options.tokens       = this.options.tokens || [];
+    this.options.frequency    = this.options.frequency || 0.4;
+    this.options.minChars     = this.options.minChars || 1;
+    this.options.onShow       = this.options.onShow || 
+    function(element, update){ 
+      if(!update.style.position || update.style.position=='absolute') {
+        update.style.position = 'absolute';
+        Position.clone(element, update, {setHeight: false, offsetTop: element.offsetHeight});
+      }
+      Effect.Appear(update,{duration:0.15});
+    };
+    this.options.onHide = this.options.onHide || 
+    function(element, update){ new Effect.Fade(update,{duration:0.15}) };
+
+    if (typeof(this.options.tokens) == 'string') 
+      this.options.tokens = new Array(this.options.tokens);
+
+    this.observer = null;
+    
+    this.element.setAttribute('autocomplete','off');
+
+    Element.hide(this.update);
+
+    Event.observe(this.element, "blur", this.onBlur.bindAsEventListener(this));
+    Event.observe(this.element, "keypress", this.onKeyPress.bindAsEventListener(this));
+  },
+
+  show: function() {
+    if(Element.getStyle(this.update, 'display')=='none') this.options.onShow(this.element, this.update);
+    if(!this.iefix && 
+      (navigator.appVersion.indexOf('MSIE')>0) &&
+      (navigator.userAgent.indexOf('Opera')<0) &&
+      (Element.getStyle(this.update, 'position')=='absolute')) {
+      new Insertion.After(this.update, 
+       '<iframe id="' + this.update.id + '_iefix" '+
+       'style="display:none;position:absolute;filter:progid:DXImageTransform.Microsoft.Alpha(opacity=0);" ' +
+       'src="javascript:false;" frameborder="0" scrolling="no"></iframe>');
+      this.iefix = $(this.update.id+'_iefix');
+    }
+    if(this.iefix) setTimeout(this.fixIEOverlapping.bind(this), 50);
+  },
+  
+  fixIEOverlapping: function() {
+    Position.clone(this.update, this.iefix);
+    this.iefix.style.zIndex = 1;
+    this.update.style.zIndex = 2;
+    Element.show(this.iefix);
+  },
+
+  hide: function() {
+    this.stopIndicator();
+    if(Element.getStyle(this.update, 'display')!='none') this.options.onHide(this.element, this.update);
+    if(this.iefix) Element.hide(this.iefix);
+  },
+
+  startIndicator: function() {
+    if(this.options.indicator) Element.show(this.options.indicator);
+  },
+
+  stopIndicator: function() {
+    if(this.options.indicator) Element.hide(this.options.indicator);
+  },
+
+  onKeyPress: function(event) {
+    if(this.active)
+      switch(event.keyCode) {
+       case Event.KEY_TAB:
+       case Event.KEY_RETURN:
+         this.selectEntry();
+         Event.stop(event);
+       case Event.KEY_ESC:
+         this.hide();
+         this.active = false;
+         Event.stop(event);
+         return;
+       case Event.KEY_LEFT:
+       case Event.KEY_RIGHT:
+         return;
+       case Event.KEY_UP:
+         this.markPrevious();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+       case Event.KEY_DOWN:
+         this.markNext();
+         this.render();
+         if(navigator.appVersion.indexOf('AppleWebKit')>0) Event.stop(event);
+         return;
+      }
+     else 
+      if(event.keyCode==Event.KEY_TAB || event.keyCode==Event.KEY_RETURN) 
+        return;
+
+    this.changed = true;
+    this.hasFocus = true;
+
+    if(this.observer) clearTimeout(this.observer);
+      this.observer = 
+        setTimeout(this.onObserverEvent.bind(this), this.options.frequency*1000);
+  },
+
+  onHover: function(event) {
+    var element = Event.findElement(event, 'LI');
+    if(this.index != element.autocompleteIndex) 
+    {
+        this.index = element.autocompleteIndex;
+        this.render();
+    }
+    Event.stop(event);
+  },
+  
+  onClick: function(event) {
+    var element = Event.findElement(event, 'LI');
+    this.index = element.autocompleteIndex;
+    this.selectEntry();
+    this.hide();
+  },
+  
+  onBlur: function(event) {
+    // needed to make click events working
+    setTimeout(this.hide.bind(this), 250);
+    this.hasFocus = false;
+    this.active = false;     
+  }, 
+  
+  render: function() {
+    if(this.entryCount > 0) {
+      for (var i = 0; i < this.entryCount; i++)
+        this.index==i ? 
+          Element.addClassName(this.getEntry(i),"selected") : 
+          Element.removeClassName(this.getEntry(i),"selected");
+        
+      if(this.hasFocus) { 
+        this.show();
+        this.active = true;
+      }
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+  
+  markPrevious: function() {
+    if(this.index > 0) this.index--
+      else this.index = this.entryCount-1;
+  },
+  
+  markNext: function() {
+    if(this.index < this.entryCount-1) this.index++
+      else this.index = 0;
+  },
+  
+  getEntry: function(index) {
+    return this.update.firstChild.childNodes[index];
+  },
+  
+  getCurrentEntry: function() {
+    return this.getEntry(this.index);
+  },
+  
+  selectEntry: function() {
+    this.active = false;
+    this.updateElement(this.getCurrentEntry());
+  },
+
+  updateElement: function(selectedElement) {
+    if (this.options.updateElement) {
+      this.options.updateElement(selectedElement);
+      return;
+    }
+    var value = '';
+    if (this.options.select) {
+      var nodes = document.getElementsByClassName(this.options.select, selectedElement) || [];
+      if(nodes.length>0) value = Element.collectTextNodes(nodes[0], this.options.select);
+    } else
+      value = Element.collectTextNodesIgnoreClass(selectedElement, 'informal');
+    
+    var lastTokenPos = this.findLastToken();
+    if (lastTokenPos != -1) {
+      var newValue = this.element.value.substr(0, lastTokenPos + 1);
+      var whitespace = this.element.value.substr(lastTokenPos + 1).match(/^\s+/);
+      if (whitespace)
+        newValue += whitespace[0];
+      this.element.value = newValue + value;
+    } else {
+      this.element.value = value;
+    }
+    this.element.focus();
+    
+    if (this.options.afterUpdateElement)
+      this.options.afterUpdateElement(this.element, selectedElement);
+  },
+
+  updateChoices: function(choices) {
+    if(!this.changed && this.hasFocus) {
+      this.update.innerHTML = choices;
+      Element.cleanWhitespace(this.update);
+      Element.cleanWhitespace(this.update.firstChild);
+
+      if(this.update.firstChild && this.update.firstChild.childNodes) {
+        this.entryCount = 
+          this.update.firstChild.childNodes.length;
+        for (var i = 0; i < this.entryCount; i++) {
+          var entry = this.getEntry(i);
+          entry.autocompleteIndex = i;
+          this.addObservers(entry);
+        }
+      } else { 
+        this.entryCount = 0;
+      }
+
+      this.stopIndicator();
+
+      this.index = 0;
+      this.render();
+    }
+  },
+
+  addObservers: function(element) {
+    Event.observe(element, "mouseover", this.onHover.bindAsEventListener(this));
+    Event.observe(element, "click", this.onClick.bindAsEventListener(this));
+  },
+
+  onObserverEvent: function() {
+    this.changed = false;   
+    if(this.getToken().length>=this.options.minChars) {
+      this.startIndicator();
+      this.getUpdatedChoices();
+    } else {
+      this.active = false;
+      this.hide();
+    }
+  },
+
+  getToken: function() {
+    var tokenPos = this.findLastToken();
+    if (tokenPos != -1)
+      var ret = this.element.value.substr(tokenPos + 1).replace(/^\s+/,'').replace(/\s+$/,'');
+    else
+      var ret = this.element.value;
+
+    return /\n/.test(ret) ? '' : ret;
+  },
+
+  findLastToken: function() {
+    var lastTokenPos = -1;
+
+    for (var i=0; i<this.options.tokens.length; i++) {
+      var thisTokenPos = this.element.value.lastIndexOf(this.options.tokens[i]);
+      if (thisTokenPos > lastTokenPos)
+        lastTokenPos = thisTokenPos;
+    }
+    return lastTokenPos;
+  }
+}
+
+Ajax.Autocompleter = Class.create();
+Object.extend(Object.extend(Ajax.Autocompleter.prototype, Autocompleter.Base.prototype), {
+  initialize: function(element, update, url, options) {