WebHelpers-1.3/0000775000175000017500000000000011542603374013006 5ustar sluggosluggoWebHelpers-1.3/setup.py0000664000175000017500000000357611542603250014524 0ustar sluggosluggotry: from setuptools import setup, find_packages except ImportError: from ez_setup import use_setuptools use_setuptools() from setuptools import setup, find_packages version = '1.3' setup( name="WebHelpers", version=version, description='Web Helpers', long_description=""" Web Helpers is a library of helper functions intended to make writing web applications easier. It's the standard function library for Pylons and TurboGears 2, but can be used with any web framework. It also contains a large number of functions not specific to the web, including text processing, number formatting, date calculations, container objects, etc. Version 1.3 fixes a performance regression in 1.2 regarding paginate with SQLAlchemy. WebHelpers itself depends only on MarkupSafe, but certain helpers depend on third-party packages as described in the docs. The development version of WebHelpers is at http://bitbucket.org/bbangert/webhelpers (Mercurial) """, author='Mike Orr, Ben Bangert, Phil Jenvey', author_email='sluggoster@gmail.com, ben@groovie.org, pjenvey@groovie.org', url='http://webhelpers.groovie.org/', packages=find_packages(exclude=['ez_setup']), zip_safe=False, include_package_data=True, install_requires=[ 'MarkupSafe>=0.9.2', ], tests_require=[ 'Nose', 'Routes', 'WebOb', ], test_suite='nose.collector', classifiers=["Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python", "Topic :: Internet :: WWW/HTTP :: Dynamic Content", "Topic :: Software Development :: Libraries :: Python Modules", ], entry_points=""" [buildutils.optional_commands] compress_resources = webhelpers.commands """, ) WebHelpers-1.3/PKG-INFO0000664000175000017500000000256211542603374014110 0ustar sluggosluggoMetadata-Version: 1.0 Name: WebHelpers Version: 1.3 Summary: Web Helpers Home-page: http://webhelpers.groovie.org/ Author: Mike Orr, Ben Bangert, Phil Jenvey Author-email: sluggoster@gmail.com, ben@groovie.org, pjenvey@groovie.org License: UNKNOWN Description: Web Helpers is a library of helper functions intended to make writing web applications easier. It's the standard function library for Pylons and TurboGears 2, but can be used with any web framework. It also contains a large number of functions not specific to the web, including text processing, number formatting, date calculations, container objects, etc. Version 1.3 fixes a performance regression in 1.2 regarding paginate with SQLAlchemy. WebHelpers itself depends only on MarkupSafe, but certain helpers depend on third-party packages as described in the docs. The development version of WebHelpers is at http://bitbucket.org/bbangert/webhelpers (Mercurial) Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules WebHelpers-1.3/WebHelpers.egg-info/0000775000175000017500000000000011542603374016540 5ustar sluggosluggoWebHelpers-1.3/WebHelpers.egg-info/PKG-INFO0000664000175000017500000000256211542603374017642 0ustar sluggosluggoMetadata-Version: 1.0 Name: WebHelpers Version: 1.3 Summary: Web Helpers Home-page: http://webhelpers.groovie.org/ Author: Mike Orr, Ben Bangert, Phil Jenvey Author-email: sluggoster@gmail.com, ben@groovie.org, pjenvey@groovie.org License: UNKNOWN Description: Web Helpers is a library of helper functions intended to make writing web applications easier. It's the standard function library for Pylons and TurboGears 2, but can be used with any web framework. It also contains a large number of functions not specific to the web, including text processing, number formatting, date calculations, container objects, etc. Version 1.3 fixes a performance regression in 1.2 regarding paginate with SQLAlchemy. WebHelpers itself depends only on MarkupSafe, but certain helpers depend on third-party packages as described in the docs. The development version of WebHelpers is at http://bitbucket.org/bbangert/webhelpers (Mercurial) Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: BSD License Classifier: Programming Language :: Python Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: Software Development :: Libraries :: Python Modules WebHelpers-1.3/WebHelpers.egg-info/entry_points.txt0000664000175000017500000000012511542603374022034 0ustar sluggosluggo [buildutils.optional_commands] compress_resources = webhelpers.commands WebHelpers-1.3/WebHelpers.egg-info/dependency_links.txt0000664000175000017500000000000111542603374022606 0ustar sluggosluggo WebHelpers-1.3/WebHelpers.egg-info/top_level.txt0000664000175000017500000000001311542603374021264 0ustar sluggosluggowebhelpers WebHelpers-1.3/WebHelpers.egg-info/SOURCES.txt0000664000175000017500000001112111542603374020420 0ustar sluggosluggoCHANGELOG LICENSE MANIFEST.in README.txt TODO requirements.txt setup.cfg setup.py WebHelpers.egg-info/PKG-INFO WebHelpers.egg-info/SOURCES.txt WebHelpers.egg-info/dependency_links.txt WebHelpers.egg-info/entry_points.txt WebHelpers.egg-info/not-zip-safe WebHelpers.egg-info/requires.txt WebHelpers.egg-info/top_level.txt docs/Makefile docs/build docs/changelog.rst docs/conf.py docs/contents.rst docs/development.rst docs/history.rst docs/index.rst docs/third_party.rst docs/todo.rst docs/whats_new.rst docs/_static/webhelpers-logo.png docs/_themes/.gitignore docs/_themes/CONTRIBUTORS.txt docs/_themes/LICENSE.txt docs/_themes/README.rst docs/_themes/pylons_theme_support.py docs/_themes/.git/HEAD docs/_themes/.git/config docs/_themes/.git/description docs/_themes/.git/index docs/_themes/.git/packed-refs docs/_themes/.git/hooks/applypatch-msg.sample docs/_themes/.git/hooks/commit-msg.sample docs/_themes/.git/hooks/post-commit.sample docs/_themes/.git/hooks/post-receive.sample docs/_themes/.git/hooks/post-update.sample docs/_themes/.git/hooks/pre-applypatch.sample docs/_themes/.git/hooks/pre-commit.sample docs/_themes/.git/hooks/pre-rebase.sample docs/_themes/.git/hooks/prepare-commit-msg.sample docs/_themes/.git/hooks/update.sample docs/_themes/.git/info/exclude docs/_themes/.git/logs/HEAD docs/_themes/.git/logs/refs/heads/master docs/_themes/.git/objects/pack/pack-7223556f5a85a1e44a4a475abe01be5e042c09f5.idx docs/_themes/.git/objects/pack/pack-7223556f5a85a1e44a4a475abe01be5e042c09f5.pack docs/_themes/.git/refs/heads/master docs/_themes/.git/refs/remotes/origin/HEAD docs/_themes/pylons/layout.html docs/_themes/pylons/theme.conf docs/_themes/pylons/static/dialog-note.png docs/_themes/pylons/static/dialog-seealso.png docs/_themes/pylons/static/dialog-topic.png docs/_themes/pylons/static/dialog-warning.png docs/_themes/pylons/static/epub.css docs/_themes/pylons/static/footerbg.png docs/_themes/pylons/static/headerbg.png docs/_themes/pylons/static/ie6.css docs/_themes/pylons/static/middlebg.png docs/_themes/pylons/static/pylons-latex.png docs/_themes/pylons/static/pylons-small.png docs/_themes/pylons/static/pylons.css_t docs/_themes/pylons/static/pylons.ico docs/_themes/pylons/static/pylons.png docs/_themes/pylons/static/transparent.gif docs/_themes/pylonsfw/theme.conf docs/_themes/pylonsfw/static/pylons.ico docs/_themes/pylonsfw/static/pylonsfw-small.png docs/_themes/pyramid/theme.conf docs/_themes/pyramid/static/pyramid-latex.png docs/_themes/pyramid/static/pyramid-small.png docs/_themes/pyramid/static/pyramid.ico docs/_themes/pyramid/static/pyramid.png docs/modules/constants.rst docs/modules/containers.rst docs/modules/date.rst docs/modules/feedgenerator.rst docs/modules/markdown.rst docs/modules/media.rst docs/modules/mimehelper.rst docs/modules/misc.rst docs/modules/number.rst docs/modules/paginate.rst docs/modules/text.rst docs/modules/textile.rst docs/modules/util.rst docs/modules/html/__init__.rst docs/modules/html/builder.rst docs/modules/html/converters.rst docs/modules/html/grid.rst docs/modules/html/tags.rst docs/modules/html/tools.rst docs/modules/pylonslib/__init__.rst docs/modules/pylonslib/flash.rst docs/modules/pylonslib/grid.rst docs/modules/pylonslib/minify.rst docs/modules/pylonslib/secure_form.rst tests/test_containers.py tests/test_converters.py tests/test_date.py tests/test_escapes.py tests/test_feedgenerator.py tests/test_html.py tests/test_mimetypes.py tests/test_misc.py tests/test_modeltags.py tests/test_number.py tests/test_paginate.py tests/test_pylonslib_flash.py tests/test_tags.py tests/test_text.py tests/test_tools.py tests/util.py unfinished/README unfinished/baseN.py unfinished/containers.py unfinished/disabled_test_pylonslib_minify.py unfinished/document.py unfinished/logging_optparse.py unfinished/multimedia.py unfinished/number_to_human_size.py unfinished/opener.py unfinished/sanitize_filename.py webhelpers/__init__.py webhelpers/constants.py webhelpers/containers.py webhelpers/date.py webhelpers/feedgenerator.py webhelpers/markdown.py webhelpers/media.py webhelpers/mimehelper.py webhelpers/misc.py webhelpers/number.py webhelpers/paginate.py webhelpers/text.py webhelpers/textile.py webhelpers/util.py webhelpers/html/__init__.py webhelpers/html/builder.py webhelpers/html/converters.py webhelpers/html/grid.py webhelpers/html/grid_demo.py webhelpers/html/render.py webhelpers/html/tags.py webhelpers/html/tools.py webhelpers/public/stylesheets/grid.css webhelpers/public/stylesheets/webhelpers.css webhelpers/pylonslib/__init__.py webhelpers/pylonslib/_jsmin.py webhelpers/pylonslib/flash.py webhelpers/pylonslib/grid.py webhelpers/pylonslib/minify.py webhelpers/pylonslib/secure_form.pyWebHelpers-1.3/WebHelpers.egg-info/requires.txt0000664000175000017500000000002111542603374021131 0ustar sluggosluggoMarkupSafe>=0.9.2WebHelpers-1.3/WebHelpers.egg-info/not-zip-safe0000664000175000017500000000000111350576011020760 0ustar sluggosluggo WebHelpers-1.3/setup.cfg0000644000175000017500000000141411542603374014625 0ustar sluggosluggo[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [pudge] template_dir = docs/pudge_template title = WebHelpers dest = docs/html docs = docs/index.txt settings = no_about=true link1=/community/ Community link2=/download/ Download extra_credits=Development Sponsored by Parachute LLC. modules = webhelpers, webhelpers.rails doc_base = docs/ blog_url = http://groovie.org/articles/category/python organization = WebHelpers organization_url = http://pylonshq.com/WebHelpers/ trac_url = http://pylonshq.com/project/ [publish] doc-dest = scp://bbangert@groovie.org/usr/local/www/pylonshq/WebHelpers/ make-dirs = 1 doc-dir = docs/html [nosetests] exclude = webhelpers.feedgenerator verbosity = 2 verbose = True with-doctest = True WebHelpers-1.3/webhelpers/0000775000175000017500000000000011542603374015146 5ustar sluggosluggoWebHelpers-1.3/webhelpers/misc.py0000664000175000017500000001466711376653022016471 0ustar sluggosluggo"""Helpers that are neither text, numeric, container, or date. """ import itertools import traceback import types import warnings def all(seq, pred=None): """Is ``pred(elm)`` true for all elements? With the default predicate, this is the same as Python 2.5's ``all()`` function; i.e., it returns true if all elements are true. >>> all(["A", "B"]) True >>> all(["A", ""]) False >>> all(["", ""]) False >>> all(["A", "B", "C"], lambda x: x <= "C") True >>> all(["A", "B", "C"], lambda x: x < "C") False From recipe in itertools docs. """ for elm in itertools.ifilterfalse(pred, seq): return False return True def any(seq, pred=None): """Is ``pred(elm)`` is true for any element? With the default predicate, this is the same as Python 2.5's ``any()`` function; i.e., it returns true if any element is true. >>> any(["A", "B"]) True >>> any(["A", ""]) True >>> any(["", ""]) False >>> any(["A", "B", "C"], lambda x: x <= "C") True >>> any(["A", "B", "C"], lambda x: x < "C") True From recipe in itertools docs. """ for elm in itertools.ifilter(pred, seq): return True return False def no(seq, pred=None): """Is ``pred(elm)`` false for all elements? With the default predicate, this returns true if all elements are false. >>> no(["A", "B"]) False >>> no(["A", ""]) False >>> no(["", ""]) True >>> no(["A", "B", "C"], lambda x: x <= "C") False >>> no(["X", "Y", "Z"], lambda x: x <="C") True From recipe in itertools docs. """ for elm in itertools.ifilter(pred, seq): return False return True def count_true(seq, pred=lambda x: x): """How many elements is ``pred(elm)`` true for? With the default predicate, this counts the number of true elements. >>> count_true([1, 2, 0, "A", ""]) 3 >>> count_true([1, "A", 2], lambda x: isinstance(x, int)) 2 This is equivalent to the ``itertools.quantify`` recipe, which I couldn't get to work. """ ret = 0 for x in seq: if pred(x): ret += 1 return ret def convert_or_none(value, type_): """Return the value converted to the type, or None if error. ``type_`` may be a Python type or any function taking one argument. >>> print convert_or_none("5", int) 5 >>> print convert_or_none("A", int) None """ try: return type_(value) except Exception: return None def flatten(iterable): """Recursively iterate lists and tuples. Examples: >>> list(flatten([1, [2, 3], 4])) [1, 2, 3, 4] >>> list(flatten([1, (2, 3, [4]), 5])) [1, 2, 3, 4, 5] """ for elm in iterable: if isinstance(elm, (list, tuple)): for relm in flatten(elm): yield relm else: yield elm def subclasses_only(class_, it, exclude=None): """Extract the subclasses of a class from a module, dict, or iterable. Return a list of subclasses found. The class itself will not be included. This is useful to collect the concrete subclasses of an abstract base class. ``class_`` is a class. ``it`` is a dict or iterable. If a dict is passed, examine its values, not its keys. To introspect the current module, pass ``globals()``. To introspect another module or namespace, pass ``vars(the_module_or_namespace)``. ``exclude`` is an optional list of additional classes to ignore. This is mainly used to exclude abstract subclasses. """ if isinstance(it, dict): it = it.itervalues() class_types = (type, types.ClassType) ignore = [class_] if exclude: ignore.extend(exclude) return [x for x in it if isinstance(x, class_types) and issubclass(x, class_) and x not in ignore] class NotGiven(object): """A default value for function args. Use this when you need to distinguish between ``None`` and no value. Example:: >>> def foo(arg=NotGiven): ... print arg is NotGiven ... >>> foo() True >>> foo(None) False """ pass class DeclarativeException(Exception): """A simpler way to define an exception with a fixed message. Subclasses have a class attribute ``.message``, which is used if no message is passed to the constructor. The default message is the empty string. Example:: >>> class MyException(DeclarativeException): ... message="can't frob the bar when foo is enabled" ... >>> try: ... raise MyException() ... except Exception, e: ... print e ... can't frob the bar when foo is enabled """ message = "" def __init__(self, message=None): Exception.__init__(self, message or self.message) class OverwriteError(Exception): """Refusing to overwrite an existing file or directory.""" def __init__(self, filename, message="not overwriting '%s'"): message %= (filename,) Exception.__init__(self, message) self.filename = filename def format_exception(exc=None): """Format the exception type and value for display, without the traceback. This is the function you always wished were in the ``traceback`` module but isn't. It's *different* from ``traceback.format_exception``, which includes the traceback, returns a list of lines, and has a trailing newline. If you don't provide an exception object as an argument, it will call ``sys.exc_info()`` to get the current exception. """ if exc: exc_type = type(exc) else: exc_type, exc = sys.exc_info()[:2] lines = traceback.format_exception_only(exc_type, exc) return "".join(lines).rstrip() def deprecate(message, pending=False, stacklevel=2): """Issue a deprecation warning. ``message``: the deprecation message. ``pending``: if true, use ``PendingDeprecationWarning``. If false (default), use ``DeprecationWarning``. Python displays deprecations and ignores pending deprecations by default. ``stacklevel``: passed to ``warnings.warn``. The default level 2 makes the traceback end at the caller's level. Higher numbers make it end at higher levels. """ category = pending and PendingDeprecationWarning or DeprecationWarning warnings.warn(message, category, stacklevel) if __name__ == "__main__": import doctest doctest.testmod() WebHelpers-1.3/webhelpers/constants.py0000664000175000017500000002511711373734100017534 0ustar sluggosluggo# -*- encoding: latin-1 -*- # Latin-1 encoding needed for countries list. """Place names and other constants often used in web forms. """ def uk_counties(): """\ Return a list of UK county names. """ # Based on http://www.gbet.com/AtoZ_counties/ # Updated 2007-10-24 return [x.strip()[2:] for x in """\ * Avon * Bedfordshire * Berkshire * Borders * Buckinghamshire * Cambridgeshire * Central * Cheshire * Cleveland * Clwyd * Cornwall * County Antrim * County Armagh * County Down * County Fermanagh * County Londonderry * County Tyrone * Cumbria * Derbyshire * Devon * Dorset * Dumfries and Galloway * Durham * Dyfed * East Sussex * Essex * Fife * Gloucestershire * Grampian * Greater Manchester * Gwent * Gwynedd County * Hampshire * Herefordshire * Hertfordshire * Highlands and Islands * Humberside * Isle of Wight * Kent * Lancashire * Leicestershire * Lincolnshire * Lothian * Merseyside * Mid Glamorgan * Norfolk * North Yorkshire * Northamptonshire * Northumberland * Nottinghamshire * Oxfordshire * Powys * Rutland * Shropshire * Somerset * South Glamorgan * South Yorkshire * Staffordshire * Strathclyde * Suffolk * Surrey * Tayside * Tyne and Wear * Warwickshire * West Glamorgan * West Midlands * West Sussex * West Yorkshire * Wiltshire * Worcestershire""".split('\n')] _country_codes = None def country_codes(): """Return a list of all country names as tuples. The tuple value is the country's 2-letter ISO code and its name; e.g., ``("GB", "United Kingdom")``. The countries are in name order. Can be used like this:: import webhelpers.constants as constants from webhelpers.html.tags import select select("country", country_codes(), prompt="Please choose a country ...") See here for more information: http://www.iso.org/iso/english_country_names_and_code_elements """ # Updated on 2007-10-24. # # This might seem a funny implementation but it makes it easier to update # next time there is a change global _country_codes if _country_codes is not None: return _country_codes else: text_directly_from_iso_website = u""" A AFGHANISTAN AF ÅLAND ISLANDS AX ALBANIA AL ALGERIA DZ AMERICAN SAMOA AS ANDORRA AD ANGOLA AO ANGUILLA AI ANTARCTICA AQ ANTIGUA AND BARBUDA AG ARGENTINA AR ARMENIA AM ARUBA AW AUSTRALIA AU AUSTRIA AT AZERBAIJAN AZ B BAHAMAS BS BAHRAIN BH BANGLADESH BD BARBADOS BB BELARUS BY BELGIUM BE BELIZE BZ BENIN BJ BERMUDA BM BHUTAN BT BOLIVIA BO BOSNIA AND HERZEGOVINA BA BOTSWANA BW BOUVET ISLAND BV BRAZIL BR BRITISH INDIAN OCEAN TERRITORY IO BRUNEI DARUSSALAM BN BULGARIA BG BURKINA FASO BF BURUNDI BI C CAMBODIA KH CAMEROON CM CANADA CA CAPE VERDE CV CAYMAN ISLANDS KY CENTRAL AFRICAN REPUBLIC CF CHAD TD CHILE CL CHINA CN CHRISTMAS ISLAND CX COCOS (KEELING) ISLANDS CC COLOMBIA CO COMOROS KM CONGO CG CONGO, THE DEMOCRATIC REPUBLIC OF THE CD COOK ISLANDS CK COSTA RICA CR CÔTE D'IVOIRE CI CROATIA HR CUBA CU CYPRUS CY CZECH REPUBLIC CZ D DENMARK DK DJIBOUTI DJ DOMINICA DM DOMINICAN REPUBLIC DO E ECUADOR EC EGYPT EG EL SALVADOR SV EQUATORIAL GUINEA GQ ERITREA ER ESTONIA EE ETHIOPIA ET F FALKLAND ISLANDS (MALVINAS) FK FAROE ISLANDS FO FIJI FJ FINLAND FI FRANCE FR FRENCH GUIANA GF FRENCH POLYNESIA PF FRENCH SOUTHERN TERRITORIES TF G GABON GA GAMBIA GM GEORGIA GE GERMANY DE GHANA GH GIBRALTAR GI GREECE GR GREENLAND GL GRENADA GD GUADELOUPE GP GUAM GU GUATEMALA GT GUERNSEY GG GUINEA GN GUINEA-BISSAU GW GUYANA GY H HAITI HT HEARD ISLAND AND MCDONALD ISLANDS HM HOLY SEE (VATICAN CITY STATE) VA HONDURAS HN HONG KONG HK HUNGARY HU I ICELAND IS INDIA IN INDONESIA ID IRAN, ISLAMIC REPUBLIC OF IR IRAQ IQ IRELAND IE ISLE OF MAN IM ISRAEL IL ITALY IT J JAMAICA JM JAPAN JP JERSEY JE JORDAN JO K KAZAKHSTAN KZ KENYA KE KIRIBATI KI KOREA, DEMOCRATIC PEOPLE'S REPUBLIC OF KP KOREA, REPUBLIC OF KR KUWAIT KW KYRGYZSTAN KG L LAO PEOPLE'S DEMOCRATIC REPUBLIC LA LATVIA LV LEBANON LB LESOTHO LS LIBERIA LR LIBYAN ARAB JAMAHIRIYA LY LIECHTENSTEIN LI LITHUANIA LT LUXEMBOURG LU M MACAO MO MACEDONIA, THE FORMER YUGOSLAV REPUBLIC OF MK MADAGASCAR MG MALAWI MW MALAYSIA MY MALDIVES MV MALI ML MALTA MT MARSHALL ISLANDS MH MARTINIQUE MQ MAURITANIA MR MAURITIUS MU MAYOTTE YT MEXICO MX MICRONESIA, FEDERATED STATES OF FM MOLDOVA, REPUBLIC OF MD MONACO MC MONGOLIA MN MONTENEGRO ME MONTSERRAT MS MOROCCO MA MOZAMBIQUE MZ MYANMAR MM N NAMIBIA NA NAURU NR NEPAL NP NETHERLANDS NL NETHERLANDS ANTILLES AN NEW CALEDONIA NC NEW ZEALAND NZ NICARAGUA NI NIGER NE NIGERIA NG NIUE NU NORFOLK ISLAND NF NORTHERN MARIANA ISLANDS MP NORWAY NO O OMAN OM P PAKISTAN PK PALAU PW PALESTINIAN TERRITORY, OCCUPIED PS PANAMA PA PAPUA NEW GUINEA PG PARAGUAY PY PERU PE PHILIPPINES PH PITCAIRN PN POLAND PL PORTUGAL PT PUERTO RICO PR Q QATAR QA R RÉUNION RE ROMANIA RO RUSSIAN FEDERATION RU RWANDA RW S SAINT BARTHÉLEMY BL SAINT HELENA SH SAINT KITTS AND NEVIS KN SAINT LUCIA LC SAINT MARTIN MF SAINT PIERRE AND MIQUELON PM SAINT VINCENT AND THE GRENADINES VC SAMOA WS SAN MARINO SM SAO TOME AND PRINCIPE ST SAUDI ARABIA SA SENEGAL SN SERBIA RS SEYCHELLES SC SIERRA LEONE SL SINGAPORE SG SLOVAKIA SK SLOVENIA SI SOLOMON ISLANDS SB SOMALIA SO SOUTH AFRICA ZA SOUTH GEORGIA AND THE SOUTH SANDWICH ISLANDS GS SPAIN ES SRI LANKA LK SUDAN SD SURINAME SR SVALBARD AND JAN MAYEN SJ SWAZILAND SZ SWEDEN SE SWITZERLAND CH SYRIAN ARAB REPUBLIC SY T TAIWAN, PROVINCE OF CHINA TW TAJIKISTAN TJ TANZANIA, UNITED REPUBLIC OF TZ THAILAND TH TIMOR-LESTE TL TOGO TG TOKELAU TK TONGA TO TRINIDAD AND TOBAGO TT TUNISIA TN TURKEY TR TURKMENISTAN TM TURKS AND CAICOS ISLANDS TC TUVALU TV U UGANDA UG UKRAINE UA UNITED ARAB EMIRATES AE UNITED KINGDOM GB UNITED STATES US UNITED STATES MINOR OUTLYING ISLANDS UM URUGUAY UY UZBEKISTAN UZ V VANUATU VU VATICAN CITY STATE see HOLY SEE VENEZUELA VE VIET NAM VN VIRGIN ISLANDS, BRITISH VG VIRGIN ISLANDS, U.S. VI W WALLIS AND FUTUNA WF WESTERN SAHARA EH Y YEMEN YE Z ZAIRE see CONGO, THE DEMOCRATIC REPUBLIC OF THE ZAMBIA ZM ZIMBABWE ZW """.replace('\t',' ').split('\n') e = [] for item in text_directly_from_iso_website: if len(item) > 1: p=[] parts = item.split(' ') for part in parts: if part.strip(): p.append(part.strip()) if len(p)>2: raise Exception("Invalid entry %s" % p) p.reverse() if len(p) == 1: # It is just a letter continue if len(p[0]) != 2: if p[0][:3] != 'see': raise Exception('Unknown entry %s'%(p)) else: # We just want to ignore it continue p = tuple(p) e.append(p) _country_codes = e return _country_codes def us_states(): """List of USA states. Return a list of ``(abbreviation, name)`` for all US states, sorted by name. Includes the District of Columbia. """ # From http://www.usps.com/ncsc/lookups/abbreviations.html #Updated 2008-05-01 return [ ("AL", "Alabama"), ("AK", "Alaska"), ("AZ", "Arizona"), ("AR", "Arkansas"), ("CA", "California"), ("CO", "Colorado"), ("CT", "Connecticut"), ("DE", "Delaware"), ("DC", "District of Columbia"), ("FL", "Florida"), ("GA", "Georgia"), ("HI", "Hawaii"), ("ID", "Idaho"), ("IL", "Illinois"), ("IN", "Indiana"), ("IA", "Iowa"), ("KS", "Kansas"), ("KY", "Kentucky"), ("LA", "Louisiana"), ("ME", "Maine"), ("MD", "Maryland"), ("MA", "Massachusetts"), ("MI", "Michigan"), ("MN", "Minnesota"), ("MS", "Mississippi"), ("MO", "Missouri"), ("MT", "Montana"), ("NE", "Nebraska"), ("NV", "Nevada"), ("NH", "New Hampshire"), ("NJ", "New Jersey"), ("NM", "New Mexico"), ("NY", "New York"), ("NC", "North Carolina"), ("ND", "North Dakota"), ("OH", "Ohio"), ("OK", "Oklahoma"), ("OR", "Oregon"), ("PA", "Pennsylvania"), ("RI", "Rhode Island"), ("SC", "South Carolina"), ("SD", "South Dakota"), ("TN", "Tennessee"), ("TX", "Texas"), ("UT", "Utah"), ("VT", "Vermont"), ("VA", "Virginia"), ("WA", "Washington"), ("WV", "West Virginia"), ("WI", "Wisconsin"), ("WY", "Wyoming"), ] def us_territories(): """USA postal abbreviations for territories, protectorates, and military. The return value is a list of ``(abbreviation, name)`` tuples. The locations are sorted by name. """ # From http://www.usps.com/ncsc/lookups/abbreviations.html # Updated 2008-05-01 return [ ("AS", "American Samoa"), ("AA", "Armed Forces Americas"), ("AE", "Armed Forces Europe/Canada/Middle East/Africa"), ("AP", "Armed Forces Pacific"), ("FM", "Federated States of Micronesia"), ("GU", "Guam"), ("MH", "Marshall Islands"), ("MP", "Northern Mariana Islands"), ("PW", "Palau"), ("PR", "Puerto Rico"), ("VI", "Virgin Islands"), ] def canada_provinces(): """List of Canadian provinces. Return a list of ``(abbreviation, name)`` tuples for all Canadian provinces and territories, sorted by name. """ # Based on: # http://en.wikipedia.org/wiki/Canadian_subnational_postal_abbreviations # See also: # http://en.wikipedia.org/wiki/Provinces_and_territories_of_Canada # Updated 2008-05-01 provinces = [ ("Alberta", "AB"), ("British Columbia", "BC"), ("Manitoba", "MB"), ("New Brunswick", "NB"), ("Newfoundland and Labrador", "NL"), ("Nova Scotia", "NS"), ("Northwest Territories", "NT"), ("Nunavut", "NU"), ("Ontario", "ON"), ("Prince Edward Island", "PE"), ("Quebec", "QC"), ("Saskatchewan", "SK"), ("Yukon", "YT"), ] provinces.sort() return [(x[1], x[0]) for x in provinces] WebHelpers-1.3/webhelpers/pylonslib/0000775000175000017500000000000011542603374017161 5ustar sluggosluggoWebHelpers-1.3/webhelpers/pylonslib/_jsmin.py0000664000175000017500000000227211445174513021015 0ustar sluggosluggo#!/usr/bin/python raise ImportError("""\ _jsmin has been removed from WebHelpers due to licensing issues Details are in this module's comments. A standalone "jsmin" package is available in PyPI.""") # This module used to contain an algorithm for compressing Javascript code # to minimize network bandwidth. It was written in C by Douglas Crockford # (www.crockford.com) in 1992. Baruch Even later ported it to Python, and # that version was added to WebHelpers. However, it retained Crockford's # license, which was MIT-style but contained the clause, "The Software shall be # used for Good, not Evil." Fedora's lawyers have declared this clause # incompatible with its free-software distribution guidelines. Debian and # other distributions have similar guidelines. Thus, it can't be included in # popular Linux distributions we want WebHelpers to be in. The legal argument # is that while the clause is unenforceably vague ("What is an Evil purpose?"), # it's an implied restriction on use, which could expose users to trivial # harassment. Both the WebHelpers maintainer and Fedora maintainers contacted # Mr Crockford and asked him to change the license. He refused, and so we have # removed his code. WebHelpers-1.3/webhelpers/pylonslib/grid.py0000664000175000017500000000502211540262773020461 0ustar sluggosluggo""" This module is DEPRECATED. Please use ``webhelpers.html.grid`` in new applications. Support for paged grids has been added to that module in a framework-neutral way. PYRAMID USERS: This implementation is incompatible with Pyramid. Use ``webhelpers.html.grid`` instead. """ from webhelpers.html.builder import HTML, literal import webhelpers.html.grid as grid from webhelpers.misc import deprecate class NoRequestError(Exception): pass class PylonsGrid(grid.Grid): """ Subclass of Grid that can handle header link generation for quick building of tables that support ordering of their contents, paginated results etc. """ def __init__(self, request, *args, **kw): deprecate("please migrate to webhelpers.html.grid") self.request = request super(PylonsGrid, self).__init__(*args, **kw) def generate_header_link(self, column_number, column, label_text): """ This handles generation of link and then decides to call self.default_header_ordered_column_format or self.default_header_column_format based on if current column is the one that is used for sorting or not """ from pylons import url # this will handle possible URL generation request_copy = dict(self.request.copy().GET) self.order_column = request_copy.pop("order_col", None) self.order_dir = request_copy.pop("order_dir", None) if column == self.order_column and self.order_dir == "asc": new_order_dir = "dsc" else: new_order_dir = "asc" url_href = url.current(order_col=column, order_dir=new_order_dir, **request_copy) label_text = HTML.tag("a", href=url_href, c=label_text) # Is the current column the one we're ordering on? if column == self.order_column: return self.default_header_ordered_column_format(column_number, column, label_text) else: return self.default_header_column_format(column_number, column, label_text) class PylonsObjectGrid(PylonsGrid): """ This grid will work well with sqlalchemy row instances """ def default_column_format(self, column_number, i, record, column_name): class_name = "c%s" % (column_number) return HTML.tag("td", getattr(record, column_name), class_=class_name) WebHelpers-1.3/webhelpers/pylonslib/flash.py0000664000175000017500000003040711540262773020636 0ustar sluggosluggo"""Accumulate messages to show on the next page request. The ``Flash`` class is useful when you want to redirect to another page and also show a status message on that page, such as "Changes saved" or "No previous search found; returning to home page". THE IMPLEMENTATION DEPENDS ON PYLONS. However, it can easily be adapted for another web framework. PYRAMID USERS: use the flash methods built into Pyramid's ``Session`` object. This implementation is incompatible with Pyramid. A typical Pylons application instantiates a ``Flash`` object in myapp/lib/helpers.py:: from webhelpers.pylonslib.flash import Flash as _Flash flash = _Flash() The helpers module is then imported into your controllers and templates as ``h``. Whenever you want to set a message, call the instance:: h.flash("Record deleted.") You can set additional messages too:: h.flash("Hope you didn't need it.") Now make a place in your site template for the messages. In Mako you might do: .. code-block:: mako <% messages = h.flash.pop_messages() %> % if messages: % endif You can style this to look however you want: .. code-block:: css ul#flash-messages { color: red; background-color: #FFFFCC; font-size: larger; font-style: italic; margin-left: 40px; padding: 4px; list-style: none; } Multiple flash objects ====================== You can define multiple flash objects in your application to display different kinds of messages at different places on the page. For instance, you might use the main flash object for general messages, and a second flash object for "Added dookickey" / "Removed doohickey" messages next to a doohickey manager. Message categories ================== WebHelpers 1.0 adds message categories, contributed by Wichert Akkerman. These work like severity levels in Python's logging system. The standard categories are "*warning*", "*notice*", "*error*", and "*success*", with the default being "*notice*". The category is available in the message's ``.category`` attribute, and is normally used to set the container's CSS class. This is the *only* thing it does. Calling ``.pop_messages()`` pops all messages in the order registered, regardless of category. It is *not* possible to pop only a certain category, or all levels above a certain level, or to group messages by category. If you want to group different kinds of messages together, or pop only certain categories while leaving other categories, you should use multiple ``Flash`` objects. You can change the standard categories by overriding the ``.categories`` and ``.default_category`` class attributes, or by providing alternate values using constructor keywords. Category example ---------------- Let's show a standard way of using flash messages in your site: we will demonstrate *self-healing messages* (similar to what Growl does on OSX) to show messages in a site. To send a message from python just call the flash helper method:: h.flash(u"Settings have been saved") This will tell the system to show a message in the rendered page. If you need more control you can specify a message category as well: one of *warning*, *notice*, *error* or *success*. The default category is *notice*. For example:: h.flash(u"Failed to send confirmation email", "warning") We will use a very simple markup style: messages will be placed in a ``div`` with id ``selfHealingFeedback`` at the end of the document body. The messages are standard paragraphs with a class indicating the message category. For example::
... ...

Succesfully updated your settings

Failed to send confirmation email

This can easily created from a template. If you are using Genshi this should work: .. code-block: html

This is a notice.

The needed CSS is very simple: .. code-block: css #selfHealingFeedback { position: fixed; top: 20px; left: 20px; z-index: 2000; } #selfHealingFeedback p { margin-bottom: 10px; width: 250px; opacity: 0.93; } p.notice,p.error,p.success,p.warning { border: 3px solid silver; padding: 10px; -webkit-border-radius: 3px; -moz-border-radius: 3px; border-radius: 3px; -webkit-box-shadow: 0 0 5px silver; } Choosing different colours for the categories is left as an exercise for the reader. Next we create the javascript that will manage the needed behaviour (this implementation is based on jQuery):: function _SetupMessage(el) { var remover = function () { msg.animate({opacity: 0}, "slow") .slideUp("slow", function() { msg.remove() }); }; msg.data("healtimer", setTimeout(remover, 10000)) .click(function() { clearTimeout(msg.data("healtimer")); remover(); }); } function ShowMessage(message, category) { if (!category) category="notice"; var container = $("#selfHealingFeedback"); if (!container.length) container=$("
").appendTo("body"); var msg = $("

").addClass(category).html(message); SetupMessage(msg); msg.appendTo(container); } $(document).ready(function() { $("#selfHealingFeedback p").each(function() { SetupMessage($(this)); }); } The ``SetupMessage`` function configures the desired behaviour: a message disappears after 10 seconds, or if you click on it. Removal is done using a simple animation to avoid messages jumping around on the screen. This function is called for all messages as soon as the document has fully loaded. The ``ShowMessage`` function works exactly like the ``flash`` method in python: you can call it with a message and optionally a category and it will pop up a new message. JSON integration ---------------- It is not unusual to perform a remote task using a JSON call and show a result message to the user. This can easily be done using a simple wrapper around the ShowMessage method:: function ShowJSONResponse(info) { if (!info.message) return; ShowMessage(info.message, info.message_category); } You can use this direct as the success callback for the jQuery AJAX method:: $.ajax({type: "POST", url: "http://your.domain/call/json", dataType: "json", success: ShowJSONResponse }); if you need to perform extra work in your callback method you can call it yourself as well, for example::

$(document).ready(function() { $("button").click(function() { var button = $(this); button.addClass("processing"); $.ajax({type: "POST", url: this.form["json_url"].value, dataType: "json", success: function(data, status) { button.removeClass("processing"); ShowJSONResponse(data); }, error: function(request, status, error) { button.removeClass("processing"); ShowMessage("JSON call failed", "error"); } }); return false; }); }); This sets up a simple form which can be submitted normally by non-javascript enabled browsers. If a user does have javascript an AJAX call will be made to the server and the result will be shown in a message. While the call is active the button will be marked with a *processing* class. The server can return a message by including a ``message`` field in its response. Optionally a ``message_category`` field can also be included which will be used to determine the message category. For example:: @jsonify def handler(self): .. .. return dict(message=u"Settings successfully updated") """ # Do not import Pylons at module level; only within functions. All WebHelpers # modules should be importable on any Python system for the standard # regression tests. from webhelpers.html import escape __all__ = ["Flash", "Message"] class Message(object): """A message returned by ``Flash.pop_messages()``. Converting the message to a string returns the message text. Instances also have the following attributes: * ``message``: the message text. * ``category``: the category specified when the message was created. """ def __init__(self, category, message): self.category=category self.message=message def __str__(self): return self.message __unicode__ = __str__ def __html__(self): return escape(self.message) class Flash(object): """Accumulate a list of messages to show at the next page request. """ # List of allowed categories. If None, allow any category. categories = ["warning", "notice", "error", "success"] # Default category if none is specified. default_category = "notice" def __init__(self, session_key="flash", categories=None, default_category=None): """Instantiate a ``Flash`` object. ``session_key`` is the key to save the messages under in the user's session. ``categories`` is an optional list which overrides the default list of categories. ``default_category`` overrides the default category used for messages when none is specified. """ self.session_key = session_key if categories is not None: self.categories = categories if default_category is not None: self.default_category = default_category if self.categories and self.default_category not in self.categories: raise ValueError("unrecognized default category %r" % (self.default_category,)) def __call__(self, message, category=None, ignore_duplicate=False): """Add a message to the session. ``message`` is the message text. ``category`` is the message's category. If not specified, the default category will be used. Raise ``ValueError`` if the category is not in the list of allowed categories. If ``ignore_duplicate`` is true, don't add the message if another message with identical text has already been added. If the new message has a different category than the original message, change the original message to the new category. """ if not category: category = self.default_category elif self.categories and category not in self.categories: raise ValueError("unrecognized category %r" % (category,)) # Don't store Message objects in the session, to avoid unpickling # errors in edge cases. new_message_tuple = (category, message) from pylons import session messages = session.setdefault(self.session_key, []) # ``messages`` is a mutable list, so changes to the local variable are # reflected in the session. if ignore_duplicate: for i, m in enumerate(messages): if m[1] == message: if m[0] != category: messages[i] = new_message_tuple session.save() return # Original message found, so exit early. messages.append(new_message_tuple) session.save() def pop_messages(self): """Return all accumulated messages and delete them from the session. The return value is a list of ``Message`` objects. """ from pylons import session messages = session.pop(self.session_key, []) session.save() return [Message(*m) for m in messages] WebHelpers-1.3/webhelpers/pylonslib/secure_form.py0000664000175000017500000000625011540262773022051 0ustar sluggosluggo""" Secure Form Tag Helpers -- For prevention of Cross-site request forgery (CSRF) attacks. Generates form tags that include client-specific authorization tokens to be verified by the destined web app. PYRAMID USERS: Use the csrf_token methods built into Pyramid's ``Session`` object. This implementation is incompatible with Pyramid. Authorization tokens are stored in the client's session. The web app can then verify the request's submitted authorization token with the value in the client's session. This ensures the request came from the originating page. See http://en.wikipedia.org/wiki/Cross-site_request_forgery for more information. Pylons provides an ``authenticate_form`` decorator that does this verification on the behalf of controllers. These helpers depend on Pylons' ``session`` object. Most of them can be easily ported to another framework by changing the API calls. The helpers are implemented in such a way that it should be easy to create your own helpers if you are using helpers for AJAX calls. authentication_token() returns the current authentication token, creating one and storing it in the session if it doesn't already exist. auth_token_hidden_field() creates a hidden field (wrapped in an invisible div; I don't know if this is necessary, but the old WebHelpers had it like this) containing the authentication token. secure_form() is form() plus auth_token_hidden_field(). """ # Do not import Pylons at module level; only within functions. All WebHelpers # modules should be importable on any Python system for the standard # regression tests. import random from webhelpers.html.builder import HTML, literal from webhelpers.html.tags import form as insecure_form from webhelpers.html.tags import hidden token_key = "_authentication_token" def authentication_token(): """Return the current authentication token, creating one if one doesn't already exist. """ from pylons import session if not token_key in session: try: token = str(random.getrandbits(128)) except AttributeError: # Python < 2.4 token = str(random.randrange(2**128)) session[token_key] = token if hasattr(session, 'save'): session.save() return session[token_key] def auth_token_hidden_field(): token = hidden(token_key, authentication_token()) return HTML.div(token, style="display: none;") def secure_form(url, method="POST", multipart=False, **attrs): """Start a form tag that points the action to an url. This form tag will also include the hidden field containing the auth token. 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". If "PUT", "DELETE", or another verb is used, a hidden input with name _method is added to simulate the verb over POST. """ form = insecure_form(url, method, multipart, **attrs) token = auth_token_hidden_field() return literal("%s\n%s" % (form, token)) WebHelpers-1.3/webhelpers/pylonslib/minify.py0000664000175000017500000001652611540262773021042 0ustar sluggosluggo#!/usr/bin/env python # -*- coding: utf-8 -*- # vim: sw=4 ts=4 fenc=utf-8 """Minification helpers. This module provides enhanced versions of the ``javascript_link`` and ``stylesheet_link`` helpers in ``webhelpers.html.tags``. These versions add three additional arguments: * **minified**: If true, reduce the file size by squeezing out whitespace and other characters insignificant to the Javascript or CSS syntax. * **combined**: If true, concatenate the specified files into one file to reduce page load time. * **beaker_kwargs** (dict): arguments to pass to ``beaker_cache``. Dependencies: ``Pylons``, ``Beaker``, ``jsmin``, and ``cssutils`` (all available in PyPI). If "jsmin" is not installed, the helper issues a warning and passes Javascript through unchanged. (Changed in WebHelpers 1.1: removed built-in "_jsmin" package due to licensing issues; details in webhelpers/pylonslib/_jsmin.py .) PYRAMID USERS: this implementation is incompatible with Pyramid. No Pyramid-compatible implementation is currently known. Contributed by Pedro Algarvio and Domen Kozar . URL: http://docs.fubar.si/minwebhelpers/ """ import re import os import logging import StringIO import warnings from webhelpers.html.tags import javascript_link as __javascript_link from webhelpers.html.tags import stylesheet_link as __stylesheet_link try: from jsmin import JavascriptMinify except ImportError: class JavascriptMinify(object): def minify(self, instream, outstream): warnings.warn(JSMIN_MISSING_MESSAGE, UserWarning) data = instream.read() outstream.write(data) instream.close() JSMIN_MISSING_MESSAGE = """\ _jsmin has been removed from WebHelpers due to licensing issues Your Javascript code has been passed through unchanged. You can install the "jsmin" package from PyPI, and this helper will use it. """ __all__ = ['javascript_link', 'stylesheet_link'] log = logging.getLogger(__name__) beaker_kwargs = dict(key='sources', expire='never', type='memory') def combine_sources(sources, ext, fs_root): if len(sources) < 2: return sources names = list() js_buffer = StringIO.StringIO() base = os.path.commonprefix([os.path.dirname(s) for s in sources]) for source in sources: # get a list of all filenames without extensions js_file = os.path.basename(source) js_file_name = os.path.splitext(js_file)[0] names.append(js_file_name) # build a master file with all contents full_source = os.path.join(fs_root, source.lstrip('/')) f = open(full_source, 'r') js_buffer.write(f.read()) js_buffer.write('\n') f.close() # glue a new name and generate path to it fname = '.'.join(names + ['COMBINED', ext]) fpath = os.path.join(fs_root, base.strip('/'), fname) # write the combined file f = open(fpath, 'w') f.write(js_buffer.getvalue()) f.close() return [os.path.join(base, fname)] def minify_sources(sources, ext, fs_root=''): import cssutils if 'js' in ext: js_minify = JavascriptMinify() minified_sources = [] for source in sources: # generate full path to source no_ext_source = os.path.splitext(source)[0] full_source = os.path.join(fs_root, (no_ext_source + ext).lstrip('/')) # generate minified source path full_source = os.path.join(fs_root, (source).lstrip('/')) no_ext_full_source = os.path.splitext(full_source)[0] minified = no_ext_full_source + ext f_minified_source = open(minified, 'w') # minify js source (read stream is auto-closed inside) if 'js' in ext: js_minify.minify(open(full_source, 'r'), f_minified_source) # minify css source if 'css' in ext: serializer = get_serializer() sheet = cssutils.parseFile(full_source) sheet.setSerializer(serializer) cssutils.ser.prefs.useMinified() f_minified_source.write(sheet.cssText) f_minified_source.close() minified_sources.append(no_ext_source + ext) return minified_sources def base_link(ext, *sources, **options): from pylons import config from pylons.decorators.cache import beaker_cache combined = options.pop('combined', False) minified = options.pop('minified', False) beaker_options = options.pop('beaker_kwargs', False) fs_root = config.get('pylons.paths').get('static_files') if not (config.get('debug', False) or options.get('builtins', False)): if beaker_options: beaker_kwargs.update(beaker_options) if combined: sources = beaker_cache(**beaker_kwargs)(combine_sources)(list(sources), ext, fs_root) if minified: sources = beaker_cache(**beaker_kwargs)(minify_sources)(list(sources), '.min.' + ext, fs_root) if 'js' in ext: return __javascript_link(*sources, **options) if 'css' in ext: return __stylesheet_link(*sources, **options) def javascript_link(*sources, **options): return base_link('js', *sources, **options) def stylesheet_link(*sources, **options): return base_link('css', *sources, **options) _serializer_class = None def get_serializer(): # This is in a function to prevent a global import of ``cssutils``, # which is not a WebHelpers dependency. # The class is cached in a global variable so that it will be # compiled only once. import cssutils global _serializer_class if not _serializer_class: class CSSUtilsMinificationSerializer(cssutils.CSSSerializer): def __init__(self, prefs=None): cssutils.CSSSerializer.__init__(self, prefs) def do_css_CSSStyleDeclaration(self, style, separator=None): try: color = style.getPropertyValue('color') if color and color is not u'': color = self.change_colors(color) style.setProperty('color', color) except: pass return re.sub(r'0\.([\d])+', r'.\1', re.sub(r'(([^\d][0])+(px|em)+)+', r'\2', cssutils.CSSSerializer.do_css_CSSStyleDeclaration( self, style, separator))) def change_colors(self, color): colours = { 'black': '#000000', 'fuchia': '#ff00ff', 'yellow': '#ffff00', '#808080': 'gray', '#008000': 'green', '#800000': 'maroon', '#000800': 'navy', '#808000': 'olive', '#800080': 'purple', '#ff0000': 'red', '#c0c0c0': 'silver', '#008080': 'teal' } if color.lower() in colours: color = colours[color.lower()] if color.startswith('#') and len(color) == 7: if color[1]==color[2] and color[3]==color[4] and color[5]==color[6]: color = '#%s%s%s' % (color[1], color[3], color[5]) return color # End of class CSSUtilsMinificationSerializer _serializer_class = CSSUtilsMinificationSerializer return _serializer_class() WebHelpers-1.3/webhelpers/pylonslib/__init__.py0000664000175000017500000000076411373653355021307 0ustar sluggosluggo"""Helpers for the `Pylons `_ web framework These helpers depend on Pylons' ``request``, ``response``, ``session`` objects or some other aspect of Pylons. Most of them can be easily ported to another framework by changing the API calls. """ # Do not import Pylons at module level; only within functions. All WebHelpers # modules should be importable on any Python system for the standard # regression tests. # Backward compatibility from webhelpers.pylonslib.flash import * WebHelpers-1.3/webhelpers/feedgenerator.py0000664000175000017500000006200511471126331020327 0ustar sluggosluggo# Copyright (c) Django Software Foundation and individual contributors. # All rights reserved. # # Redistribution and use in source and binary forms, with or without modification, # are permitted provided that the following conditions are met: # # 1. Redistributions of source code must retain the above copyright notice, # this list of conditions and the following disclaimer. # # 2. Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # 3. Neither the name of Django nor the names of its contributors may be used # to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # Last synched with Django source 2009-12-18 (Django revision 11910): # http://code.djangoproject.com/browser/django/trunk/django/utils/feedgenerator.py # http://code.djangoproject.com/browser/django/trunk/django/contrib/gis/feeds.py # WebHelpers changes from original: # --------------------------------- # - Combine ``django.utils.feedgenerator`` and ``django.contrib.gis.feeds``. # - Change imports: # * ``SimpleXMLGenerator`` and ``iri_to_uri`` are in ``webhelpers.util``. # * Add local copy of ``force_unicode``. # - Delete parts that depend on Django's ORM system: ``Feed`` class, # ``BaseFeed`` and ``FeedDoesNotExist`` imports. # - Delete ``to_unicode`` lambdas and ``force_unicode`` import; these seem # unnecessary. # - Change docstring imports. # - Apply ``rfc3339_date`` bugfix (02044132a2ef) to both that function and # and ``rfc2822_date``. (``.tzinfo`` attribute may not exist in datetime # objects.) # - Apply 'published' property patch (1f234b039b58). # - Note: 'generator' and 'source' properties were lost from a previous # revision of webhelpers.feedgenerator. The implementation had a bug and # can't be used as is. # - Extend latitude-longitude behavior so that data can be input in either # lat-lon or lon-lat format. Output is always written in lat-lon per the # GeoRSS spec. Django's feedgenerator expects lon-lat input for GeoDjango # compatibility. WebHelpers defaults to lat-lon but can be switched to # lon-lat by setting the ``GeoFeedMixin.is_input_latitude_first`` flag to # false. (The flag can be set in a subclass or instance anytime before the # output is written.) The swapping is done in # ``GeoFeedMixin.georss_coords()`` and ``.add_georss_point()``. # - "if geom is not None" syntax fix in ``GeoFeedMixin.get_georss_element()``. # - Add a ``Geometry`` class for passing geometries to the Geo classes. # - ``GeoFeedMixin`` docstring. # - Add a dummy version attribute to ``RssFeed`` base class. # ``RssFeed._version = # "?"`` This avoids AttributeError when instantiating # ``RssFeed`` directly, although it's obviously invalid RSS. """ Syndication feed generation library -- used for generating RSS, etc. Sample usage: >>> import webhelpers.feedgenerator as feedgenerator >>> feed = feedgenerator.Rss201rev2Feed( ... title=u"Poynter E-Media Tidbits", ... link=u"http://www.poynter.org/column.asp?id=31", ... description=u"A group weblog by the sharpest minds in online media/journalism/publishing.", ... language=u"en", ... ) >>> feed.add_item(title="Hello", link=u"http://www.holovaty.com/test/", description="Testing.") >>> fp = open('test.rss', 'w') >>> feed.write(fp, 'utf-8') >>> fp.close() For definitions of the different versions of RSS, see: http://diveintomark.org/archives/2004/02/04/incompatible-rss """ import re import datetime from webhelpers.util import SimplerXMLGenerator, iri_to_uri #### The following code comes from ``django.utils.feedgenerator`` #### def rfc2822_date(date): # We do this ourselves to be timezone aware, email.Utils is not tz aware. if getattr(date, "tzinfo", False): time_str = date.strftime('%a, %d %b %Y %H:%M:%S ') offset = date.tzinfo.utcoffset(date) timezone = (offset.days * 24 * 60) + (offset.seconds / 60) hour, minute = divmod(timezone, 60) return time_str + "%+03d%02d" % (hour, minute) else: return date.strftime('%a, %d %b %Y %H:%M:%S -0000') def rfc3339_date(date): if getattr(date, "tzinfo", False): time_str = date.strftime('%Y-%m-%dT%H:%M:%S') offset = date.tzinfo.utcoffset(date) timezone = (offset.days * 24 * 60) + (offset.seconds / 60) hour, minute = divmod(timezone, 60) return time_str + "%+03d:%02d" % (hour, minute) else: return date.strftime('%Y-%m-%dT%H:%M:%SZ') def get_tag_uri(url, date): "Creates a TagURI. See http://diveintomark.org/archives/2004/05/28/howto-atom-id" tag = re.sub('^http://', '', url) if date is not None: tag = re.sub('/', ',%s:/' % date.strftime('%Y-%m-%d'), tag, 1) tag = re.sub('#', '/', tag) return u'tag:' + tag class SyndicationFeed(object): "Base class for all syndication feeds. Subclasses should provide write()" def __init__(self, title, link, description, language=None, author_email=None, author_name=None, author_link=None, subtitle=None, categories=None, feed_url=None, feed_copyright=None, feed_guid=None, ttl=None, **kwargs): if categories: categories = [force_unicode(c) for c in categories] self.feed = { 'title': title, 'link': iri_to_uri(link), 'description': description, 'language': language, 'author_email': author_email, 'author_name': author_name, 'author_link': iri_to_uri(author_link), 'subtitle': subtitle, 'categories': categories or (), 'feed_url': iri_to_uri(feed_url), 'feed_copyright': feed_copyright, 'id': feed_guid or link, 'ttl': ttl, } self.feed.update(kwargs) self.items = [] def add_item(self, title, link, description, author_email=None, author_name=None, author_link=None, pubdate=None, comments=None, unique_id=None, enclosure=None, categories=(), item_copyright=None, ttl=None, **kwargs): """ Adds an item to the feed. All args are expected to be Python Unicode objects except pubdate, which is a datetime.datetime object, and enclosure, which is an instance of the Enclosure class. """ item = { 'title': title, 'link': iri_to_uri(link), 'description': description, 'author_email': author_email, 'author_name': author_name, 'author_link': iri_to_uri(author_link), 'pubdate': pubdate, 'comments': comments, 'unique_id': unique_id, 'enclosure': enclosure, 'categories': categories or (), 'item_copyright': item_copyright, 'ttl': ttl, } item.update(kwargs) self.items.append(item) def num_items(self): return len(self.items) def root_attributes(self): """ Return extra attributes to place on the root (i.e. feed/channel) element. Called from write(). """ return {} def add_root_elements(self, handler): """ Add elements in the root (i.e. feed/channel) element. Called from write(). """ pass def item_attributes(self, item): """ Return extra attributes to place on each item (i.e. item/entry) element. """ return {} def add_item_elements(self, handler, item): """ Add elements on each item (i.e. item/entry) element. """ pass def write(self, outfile, encoding): """ Outputs the feed in the given encoding to outfile, which is a file-like object. Subclasses should override this. """ raise NotImplementedError def writeString(self, encoding): """ Returns the feed in the given encoding as a string. """ from StringIO import StringIO s = StringIO() self.write(s, encoding) return s.getvalue() def latest_post_date(self): """ Returns the latest item's pubdate. If none of them have a pubdate, this returns the current date/time. """ updates = [i['pubdate'] for i in self.items if i['pubdate'] is not None] if len(updates) > 0: updates.sort() return updates[-1] else: return datetime.datetime.now() class Enclosure(object): "Represents an RSS enclosure" def __init__(self, url, length, mime_type): "All args are expected to be Python Unicode objects" self.length, self.mime_type = length, mime_type self.url = iri_to_uri(url) class RssFeed(SyndicationFeed): mime_type = 'application/rss+xml' _version = u"?" def write(self, outfile, encoding): handler = SimplerXMLGenerator(outfile, encoding) handler.startDocument() handler.startElement(u"rss", self.rss_attributes()) handler.startElement(u"channel", self.root_attributes()) self.add_root_elements(handler) self.write_items(handler) self.endChannelElement(handler) handler.endElement(u"rss") def rss_attributes(self): return {u"version": self._version} def write_items(self, handler): for item in self.items: handler.startElement(u'item', self.item_attributes(item)) self.add_item_elements(handler, item) handler.endElement(u"item") def add_root_elements(self, handler): handler.addQuickElement(u"title", self.feed['title']) handler.addQuickElement(u"link", self.feed['link']) handler.addQuickElement(u"description", self.feed['description']) if self.feed['language'] is not None: handler.addQuickElement(u"language", self.feed['language']) for cat in self.feed['categories']: handler.addQuickElement(u"category", cat) if self.feed['feed_copyright'] is not None: handler.addQuickElement(u"copyright", self.feed['feed_copyright']) handler.addQuickElement(u"lastBuildDate", rfc2822_date(self.latest_post_date()).decode('utf-8')) if self.feed['ttl'] is not None: handler.addQuickElement(u"ttl", self.feed['ttl']) def endChannelElement(self, handler): handler.endElement(u"channel") class RssUserland091Feed(RssFeed): _version = u"0.91" def add_item_elements(self, handler, item): handler.addQuickElement(u"title", item['title']) handler.addQuickElement(u"link", item['link']) if item['description'] is not None: handler.addQuickElement(u"description", item['description']) class Rss201rev2Feed(RssFeed): # Spec: http://blogs.law.harvard.edu/tech/rss _version = u"2.0" def add_item_elements(self, handler, item): handler.addQuickElement(u"title", item['title']) handler.addQuickElement(u"link", item['link']) if item['description'] is not None: handler.addQuickElement(u"description", item['description']) # Author information. if item["author_name"] and item["author_email"]: handler.addQuickElement(u"author", "%s (%s)" % \ (item['author_email'], item['author_name'])) elif item["author_email"]: handler.addQuickElement(u"author", item["author_email"]) elif item["author_name"]: handler.addQuickElement(u"dc:creator", item["author_name"], {"xmlns:dc": u"http://purl.org/dc/elements/1.1/"}) if item['pubdate'] is not None: handler.addQuickElement(u"pubDate", rfc2822_date(item['pubdate']).decode('utf-8')) if item['comments'] is not None: handler.addQuickElement(u"comments", item['comments']) if item['unique_id'] is not None: handler.addQuickElement(u"guid", item['unique_id']) if item['ttl'] is not None: handler.addQuickElement(u"ttl", item['ttl']) # Enclosure. if item['enclosure'] is not None: handler.addQuickElement(u"enclosure", '', {u"url": item['enclosure'].url, u"length": item['enclosure'].length, u"type": item['enclosure'].mime_type}) # Categories. for cat in item['categories']: handler.addQuickElement(u"category", cat) class Atom1Feed(SyndicationFeed): # Spec: http://atompub.org/2005/07/11/draft-ietf-atompub-format-10.html mime_type = 'application/atom+xml' ns = u"http://www.w3.org/2005/Atom" def write(self, outfile, encoding): handler = SimplerXMLGenerator(outfile, encoding) handler.startDocument() handler.startElement(u'feed', self.root_attributes()) self.add_root_elements(handler) self.write_items(handler) handler.endElement(u"feed") def root_attributes(self): if self.feed['language'] is not None: return {u"xmlns": self.ns, u"xml:lang": self.feed['language']} else: return {u"xmlns": self.ns} def add_root_elements(self, handler): handler.addQuickElement(u"title", self.feed['title']) handler.addQuickElement(u"link", "", {u"rel": u"alternate", u"href": self.feed['link']}) if self.feed['feed_url'] is not None: handler.addQuickElement(u"link", "", {u"rel": u"self", u"href": self.feed['feed_url']}) handler.addQuickElement(u"id", self.feed['id']) handler.addQuickElement(u"updated", rfc3339_date(self.latest_post_date()).decode('utf-8')) if self.feed['author_name'] is not None: handler.startElement(u"author", {}) handler.addQuickElement(u"name", self.feed['author_name']) if self.feed['author_email'] is not None: handler.addQuickElement(u"email", self.feed['author_email']) if self.feed['author_link'] is not None: handler.addQuickElement(u"uri", self.feed['author_link']) handler.endElement(u"author") if self.feed['subtitle'] is not None: handler.addQuickElement(u"subtitle", self.feed['subtitle']) for cat in self.feed['categories']: handler.addQuickElement(u"category", "", {u"term": cat}) if self.feed['feed_copyright'] is not None: handler.addQuickElement(u"rights", self.feed['feed_copyright']) def write_items(self, handler): for item in self.items: handler.startElement(u"entry", self.item_attributes(item)) self.add_item_elements(handler, item) handler.endElement(u"entry") def add_item_elements(self, handler, item): handler.addQuickElement(u"title", item['title']) handler.addQuickElement(u"link", u"", {u"href": item['link'], u"rel": u"alternate"}) if item['pubdate'] is not None: handler.addQuickElement(u"updated", rfc3339_date(item['pubdate']).decode('utf-8')) handler.addQuickElement(u"published", rfc3339_date(item['pubdate']).decode('utf-8')) # Author information. if item['author_name'] is not None: handler.startElement(u"author", {}) handler.addQuickElement(u"name", item['author_name']) if item['author_email'] is not None: handler.addQuickElement(u"email", item['author_email']) if item['author_link'] is not None: handler.addQuickElement(u"uri", item['author_link']) handler.endElement(u"author") # Unique ID. if item['unique_id'] is not None: unique_id = item['unique_id'] else: unique_id = get_tag_uri(item['link'], item['pubdate']) handler.addQuickElement(u"id", unique_id) # Summary. if item['description'] is not None: handler.addQuickElement(u"summary", item['description'], {u"type": u"html"}) # Enclosure. if item['enclosure'] is not None: handler.addQuickElement(u"link", '', {u"rel": u"enclosure", u"href": item['enclosure'].url, u"length": item['enclosure'].length, u"type": item['enclosure'].mime_type}) # Categories. for cat in item['categories']: handler.addQuickElement(u"category", u"", {u"term": cat}) # Rights. if item['item_copyright'] is not None: handler.addQuickElement(u"rights", item['item_copyright']) # This isolates the decision of what the system default is, so calling code can # do "feedgenerator.DefaultFeed" instead of "feedgenerator.Rss201rev2Feed". DefaultFeed = Rss201rev2Feed #### The following code comes from ``django.contrib.gis.feeds`` #### class GeoFeedMixin(object): """ This mixin provides the necessary routines for SyndicationFeed subclasses to produce simple GeoRSS or W3C Geo elements. Subclasses recognize a ``geometry`` keyword argument to ``.add_item()``. The value may be any of several types: * a 2-element tuple or list of floats representing latitude/longitude: ``(X, Y)``. This is called a "point". * a 4-element tuple or list of floats representing a box: ``(X0, Y0, X1, Y1)``. * a tuple or list of two points: ``( (X0, Y0), (X1, Y1) )``. * a ``Geometry`` instance. (Or any compatible class.) This provides limited support for points, lines, and polygons. Read the ``Geometry`` docstring and the source of ``GeoFeedMixin.add_georss_element()`` before using this. The mixin provides one class attribute: .. attribute:: is_input_latitude_first The default value False indicates that input data is in latitude/longitude order. Change to True if the input data is longitude/latitude. The output is always written latitude/longitude to conform to the GeoRSS spec. The reason for this attribute is that the Django original stores data in longitude/latutude order and reverses the arguments before writing. WebHelpers does not do this by default, but if you're using Django data or other data that has longitude first, you'll have to set this. """ # Set to True if the input data is in lat-lon order, or False if lon-lat. # The output is always written in lat-lon order. is_input_latitude_first = True def georss_coords(self, coords): """ In GeoRSS coordinate pairs are ordered by lat/lon and separated by a single white space. Given a tuple of coordinates, this will return a unicode GeoRSS representation. """ if self.is_input_latitude_first: return u' '.join([u'%f %f' % x for x in coords]) else: return u' '.join([u'%f %f' % (x[1], x[0]) for x in coords]) def add_georss_point(self, handler, coords, w3c_geo=False): """ Adds a GeoRSS point with the given coords using the given handler. Handles the differences between simple GeoRSS and the more popular W3C Geo specification. """ if w3c_geo: if self.is_input_latitude_first: lat, lon = coords[:2] else: lon, lat = coords[:2] handler.addQuickElement(u'geo:lat', u'%f' % lat) handler.addQuickElement(u'geo:lon', u'%f' % lon) else: handler.addQuickElement(u'georss:point', self.georss_coords((coords,))) def add_georss_element(self, handler, item, w3c_geo=False): """ This routine adds a GeoRSS XML element using the given item and handler. """ # Getting the Geometry object. geom = item.get('geometry', None) if geom is not None: if isinstance(geom, (list, tuple)): # Special case if a tuple/list was passed in. The tuple may be # a point or a box box_coords = None if isinstance(geom[0], (list, tuple)): # Box: ( (X0, Y0), (X1, Y1) ) if len(geom) == 2: box_coords = geom else: raise ValueError('Only should be two sets of coordinates.') else: if len(geom) == 2: # Point: (X, Y) self.add_georss_point(handler, geom, w3c_geo=w3c_geo) elif len(geom) == 4: # Box: (X0, Y0, X1, Y1) box_coords = (geom[:2], geom[2:]) else: raise ValueError('Only should be 2 or 4 numeric elements.') # If a GeoRSS box was given via tuple. if not box_coords is None: if w3c_geo: raise ValueError('Cannot use simple GeoRSS box in W3C Geo feeds.') handler.addQuickElement(u'georss:box', self.georss_coords(box_coords)) else: # Getting the lower-case geometry type. gtype = str(geom.geom_type).lower() if gtype == 'point': self.add_georss_point(handler, geom.coords, w3c_geo=w3c_geo) else: if w3c_geo: raise ValueError('W3C Geo only supports Point geometries.') # For formatting consistent w/the GeoRSS simple standard: # http://georss.org/1.0#simple if gtype in ('linestring', 'linearring'): handler.addQuickElement(u'georss:line', self.georss_coords(geom.coords)) elif gtype in ('polygon',): # Only support the exterior ring. handler.addQuickElement(u'georss:polygon', self.georss_coords(geom[0].coords)) else: raise ValueError('Geometry type "%s" not supported.' % geom.geom_type) ### SyndicationFeed subclasses ### class GeoRSSFeed(Rss201rev2Feed, GeoFeedMixin): def rss_attributes(self): attrs = super(GeoRSSFeed, self).rss_attributes() attrs[u'xmlns:georss'] = u'http://www.georss.org/georss' return attrs def add_item_elements(self, handler, item): super(GeoRSSFeed, self).add_item_elements(handler, item) self.add_georss_element(handler, item) def add_root_elements(self, handler): super(GeoRSSFeed, self).add_root_elements(handler) self.add_georss_element(handler, self.feed) class GeoAtom1Feed(Atom1Feed, GeoFeedMixin): def root_attributes(self): attrs = super(GeoAtom1Feed, self).root_attributes() attrs[u'xmlns:georss'] = u'http://www.georss.org/georss' return attrs def add_item_elements(self, handler, item): super(GeoAtom1Feed, self).add_item_elements(handler, item) self.add_georss_element(handler, item) def add_root_elements(self, handler): super(GeoAtom1Feed, self).add_root_elements(handler) self.add_georss_element(handler, self.feed) class W3CGeoFeed(Rss201rev2Feed, GeoFeedMixin): def rss_attributes(self): attrs = super(W3CGeoFeed, self).rss_attributes() attrs[u'xmlns:geo'] = u'http://www.w3.org/2003/01/geo/wgs84_pos#' return attrs def add_item_elements(self, handler, item): super(W3CGeoFeed, self).add_item_elements(handler, item) self.add_georss_element(handler, item, w3c_geo=True) def add_root_elements(self, handler): super(W3CGeoFeed, self).add_root_elements(handler) self.add_georss_element(handler, self.feed, w3c_geo=True) class Geometry(object): """A basic geometry class for ``GeoFeedMixin``. Instances have two public attributes: .. attribute:: geom_type "point", "linestring", "linearring", "polygon" .. attribute:: coords For **point**, a tuple or list of two floats: ``(X, Y)``. For **linestring** or **linearring**, a string: ``"X0 Y0 X1 Y1 ..."``. For **polygon**, a list of strings: ``["X0 Y0 X1 Y1 ..."]``. Only the first element is used because the Geo classes support only the exterior ring. The constructor does not check its argument types. This class was created for WebHelpers based on the interface expected by ``GeoFeedMixin.add_georss_element()``. The class is untested. Please send us feedback on whether it works for you. """ def __init__(self, geom_type, coords): self.geom_type = geom_type coords = coords WebHelpers-1.3/webhelpers/html/0000775000175000017500000000000011542603374016112 5ustar sluggosluggoWebHelpers-1.3/webhelpers/html/grid.py0000664000175000017500000003453711540534212017415 0ustar sluggosluggo"""A helper to make an HTML table from a list of dicts, objects, or sequences. A set of CSS styles complementing this helper is in "webhelpers/html/public/stylesheets/grid.css". To use them, include the stylesheet in your applcation and set your class to "stylized". The documentation below is not very clear. This is a known bug. We need a native English speaker who uses the module to volunteer to rewrite it. This module is written and maintained by Ergo^. """ from webhelpers.html.builder import HTML, literal class Grid(object): """ This class is designed to aid programmer in the task of creation of tables/grids - structures that are mostly built from datasets. To create a grid at minimum one one needs to pass a dataset, like a list of dictionaries, or sqlalchemy proxy or query object:: grid = Grid(itemlist, ['_numbered','c1', 'c2','c4']) where itemlist in this simple scenario is a list of dicts: [{'c1':1,'c2'...}, {'c1'...}, ...] This helper also received the list that defines order in which columns will be rendered - also keep note of special column name that can be passed in list that defines order - ``_numbered`` - this adds additional column that shows the number of item. For paging sql data there one can pass ``start_number`` argument to the grid to define where to start counting. Descendant sorting on ``_numbered`` column decrements the value, you can change how numberign function behaves by overloading ``calc_row_no`` property. Converting the grid to a string renders the table rows. That's *just* the tags, not the
around them. The part outside the s have too many variations for us to render it. In many template systems, you can simply assign the grid to a template variable and it will be automatically converted to a string. Example using a Mako template: .. code-block:: html
${my_grid}
My Lovely Grid
The names of the columns will get automatically converted for humans ie. foo_bar becomes Foo Bar. If you want the title to be something else you can change the grid.labels dict. If you want the column ``part_no`` to become ``Catalogue Number`` just do:: grid.labels[``part_no``] = u'Catalogue Number' It may be desired to exclude some or all columns from generation sorting urls (used by subclasses that are sorting aware). You can use grids exclude_ordering property to pass list of columns that should not support sorting. By default sorting is disabled - this ``exclude_ordering`` contains every column name. Since various programmers have different needs, Grid is highly customizable. By default grid attempts to read the value from dict directly by key. For every column it will try to output value of current_row['colname']. Since very often this behavior needs to be overridden like we need date formatted, use conditionals or generate a link one can use the ``column_formats`` dict and pass a rendering function/lambda to it. For example we want to apppend ``foo`` to part number:: def custem_part_no_td(col_num, i, item): return HTML.td(`Foo %s` % item[``part_no``]) grid.column_formats[``part_no``] = custem_part_no_td You can customize the grids look and behavior by overloading grids instance render functions:: grid.default_column_format(self, column_number, i, record, column_name) by default generates markup like: VALUE grid.default_header_column_format(self, column_number, column_name, header_label) by default generates markup like: VALUE grid.default_header_ordered_column_format(self, column_number, order, column_name, header_label) Used by grids that support ordering of columns in the grid like, webhelpers.pylonslib.grid.GridPylons. by default generates markup like: LABEL grid.default_header_record_format(self, headers) by default generates markup like: HEADERS_MARKUP grid.default_record_format(self, i, record, columns) Make an HTML table from a list of objects, and soon a list of sequences, a list of dicts, and a single dict. RECORD_MARKUP grid.generate_header_link(self, column_number, column, label_text) by default just sets the order direction and column properties for grid. Actual link generation is handled by sublasses of Grid. grid.numbered_column_format(self, column_number, i, record) by default generates markup like: RECORD_NO """ def __init__(self, itemlist, columns, column_labels=None, column_formats=None, start_number=1, order_column=None, order_direction=None, request=None, url=None, **kw): """ additional keywords are appended to self.additional_kw handy for url generation """ self.labels = column_labels or {} self.exclude_ordering = columns self.itemlist = itemlist self.columns = columns self.column_formats = column_formats or {} if "_numbered" in columns: self.labels["_numbered"] = "#" if "_numbered" not in self.column_formats: self.column_formats["_numbered"] = self.numbered_column_format self.start_number = start_number self.order_dir = order_direction self.order_column = order_column #backward compatibility with old pylons grid if not hasattr(self,'request'): self.request = request self.url_generator = url self.additional_kw = kw def calc_row_no(self, i, column): if self.order_dir == 'dsc' and self.order_column == column: return self.start_number - i else: return self.start_number + i def make_headers(self): header_columns = [] for i, column in enumerate(self.columns): # let"s generate header column contents label_text = "" if column in self.labels: label_text = self.labels[column] else: label_text = column.replace("_", " ").title() # handle non clickable columns if column in self.exclude_ordering: header = self.default_header_column_format(i + 1, column, label_text) # handle clickable columns else: header = self.generate_header_link(i + 1, column, label_text) header_columns.append(header) return HTML(*header_columns) def make_columns(self, i, record): columns = [] for col_num, column in enumerate(self.columns): if column in self.column_formats: r = self.column_formats[column](col_num + 1, self. calc_row_no(i, column), record) else: r = self.default_column_format(col_num + 1, self.calc_row_no(i, column), record, column) columns.append(r) return HTML(*columns) def __html__(self): """ renders the grid """ records = [] #first render headers record headers = self.make_headers() r = self.default_header_record_format(headers) records.append(r) # now lets render the actual item grid for i, record in enumerate(self.itemlist): columns = self.make_columns(i, record) if hasattr(self, 'custom_record_format'): r = self.custom_record_format(i + 1, record, columns) else: r = self.default_record_format(i + 1, record, columns) records.append(r) return HTML(*records) def __str__(self): return self.__html__() def generate_header_link(self, column_number, column, label_text): """ This handles generation of link and then decides to call ``self.default_header_ordered_column_format`` or ``self.default_header_column_format`` based on whether current column is the one that is used for sorting. you need to extend Grid class and overload this method implementing ordering here, whole operation consists of setting self.order_column and self.order_dir to their CURRENT values, and generating new urls for state that header should set set after its clicked (additional kw are passed to url gen. - like for webhelpers.paginate) example URL generation code below:: GET = dict(self.request.copy().GET) # needs dict() for py2.5 compat self.order_column = GET.pop("order_col", None) self.order_dir = GET.pop("order_dir", None) # determine new order if column == self.order_column and self.order_dir == "asc": new_order_dir = "dsc" else: new_order_dir = "asc" self.additional_kw['order_col'] = column self.additional_kw['order_dir'] = new_order_dir # generate new url for example url_generator uses # pylons's url.current() or pyramid's current_route_url() new_url = self.url_generator(**self.additional_kw) # set label for header with link label_text = HTML.tag("a", href=new_url, c=label_text) """ # Is the current column the one we're ordering on? if (column == self.order_column): return self.default_header_ordered_column_format(column_number, column, label_text) else: return self.default_header_column_format(column_number, column, label_text) #### Default HTML tag formats #### def default_column_format(self, column_number, i, record, column_name): class_name = "c%s" % (column_number) return HTML.tag("td", record[column_name], class_=class_name) def numbered_column_format(self, column_number, i, record): class_name = "c%s" % (column_number) return HTML.tag("td", i, class_=class_name) def default_record_format(self, i, record, columns): if i % 2 == 0: class_name = "even r%s" % i else: class_name = "odd r%s" % i return HTML.tag("tr", columns, class_=class_name) def default_header_record_format(self, headers): return HTML.tag("tr", headers, class_="header") def default_header_ordered_column_format(self, column_number, column_name, header_label): header_label = HTML(header_label, HTML.tag("span", class_="marker")) if column_name == "_numbered": column_name = "numbered" class_name = "c%s ordering %s %s" % (column_number, self.order_dir, column_name) return HTML.tag("td", header_label, class_=class_name) def default_header_column_format(self, column_number, column_name, header_label): if column_name == "_numbered": column_name = "numbered" if column_name in self.exclude_ordering: class_name = "c%s %s" % (column_number, column_name) return HTML.tag("td", header_label, class_=class_name) else: header_label = HTML(header_label, HTML.tag("span", class_="marker")) class_name = "c%s ordering %s" % (column_number, column_name) return HTML.tag("td", header_label, class_=class_name) class ObjectGrid(Grid): """ A grid class for a sequence of objects. This grid class assumes that the rows are objects rather than dicts, and uses attribute access to retrieve the column values. It works well with SQLAlchemy ORM instances. """ def default_column_format(self, column_number, i, record, column_name): class_name = "c%s" % (column_number) return HTML.tag("td", getattr(record, column_name), class_=class_name) class ListGrid(Grid): """ A grid class for a sequence of lists. This grid class assumes that the rows are lists rather than dicts, and uses subscript access to retrieve the column values. Some constructor args are also different. If ``columns`` is not specified in the constructor, it will examine ``itemlist[0]`` to determine the number of columns, and display them in order. This works only if ``itemlist`` is a sequence and not just an iterable. Alternatively, you can pass an int to specify the number of columns, or a list of int subscripts to override the column order. Examples:: grid = ListGrid(list_data) grid = ListGrid(list_data, columns=4) grid = ListGrid(list_data, columns=[1, 3, 2, 0]) ``column_labels`` may be a list of strings. The class will calculate the appropriate subscripts for the superclass dict. """ def __init__(self, itemlist, columns=None, column_labels=None, *args, **kw): if columns is None: columns = range(len(itemlist[0])) elif isinstance(columns, int): columns = range(columns) # The superclass requires the ``columns`` elements to be strings. super_columns = [str(x) for x in columns] # The superclass requires ``column_labels`` to be a dict. super_labels = column_labels if isinstance(column_labels, (list, tuple)): super_labels = dict(zip(super_columns, column_labels)) Grid.__init__(self, itemlist, super_columns, super_labels, *args, **kw) def default_column_format(self, column_number, i, record, column_name): class_name = "c%s" % (column_number) return HTML.tag("td", record[int(column_name)], class_=class_name) WebHelpers-1.3/webhelpers/html/grid_demo.py0000664000175000017500000001765211376647675020451 0ustar sluggosluggo"""Demos for webhelpers.html.grid Run this module as a script:: python -m webhelpers.html.grid_demo OUTPUT_DIRECTORY Dec 16 19:39:54 PST 2009 """ import optparse import os import urllib from webhelpers.html import * from webhelpers.html.grid import * from webhelpers.html.tags import link_to from webhelpers.misc import subclasses_only # XXX You may find other helpers in webhelpers.html.tags useful too #### Global constants #### USAGE = "python -m %s OUTPUT_DIRECTORY" % __name__ DESCRIPTION = """\ Run the demos in this module and put the HTML output in OUTPUT_DIRECTORY.""" STYLESHEET = """\ /******************* tables ****************/ table.stylized { background-color: #ffffff; border-collapse: separate; border-spacing: 1px; border-bottom: 2px solid #666666; margin: 1px 5px 5px 5px; -moz-border-radius: 5px; -webkit-border-radius: 5px; width: 100%; border-collapse: collapse; } table.stylized caption { color: #ffffff; background-color: #444466; padding: 5px; font-size: 1.3em; font-weight: bold; margin: 5px 0px 0px 0px; -moz-border-radius: 5px; -webkit-border-radius: 5px; } table.stylized caption a:link,table.stylized caption a:visited { color: #ffffff; text-decoration: none; font-weight: bold; } table.stylized caption a:link,table.stylized caption a:hover { color: #ffcc00; text-decoration: none; font-weight: bold; } table.stylized thead { background-color: #ffffff; } table.stylized tbody { background-color: #ffffff; } table.stylized tfooter { background-color: #ffffff; } table.stylized th { text-align: center; } table.stylized tr.header { text-align: center; } table.stylized tr.header td, table.stylized th { text-align: center; color: #ffffff; background-color: #444466; } table.stylized td { padding: 5px 5px 5px 5px; border: 1px solid #dcdcdc; } table.stylized tr.odd td { border-top: 1px solid #999 !important; background-color: #ffffff; } table.stylized tr.even td { border-top: 1px solid #999 !important; background-color: #f6f6f6; } table.stylized .no { width: 30px; } table.stylized td.ordering{ background-color: #666666 !important; padding-right: 20px; } table.stylized td.ordering.dsc .marker { height: 20px; width: 20px; display: block; float: right; margin: 0px -18px; /* background-image for neutral marker here */ } table.stylized td.ordering.dsc .marker { /* background-image for dsc marker here */ } table.stylized td.ordering.asc .marker { /* background-image for asc marker here */ } table.stylized .header a:link,table.stylized .header a:visited { color: #ffffff; text-decoration: none; font-weight: bold; } table.stylized td.ordering a:link,table.stylized td.ordering a:visited { color: #ffcc00; text-decoration: none; font-weight: bold; } """ HTML_TEMPLATE = literal("""\ %(title)s

%(title)s

%(grid)s

%(description)s

""") # XXX There should be helpers to create a basic HTML file. test_data = [ {"group_name": "foo", "options": "lalala", "id":1}, {"group_name": "foo2", "options": "lalala2", "id":2}, {"group_name": "foo3", "options": "lalala3", "id":3}, {"group_name": "foo4", "options": "lalala4", "id":4}, ] class _DemoBase(object): title = None description = None def get_grid(): raise NotImplementedError("subclass responsibility") #### Demo classes ### class BasicDemo(_DemoBase): name = "Tickets" description = """\ This table shows a basic grid.""" def get_grid(self): """ basic demo """ g = Grid(test_data, columns=["_numbered","group_name","options"]) return g #### Demo classes ### class CustomColumnDemo(_DemoBase): name = "CustomColumn" description = """\ This table shows a grid with a customized column and header label.""" def get_grid(self): """ let's override how rows look like subject is link categories and status hold text based on param of item text , the translations are dicts holding translation strings correlated with integers from db, in this example """ def options_td(col_num, i, item): # XXX This module can't depend on 'app_globals' or 'url' or # external data. Define data within this method or class or # in a base class. # Could use HTML.a() instead of link_to(). u = url("/tickets/view", ticket_id=item["id"]) a = link_to(item["options"], u) return HTML.td(a) g = Grid(test_data, columns=["_numbered","group_name","options"]) g.labels["options"] = 'FOOBAAR' g.column_formats["options"] = options_td return g class OrderShiftDemo(_DemoBase): name = "OrderShift" description = """\ This table shows a grid with order starting from 10.""" def get_grid(self): """ order direction demo """ g = Grid(test_data, columns=["_numbered","group_name","options"], start_number=10 ) return g class OrderingDirectionHeaderAwareDemo(_DemoBase): name = "OrderDirectionHeaderAwareDemo" description = """\ This table shows a grid that has a markup indicating order direction. Options column has sorting set to "asc" """ def get_grid(self): """ order direction demo """ g = Grid(test_data, columns=["_numbered","group_name","options"], order_column='options', order_direction='asc' ) #enable ordering support g.exclude_ordering = [] return g list_data = [ [1,'a',3,'c',5], [11,'aa',33,'cc',55], [111,'aaa',333,'ccc',555] ] class ListDemo(_DemoBase): name = "List grid demo" description = """\ This table shows a basic grid generated from lists - it has customized order of columns.""" def get_grid(self): """ basic demo """ g = ListGrid(list_data, columns=[1, 3, 2, 0], column_labels=["One", "Three", "Two", "Zero"]) return g demos = subclasses_only(_DemoBase, globals()) #demos = [BasicDemo, CustomColumnDemo] #### Utility functions #### def url(urlpath, **params): # This should be a helper and I think it's defined somewhere but I # can't think of where. return urlpath + "?" + urllib.urlencode(params) def write_file(dir, filename, content): print "... writing '%s'" % filename path = os.path.join(dir, filename) f = open(path, "w") f.write(content) f.close() #### Main routine #### def main(): parser = optparse.OptionParser(usage=USAGE, description=DESCRIPTION) opts, args = parser.parse_args() if len(args) != 1: parser.error("wrong number of command-line arguments") dir = args[0] if not os.path.exists(dir): os.makedirs(dir) print "Putting output in directory '%s'" % dir write_file(dir, "demo.css", STYLESHEET) for class_ in demos: d = class_() name = d.name or d.__class__.__name__ filename = name + ".html" dic = { "title": d.name or d.__class__.__name__.lower(), "description": d.description, "grid": d.get_grid(), } html = HTML_TEMPLATE % dic write_file(dir, filename, html) if __name__ == "__main__": main() WebHelpers-1.3/webhelpers/html/render.py0000664000175000017500000003057411373653355017762 0ustar sluggosluggo# This is a private implementation module. Import render() and sanitize() # from webhelpers.html.converters . # Contributed by Ian Bicking, downloaded from # http://svn.w4py.org/ZPTKit/trunk/ZPTKit/htmlrender.py # (Webware for Python framework) # # Changed by Mike Orr: # - Add ``render()`` docstring. Export only that function by default. # - Add ``sanitize()`` and ``HTMLSanitizer. # - Change code when run as a script. # - Don't convert Unicode to text. # - Drop textwrap backport. (WebHelpers doesn't support Python < 2.3.) ########################################################################## # # Copyright (c) 2005 Imaginary Landscape LLC and Contributors. # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the # "Software"), to deal in the Software without restriction, including # without limitation the rights to use, copy, modify, merge, publish, # distribute, sublicense, and/or sell copies of the Software, and to # permit persons to whom the Software is furnished to do so, subject to # the following conditions: # # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ########################################################################## """An HTML-to-text formatter and HTML sanitizer. """ from HTMLParser import HTMLParser import htmlentitydefs import re import textwrap __all__ = ["render", "sanitize"] #### Public def render(html, width=70): """Render HTML as formatted text, like lynx's "print" function. Paragraphs are collected and wrapped at the specified width, default 70. Blockquotes are indented. Paragraphs cannot be nested at this time. HTML entities are substituted. The formatting is simplistic, and works best with HTML that was written for this renderer (e.g., ZPTKit.emailtemplate). The output usually ends with two newlines. If the input ends with a close tag plus any whitespace, the output ends with four newlines. This is probably a bug. """ context = Context() context.width = width context.indent = 0 p = HTMLRenderer() p.feed(html) p.close() paras = [para.to_text(context) for para in p.paragraphs if para] return "".join(paras) def sanitize(html): """Strip all HTML tags but leave their content. Use this to strip any potentially malicious tags from user input. HTML entities are left as-is. Usage:: >>> sanitize(u'I really like steak!') u'I really like steak!' >>> sanitize(u'I really like steak!') u'I really like NEFARIOUS CODE steak!' """ p = HTMLSanitizer() p.feed(html) p.close() return "".join(p.output_chunks) #### Private (though safe to use) class HTMLRenderer(HTMLParser): block_tags = 'p div blockquote h1 h2 h3 h4 h5 h6 ul ol'.split() def reset(self): HTMLParser.reset(self) self.paragraphs = [] self.in_paragraph = None self.last_href = None self.href_content = None self.in_table = None self.cell_content = None self.list_type = [] def handle_starttag(self, tag, attrs): tag = tag.lower() if tag == 'body': self.paragraphs = [] self.in_paragraph = None if tag == 'blockquote': self.paragraphs.append(Indenter(4)) if tag in self.block_tags: self.start_para(tag, attrs) if tag == 'br': self.add_br(tag, attrs) if tag == 'a': self.last_href = self.get_attr(attrs, 'href') self.href_content = [] if tag == 'img': alt = self.get_attr(attrs, 'alt') if alt: self.handle_data(alt) if tag == 'table': # @@: This is a hacky way of dealing with nested # tables. Basically the tables are crudely flattened, # and we keep track of how many 's we see based # on the table.depth attribute (so we can later remove # the table when sufficient
's have been seen) if self.in_table: self.in_table.depth += 1 else: self.in_table = Table() if tag == 'tr': self.in_table.add_row() if tag == 'td': self.cell_content = [] if tag == 'ul': self.paragraphs.append(Indenter(2)) self.list_type.append('ul') if tag == 'ol': self.paragraphs.append(Indenter(2)) self.list_type.append(1) if tag == 'li': self.add_br(None, None) if not self.list_type or self.list_type[-1] == 'ul': self.handle_data('* ') else: self.handle_data('%i) ' % self.list_type[-1]) self.list_type[-1] += 1 def handle_endtag(self, tag): if tag in self.block_tags: self.end_para(tag) if tag == 'a': if self.href_content: content = ''.join(self.href_content) else: content = None self.href_content = None if content and self.last_href and content != self.last_href: self.handle_data(' <%s>' % self.last_href) self.last_href = None if tag == 'table': self.paragraphs.append(self.in_table) self.in_table.depth -= 1 if not self.in_table.depth: self.in_table = None if tag == 'td': self.end_para(tag) if self.paragraphs: self.in_table.add_cell(self.paragraphs[-1]) self.paragraphs.pop() if tag == 'ul' or tag == 'ol': self.paragraphs.append(Indenter(-2)) self.list_type.pop() if tag == 'blockquote': self.paragraphs.append(Indenter(-4)) def handle_data(self, data): if self.in_paragraph is None: self.start_para(None, None) self.in_paragraph.add_text(data) if self.href_content is not None: self.href_content.append(data) def handle_entityref(self, name): name = name.lower() if name not in htmlentitydefs.entitydefs: # bad entity, just let it through # (like a &var=value in a URL) self.handle_data('&'+name) return result = htmlentitydefs.entitydefs[name] if result.startswith('&'): self.handle_charref(result[2:-1]) else: self.handle_data(result) def handle_charref(self, name): try: self.handle_data(unichr(int(name))) except ValueError: self.handle_data('&' + name) def start_para(self, tag, attrs): if tag is None: # Implicit paragraph tag = 'p' attrs = [] self.end_para(None) self.in_paragraph = Paragraph(tag, attrs) def end_para(self, tag): if self.in_paragraph: self.paragraphs.append(self.in_paragraph) self.in_paragraph = None def add_br(self, tag, attrs): if not self.in_paragraph: self.start_para(None, None) self.in_paragraph.add_tag('
') def close(self): HTMLParser.close(self) self.end_para(None) def get_attr(self, attrs, name, default=None): for attr_name, value in attrs: if attr_name.lower() == name.lower(): return value return default class Paragraph: def __init__(self, tag, attrs): self.tag = tag self.attrs = attrs self.text = [] self._default_align = 'left' def __repr__(self): length = len(''.join(map(str, self.text))) attrs = ' '.join([self.tag] + ['%s="%s"' % (name, value) for name, value in self.attrs] + ['length=%i' % length]) return '' % (hex(id(self))[2:], attrs) def add_text(self, text): self.text.append(text) def add_tag(self, tag): self.text.append([tag]) def to_text(self, context): lines = self.make_lines() width = context.width indent = context.indent wrapped_lines = [] for line in lines: wrapped = textwrap.wrap( line, width, replace_whitespace=True, initial_indent=' '*indent, subsequent_indent=' '*indent, fix_sentence_endings=False, break_long_words=False) wrapped_lines.extend(wrapped) if self.tag in ('h1', 'h2'): self._default_align = 'center' lines = self.align_lines(wrapped_lines, width) text = '\n'.join(lines) if self.tag in ('h1', 'h3'): text = text.upper() if self.tag == 'h4': text = '*%s*' % text return text + '\n\n' def align_lines(self, lines, width): if self.alignment() == 'right': return [' '*(width-len(line))+line for line in lines] elif self.alignment() == 'center': return [' '*((width-len(line))/2)+line for line in lines] elif self.alignment() == 'left': return lines else: # Could be odd things like 'baseline'; treat it as normal return lines def make_lines(self): lines = [''] for data in self.text: if isinstance(data, list): tag = data[0] if tag == '
': lines.append('') else: assert 0, "Unknown tag: %r" % tag else: lines[-1] = lines[-1] + data return [normalize(line).strip() for line in lines if line] def alignment(self): for name, value in self.attrs: if name.lower() == 'align': return value.lower() return self._default_align def __nonzero__(self): for t in self.text: if t: return True return False class Table: def __init__(self): self.rows = [] self.row_num = 0 self.depth = 1 def add_row(self): self.row_num += 1 self.rows.append([]) def add_cell(self, value): self.rows[-1].append(value) def __nonzero__(self): return not not self.rows def to_text(self, context): if self.rows and not self.rows[-1]: # Get rid of blank last line self.rows.pop() if not self.rows: return '' headers = [p.to_text(context).strip() for p in self.rows.pop(0)] context.indent += 4 lines = [] for row in self.rows: for header, cell in zip(headers, row): cell_text = cell.to_text(context).strip() lines.append('%s: %s' % (header, cell_text)) lines.append('') context.indent -= 4 return '\n'.join(lines) + '\n\n' class Indenter: def __init__(self, indent): self.indent = indent def to_text(self, context): context.indent += self.indent return '' class Context: pass class HTMLSanitizer(HTMLParser): def reset(self): HTMLParser.reset(self) self.output_chunks = [] def handle_data(self, data): self.output_chunks.append(data) def normalize(text): text = re.sub(r'\s+', ' ', text) # nbsp: if not isinstance(text, unicode): text = text.replace('\xa0', ' ') return text #### Main routine def main(): import os import sys if len(sys.argv) > 1: prog = os.path.basename(sys.argv[0]) sys.exit("usage: %s ", "&", '"', "'") or malicious Javascript tags. Escaped strings are returned as literals to prevent them from being double-escaped later. ``literal`` is a subclass of ``unicode``, so it works with all string methods and expressions. The only thing special about it is the ``.__html__`` method, which returns the string itself. The ``escape()`` function follows a simple protocol: if the object has an ``.__html__`` method, it calls that rather than ``.__str__`` to get the HTML representation. Third-party libraries that do not want to import ``literal`` (and this create a dependency on WebHelpers) can put an ``.__html__`` method in their own classes returning the desired HTML representation. WebHelpers 1.2 uses MarkupSafe, a package which provides an enhanced implementation of this protocol. Mako and Pylons have also switched to MarkupSafe. Its advantages are a C speedup for escaping, escaping single-quotes for security, and adding new methods to ``literal``. **literal** is now a subclass of ``markupsafe.Markup``. **escape** is ``markupsafe.escape_silent``. (The latter does not exist yet in MarkupSafe 0.9.3, but WebHelpers itself converts None to "" in the meantime). Single-quote escaping affects HTML attributes that are written like this: *alt='Some text.'* rather than the normal *alt="Some text."* If the text is a replaceable parameter whose value contains a single quote, the browser would think the value ends earlier than it does, thus enabling a potential cross-site scripting (XSS) attack. WebHelpers 1.0 and earlier escaped double quotes but not single quotes. MarkupSafe escapes both double and single quotes, preventing this sort of attack. MarkupSafe has some slight differences which should not cause compatibility issues but may in the following edge cases. (A) The ``force`` argument to ``escape()`` is gone. We doubt it was ever used. (B) The default encoding of ``literal()`` is "ascii" instead of "utf-8". (C) Double quotes are escaped as """ instead of """. Single quotes are escaped as "'". When ``literal`` is used in a mixed expression containing both literals and ordinary strings, it tries hard to escape the strings and return a literal. However, this depends on which value has "control" of the expression. ``literal`` seems to be able to take control with all combinations of the ``+`` operator, but with ``%`` and ``join`` it must be on the left side of the expression. So these all work:: "A" + literal("B") literal(", ").join(["A", literal("B")]) literal("%s %s") % (16, literal("kg")) But these return an ordinary string which is prone to double-escaping later:: "\\n".join([literal('Foo!'), literal('Bar!')]) "%s %s" % (literal("16"), literal("<em>kg</em>")) Third-party libraries that don't want to import ``literal`` and thus avoid a dependency on WebHelpers can add an ``.__html__`` method to any class, which can return the same as ``.__str__`` or something else. ``escape()`` trusts the HTML method and does not escape the return value. So only strings that lack an ``.__html__`` method will be escaped. The ``HTML`` object has the following methods for tag building: ``HTML(*strings)`` Escape the string args, concatenate them, and return a literal. This is the same as ``escape(s)`` but accepts multiple strings. Multiple args are useful when mixing child tags with text, such as:: html = HTML("The king is a >>", HTML.strong("fink"), "<>> HTML.tag("a", href="http://www.yahoo.com", name=None, ... c="Click Here") literal(u'Click Here') ``HTML.__getattr__`` Same as ``HTML.tag`` but using attribute access. Example: >>> HTML.a("Foo", href="http://example.com/", class_="important") literal(u'Foo') ``HTML.cdata`` Wrap the text in a "" section. Plain strings will not be escaped because CDATA itself is an escaping syntax. >>> HTML.cdata(u"Foo") literal(u'') >>> HTML.cdata(u"

") literal(u']]>') About XHTML and HTML -------------------- This builder always produces tags that are valid as *both* HTML and XHTML. "Void" tags -- those which can never have content like ``
`` and ```` -- are written like ``
``, with a space and a trailing ``/``. *Only* void tags get this treatment. The library will never, for example, produce ````. The `W3C HTML validator `_ validates these constructs as valid HTML Strict. It does produce warnings, but those warnings warn about the ambiguity if this same XML-style self-closing tags are used for HTML elements that are allowed to take content (`` >>> print javascript_link('/app.js', '/test/test.1.js') """ convert_boolean_attrs(attrs, ["defer"]) tags = [] for url in urls: tag = HTML.script("", type="text/javascript", src=url, **attrs) tags.append(tag) return literal("\n").join(tags) def stylesheet_link(*urls, **attrs): """Return CSS link tags for the specified stylesheet URLs. ``urls`` should be the exact URLs desired. A previous version of this helper added magic prefixes; this is no longer the case. Examples:: >>> stylesheet_link('/stylesheets/style.css') literal(u'') >>> stylesheet_link('/stylesheets/dir/file.css', media='all') literal(u'') """ if "href" in attrs: raise TypeError("keyword arg 'href' not allowed") attrs.setdefault("rel", "stylesheet") attrs.setdefault("type", "text/css") attrs.setdefault("media", "screen") tags = [] for url in urls: tag = HTML.link(href=url, **attrs) tags.append(tag) return literal('\n').join(tags) def auto_discovery_link(url, feed_type="rss", **attrs): """Return a link tag allowing auto-detecting of RSS or ATOM feed. The auto-detection of feed for the current page is only for browsers and news readers that support it. ``url`` The URL of the feed. (This should be the exact URLs desired. A previous version of this helper added magic prefixes; this is no longer the case.) ``feed_type`` The type of feed. Specifying 'rss' or 'atom' automatically translates to a type of 'application/rss+xml' or 'application/atom+xml', respectively. Otherwise the type is used as specified. Defaults to 'rss'. Examples:: >>> auto_discovery_link('http://feed.com/feed.xml') literal(u'') >>> auto_discovery_link('http://feed.com/feed.xml', feed_type='atom') literal(u'') >>> auto_discovery_link('app.rss', feed_type='atom', title='atom feed') literal(u'') >>> auto_discovery_link('/app.html', feed_type='text/html') literal(u'') """ if "href" in attrs: raise TypeError("keyword arg 'href' is not allowed") if "type" in attrs: raise TypeError("keyword arg 'type' is not allowed") title = "" if feed_type.lower() in ('rss', 'atom'): title = feed_type.upper() feed_type = 'application/%s+xml' % feed_type.lower() attrs.setdefault("title", title) return HTML.link(rel="alternate", type=feed_type, href=url, **attrs) #### Document type and XML declaration class Doctype(object): """Document type declarations for HTML and XHTML.""" def html5(self): """Create a for HTML 5. Usage:: >>> Doctype().html5() literal(u'') """ return literal("") def xhtml1(self, subtype="transitional", version="1.0"): """Create a for XHTML 1. Usage:: >>> Doctype().xhtml1() literal(u'') >>> Doctype().xhtml1("strict") literal(u'') >>> Doctype().xhtml1("frameset") literal(u'') """ if subtype in ["transitional", "loose"]: name = "Transitional" dtd = "transitional" else: name = subtype.capitalize() dtd = subtype.lower() uri = "-//W3C//DTD XHTML %s %s//EN" % (version, name) dtd = "http://www.w3.org/TR/xhtml1/DTD/xhtml1-%s.dtd" % dtd return self._make_doctype("html", uri, dtd) def html4(self, subtype="transitional", version="4.01"): """Create a for HTML 4. Usage: >>> Doctype().html4() literal(u'') >>> Doctype().html4("strict") literal(u'') >>> Doctype().html4("frameset") literal(u'') """ if subtype in ["transitional", "loose"]: name = "Transitional" dtd = "loose" else: name = subtype.capitalize() dtd = subtype.lower() uri = "-//W3C//DTD HTML %s %s//EN" % (version, name) dtd = "http://www.w3.org/TR/html4/%s.dtd" % dtd return self._make_doctype("HTML", uri, dtd) #### Private methods def _make_doctype(self, type, uri, dtd): return literal('') % (type, uri, dtd) def xml_declaration(version="1.0", encoding="utf-8"): """Create an XML declaration. Usage:: >>> xml_declaration() literal(u'') """ return literal('') % (version, encoding) ########## INTERNAL FUNCTIONS ########## def convert_boolean_attrs(attrs, bool_attrs): """Convert boolean values into proper HTML attributes. ``attrs`` is a dict of HTML attributes, which will be modified in place. ``bool_attrs`` is a list of attribute names. For every element in ``bool_attrs``, I look for a corresponding key in ``attrs``. If its value is true, I change the value to match the key. For example, I convert ``selected=True`` into ``selected="selected"``. If the value is false, I delete the key. """ for a in bool_attrs: if attrs.has_key(a) and attrs[a]: attrs[a] = a elif attrs.has_key(a): del attrs[a] def _set_input_attrs(attrs, type, name, value): attrs["type"] = type attrs["name"] = name attrs["value"] = value def _set_id_attr(attrs, id_arg, name): if "id_" in attrs: if id_arg is not NotGiven: raise TypeError("can't pass both 'id' and 'id_' args to helper") attrs["id"] = attrs.pop("id_") elif id_arg is NotGiven: attrs["id"] = _make_safe_id_component(name) elif id_arg is not None and id_arg != "": attrs["id"] = id_arg # Else id_arg is None or "", so do nothing. def _make_safe_id_component(idstring): """Make a string safe for including in an id attribute. The HTML spec says that id attributes 'must begin with a letter ([A-Za-z]) and may be followed by any number of letters, digits ([0-9]), hyphens ("-"), underscores ("_"), colons (":"), and periods (".")'. These regexps are slightly over-zealous, in that they remove colons and periods unnecessarily. Whitespace is transformed into underscores, and then anything which is not a hyphen or a character that matches \w (alphanumerics and underscore) is removed. """ # Transform all whitespace to underscore idstring = re.sub(r'\s', "_", '%s' % idstring) # Remove everything that is not a hyphen or a member of \w idstring = re.sub(r'(?!-)\W', "", idstring).lower() return idstring if __name__ == "__main__": import doctest doctest.testmod() WebHelpers-1.3/webhelpers/html/tools.py0000664000175000017500000003616211445174513017634 0ustar sluggosluggo"""HTML helpers that are more than just simple tags. There are no helpers to prettify HTML or canonicalize whitespace because BeautifulSoup and HTMLTidy handle this well. """ import re import urllib import warnings from webhelpers.html import HTML, literal, lit_sub, escape import webhelpers.html.tags as tags __all__ = [ 'auto_link', 'button_to', 'js_obfuscate', 'highlight', 'mail_to', 'strip_links', 'strip_tags', ] tag_re = re.compile(r'<.*?>', re.S) br_re = re.compile(r'', re.I|re.S) comment_re = re.compile(r'') AUTO_LINK_RE = re.compile(r""" ( # leading text <\w+.*?>| # leading HTML tag, or [^=!:'"/]| # leading punctuation, or ^ # beginning of line ) ( (?:https?://)| # protocol spec, or (?:www\.) # www.* ) ( [-\w]+ # subdomain or domain (?:\.[-\w]+)* # remaining subdomains or domain (?::\d+)? # port (?:/(?:(?:[~\w\+%-]|(?:[,.;:][^\s$]))+)?)* # path (?:\?[\w\+\/%&=.;-]+)? # query string (?:\#[\w\-]*)? # trailing anchor ) ([\.,"'?!;:]|\s|<|\]|$) # trailing text """, re.X) def button_to(name, url='', **html_attrs): """Generate a form containing a sole button that submits to ``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_attrs`` that you pass will be applied to the inner ``input`` element. In particular, pass disabled = True/False as part of ``html_attrs`` 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. The submit button itself will be displayed as an image if you provide both ``type`` and ``src`` as followed: type='image', src='icon_delete.gif' The ``src`` path should be the exact URL desired. A previous version of this helper added magical prefixes but this is no longer the case. Example 1:: # inside of controller for "feeds" >> button_to("Edit", url(action='edit', id=3))

Example 2:: >> button_to("Destroy", url(action='destroy', id=3), .. method='DELETE')
Example 3:: # Button as an image. >> button_to("Edit", url(action='edit', id=3), type='image', .. src='icon_delete.gif')
.. 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.) Changed in WebHelpers 1.2: Preserve case of "method" arg for XHTML compatibility. E.g., "POST" or "PUT" causes *method="POST"*; "post" or "put" causes *method="post"*. """ if html_attrs: tags.convert_boolean_attrs(html_attrs, ['disabled']) method_tag = '' method = html_attrs.pop('method', '') if method.upper() in ['PUT', 'DELETE']: method_tag = HTML.input( type='hidden', id='_method', name_='_method', value=method) if method.upper() in ('GET', 'POST'): form_method = method elif method in ('put', 'delete'): # preserve lowercasing of verb form_method = 'post' else: form_method = 'POST' url, name = url, name or url submit_type = html_attrs.get('type') img_source = html_attrs.get('src') if submit_type == 'image' and img_source: html_attrs["value"] = name html_attrs.setdefault("alt", name) else: html_attrs["type"] = "submit" html_attrs["value"] = name return HTML.form(method=form_method, action=url, class_="button-to", c=[HTML.div(method_tag, HTML.input(**html_attrs))]) def js_obfuscate(content): """Obfuscate data in a Javascript tag. Example:: >>> js_obfuscate("") literal(u'') """ doc_write = "document.write('%s');" % content obfuscated = ''.join(['%%%x' % ord(x) for x in doc_write]) complete = "eval(unescape('%s'))" % obfuscated cdata = HTML.cdata("\n", complete, "\n//") return HTML.script("\n//", cdata, "\n", type="text/javascript") def mail_to(email_address, name=None, cc=None, bcc=None, subject=None, body=None, replace_at=None, replace_dot=None, encode=None, **html_attrs): """Create a link tag for starting an email to the specified ``email_address``. This ``email_address`` is also used as the name of the link unless ``name`` is specified. Additional HTML options, such as class or id, can be passed in the ``html_attrs`` hash. You can also make it difficult for spiders to harvest email address by obfuscating them. Examples:: >>> mail_to("me@domain.com", "My email", encode = "javascript") literal(u'') >>> mail_to("me@domain.com", "My email", encode = "hex") literal(u'My email') You can also specify the cc address, bcc address, subject, and body parts of the message header to create a complex e-mail using the corresponding ``cc``, ``bcc``, ``subject``, and ``body`` keyword arguments. Each of these options are URI escaped and then appended to the ``email_address`` before being output. **Be aware that javascript keywords will not be escaped and may break this feature when encoding with javascript.** Examples:: >>> mail_to("me@domain.com", "My email", cc="ccaddress@domain.com", bcc="bccaddress@domain.com", subject="This is an example email", body= "This is the body of the message.") literal(u'My email') """ extras = [] for item in ('cc', cc), ('bcc', bcc), ('subject', subject), ('body', body): option = item[1] if option: if not isinstance(option, literal): item = (item[0], escape(option)) extras.append(item) options_query = urllib.urlencode(extras).replace("+", "%20") protocol = 'mailto:' email_address_obfuscated = email_address if replace_at: email_address_obfuscated = email_address_obfuscated.replace('@', replace_at) if replace_dot: email_address_obfuscated = email_address_obfuscated.replace('.', replace_dot) if encode == 'hex': email_address_obfuscated = HTML.literal(''.join( ['&#%d;' % ord(x) for x in email_address_obfuscated])) protocol = HTML.literal(''.join(['&#%d;' % ord(x) for x in protocol])) word_re = re.compile('\w') encoded_parts = [] for x in email_address: if word_re.match(x): encoded_parts.append('%%%x' % ord(x)) else: encoded_parts.append(x) email_address = HTML.literal(''.join(encoded_parts)) url = HTML.literal(protocol + email_address) if options_query: url += HTML.literal('?') + options_query html_attrs['href'] = url tag = HTML.a(name or email_address_obfuscated, **html_attrs) if encode == 'javascript': tmp = "document.write('%s');" % tag string = ''.join(['%%%x' % ord(x) for x in tmp]) return HTML.script( HTML.literal("\n//\n" % string), type="text/javascript") else: return tag def highlight(text, phrase, highlighter=None, case_sensitive=False, class_="highlight", **attrs): """Highlight all occurrences of ``phrase`` in ``text``. This inserts "..." around every occurrence. Arguments: ``text``: The full text. ``phrase``: A phrase to find in the text. This may be a string, a list of strings, or a compiled regular expression. If a string, it's regex-escaped and compiled. If a list, all of the strings will be highlighted. This is done by regex-escaping all elements and then joining them using the regex "|" token. ``highlighter``: Deprecated. A replacement expression for the regex substitution. This was deprecated because it bypasses the HTML builder and creates tags via string mangling. The previous default was '\\1', which mimics the normal behavior of this function. ``phrase`` must be a string if ``highlighter`` is specified. Overrides ``class_`` and ``attrs_`` arguments. ``case_sensitive``: If false (default), the phrases are searched in a case-insensitive manner. No effect if ``phrase`` is a regex object. ``class_``: CSS class for the tag. ``**attrs``: Additional HTML attributes for the tag. Changed in WebHelpers 1.0b2: new implementation using HTML builder. Allow ``phrase`` to be list or regex. Deprecate ``highlighter`` and change its default value to None. Add ``case_sensitive``, ``class_``, and ``**attrs`` arguments. """ if not phrase or not text: return text text = escape(text) if case_sensitive: flags = 0 # No flags. else: flags = re.IGNORECASE if highlighter: return _legacy_highlight(text, phrase, highlighter, flags) if isinstance(phrase, basestring): pat = re.escape(phrase) rx = re.compile(pat, flags) elif isinstance(phrase, (list, tuple)): parts = [re.escape(x) for x in phrase] pat = "|".join(parts) rx = re.compile(pat, flags) else: rx = phrase def repl(m): return HTML.strong(m.group(), class_=class_, **attrs) return lit_sub(rx, repl, text) def _legacy_highlight(text, phrase, highlighter, flags): """WebHelpers 0.6 style highlight with deprecated ``highlighter arg.""" warnings.warn("the ``highlighter`` argument is deprecated", DeprecationWarning) pat = "(%s)" % re.escape(phrase) rx = re.compile(pat, flags) highlighter = literal(highlighter) return lit_sub(rx, highlighter, text) def auto_link(text, link="all", **href_attrs): """ Turn all urls and email addresses into clickable links. ``link`` Used to determine what to link. Options are "all", "email_addresses", or "urls" ``href_attrs`` Additional attributes for generated tags. Example:: >>> auto_link("Go to http://www.planetpython.com and say hello to guido@python.org") literal(u'Go to http://www.planetpython.com and say hello to guido@python.org') """ if not text: return literal(u"") text = escape(text) if link == "all": return _auto_link_urls(_auto_link_email_addresses(text), **href_attrs) elif link == "email_addresses": return _auto_link_email_addresses(text) else: return _auto_link_urls(text, **href_attrs) def _auto_link_urls(text, **href_attrs): def handle_match(matchobj): all = matchobj.group() before, prefix, link, after = matchobj.group(1, 2, 3, 4) if re.match(r'\1'), text) def strip_links(text): """ Strip link tags from ``text`` leaving just the link label. Example:: >>> strip_links('else') 'else' """ if isinstance(text, literal): lit = literal else: lit = lambda x: x strip_re = re.compile(r'(.*?)<\/a>', re.I | re.M) return lit(strip_re.sub(r'\1', text)) def strip_tags(text): """Delete any HTML tags in the text, leaving their contents intact. Convert newlines to spaces, and
to newlines. Example:: >>> strip_tags('Text emphasis .') 'Text emphasis Javascript.' >>> strip_tags('Ordinary text.') 'Ordinary COMMENT! text.' >>> strip_tags('Line\\n1
Line 2') 'Line 1\\nLine 2' Implementation copied from ``WebOb``. ``webhelpers.html.converters`` contains more sophisticated versions of this. """ text = text.replace('\n', ' ') text = text.replace('\r', '') text = br_re.sub('\n', text) text = comment_re.sub('', text) text = tag_re.sub('', text) return text WebHelpers-1.3/webhelpers/markdown.py0000664000175000017500000017025411471126331017345 0ustar sluggosluggo#!/usr/bin/env python version = "1.7" version_info = (1,7,0,"rc-2") __revision__ = "$Rev: 72 $" """ Python-Markdown =============== Converts Markdown to HTML. Basic usage as a module: import markdown md = Markdown() html = md.convert(your_text_string) See http://www.freewisdom.org/projects/python-markdown/ for more information and instructions on how to extend the functionality of the script. (You might want to read that before you try modifying this file.) Started by [Manfred Stienstra](http://www.dwerg.net/). Continued and maintained by [Yuri Takhteyev](http://www.freewisdom.org) and [Waylan Limberg](http://achinghead.com/). Contact: yuri [at] freewisdom.org waylan [at] gmail.com License: GPL 2 (http://www.gnu.org/copyleft/gpl.html) or BSD """ import re, sys, codecs from logging import getLogger, StreamHandler, Formatter, \ DEBUG, INFO, WARN, ERROR, CRITICAL MESSAGE_THRESHOLD = CRITICAL # Configure debug message logger (the hard way - to support python 2.3) logger = getLogger('MARKDOWN') logger.setLevel(DEBUG) # This is restricted by handlers later console_hndlr = StreamHandler() formatter = Formatter('%(name)s-%(levelname)s: "%(message)s"') console_hndlr.setFormatter(formatter) console_hndlr.setLevel(MESSAGE_THRESHOLD) logger.addHandler(console_hndlr) def message(level, text): ''' A wrapper method for logging debug messages. ''' logger.log(level, text) # --------------- CONSTANTS YOU MIGHT WANT TO MODIFY ----------------- TAB_LENGTH = 4 # expand tabs to this many spaces ENABLE_ATTRIBUTES = True # @id = xyz -> <... id="xyz"> SMART_EMPHASIS = 1 # this_or_that does not become thisorthat HTML_REMOVED_TEXT = "[HTML_REMOVED]" # text used instead of HTML in safe mode RTL_BIDI_RANGES = ( (u'\u0590', u'\u07FF'), # from Hebrew to Nko (includes Arabic, Syriac and Thaana) (u'\u2D30', u'\u2D7F'), # Tifinagh ) # Unicode Reference Table: # 0590-05FF - Hebrew # 0600-06FF - Arabic # 0700-074F - Syriac # 0750-077F - Arabic Supplement # 0780-07BF - Thaana # 07C0-07FF - Nko BOMS = { 'utf-8': (codecs.BOM_UTF8, ), 'utf-16': (codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE), #'utf-32': (codecs.BOM_UTF32_LE, codecs.BOM_UTF32_BE) } def removeBOM(text, encoding): convert = isinstance(text, unicode) for bom in BOMS[encoding]: bom = convert and bom.decode(encoding) or bom if text.startswith(bom): return text.lstrip(bom) return text # The following constant specifies the name used in the usage # statement displayed for python versions lower than 2.3. (With # python2.3 and higher the usage statement is generated by optparse # and uses the actual name of the executable called.) EXECUTABLE_NAME_FOR_USAGE = "python markdown.py" # --------------- CONSTANTS YOU _SHOULD NOT_ HAVE TO CHANGE ---------- # a template for html placeholders HTML_PLACEHOLDER_PREFIX = "qaodmasdkwaspemas" HTML_PLACEHOLDER = HTML_PLACEHOLDER_PREFIX + "%dajkqlsmdqpakldnzsdfls" BLOCK_LEVEL_ELEMENTS = ['p', 'div', 'blockquote', 'pre', 'table', 'dl', 'ol', 'ul', 'script', 'noscript', 'form', 'fieldset', 'iframe', 'math', 'ins', 'del', 'hr', 'hr/', 'style'] def isBlockLevel (tag): return ( (tag in BLOCK_LEVEL_ELEMENTS) or (tag[0] == 'h' and tag[1] in "0123456789") ) """ ====================================================================== ========================== NANODOM =================================== ====================================================================== The three classes below implement some of the most basic DOM methods. I use this instead of minidom because I need a simpler functionality and do not want to require additional libraries. Importantly, NanoDom does not do normalization, which is what we want. It also adds extra white space when converting DOM to string """ ENTITY_NORMALIZATION_EXPRESSIONS = [ (re.compile("&"), "&"), (re.compile("<"), "<"), (re.compile(">"), ">")] ENTITY_NORMALIZATION_EXPRESSIONS_SOFT = [ (re.compile("&(?!\#)"), "&"), (re.compile("<"), "<"), (re.compile(">"), ">"), (re.compile("\""), """)] def getBidiType(text): if not text: return None ch = text[0] if not isinstance(ch, unicode) or not ch.isalpha(): return None else: for min, max in RTL_BIDI_RANGES: if ( ch >= min and ch <= max ): return "rtl" else: return "ltr" class Document: def __init__ (self): self.bidi = "ltr" def appendChild(self, child): self.documentElement = child child.isDocumentElement = True child.parent = self self.entities = {} def setBidi(self, bidi): if bidi: self.bidi = bidi def createElement(self, tag, textNode=None): el = Element(tag) el.doc = self if textNode: el.appendChild(self.createTextNode(textNode)) return el def createTextNode(self, text): node = TextNode(text) node.doc = self return node def createEntityReference(self, entity): if entity not in self.entities: self.entities[entity] = EntityReference(entity) return self.entities[entity] def createCDATA(self, text): node = CDATA(text) node.doc = self return node def toxml (self): return self.documentElement.toxml() def normalizeEntities(self, text, avoidDoubleNormalizing=False): if avoidDoubleNormalizing: regexps = ENTITY_NORMALIZATION_EXPRESSIONS_SOFT else: regexps = ENTITY_NORMALIZATION_EXPRESSIONS for regexp, substitution in regexps: text = regexp.sub(substitution, text) return text def find(self, test): return self.documentElement.find(test) def unlink(self): self.documentElement.unlink() self.documentElement = None class CDATA: type = "cdata" def __init__ (self, text): self.text = text def handleAttributes(self): pass def toxml (self): return "" class Element: type = "element" def __init__ (self, tag): self.nodeName = tag self.attributes = [] self.attribute_values = {} self.childNodes = [] self.bidi = None self.isDocumentElement = False def setBidi(self, bidi): if bidi: orig_bidi = self.bidi if not self.bidi or self.isDocumentElement: # Once the bidi is set don't change it (except for doc element) self.bidi = bidi self.parent.setBidi(bidi) def unlink(self): for child in self.childNodes: if child.type == "element": child.unlink() self.childNodes = None def setAttribute(self, attr, value): if not attr in self.attributes: self.attributes.append(attr) self.attribute_values[attr] = value def insertChild(self, position, child): self.childNodes.insert(position, child) child.parent = self def removeChild(self, child): self.childNodes.remove(child) def replaceChild(self, oldChild, newChild): position = self.childNodes.index(oldChild) self.removeChild(oldChild) self.insertChild(position, newChild) def appendChild(self, child): self.childNodes.append(child) child.parent = self def handleAttributes(self): pass def find(self, test, depth=0): """ Returns a list of descendants that pass the test function """ matched_nodes = [] for child in self.childNodes: if test(child): matched_nodes.append(child) if child.type == "element": matched_nodes += child.find(test, depth+1) return matched_nodes def toxml(self): if ENABLE_ATTRIBUTES: for child in self.childNodes: child.handleAttributes() buffer = "" if self.nodeName in ['h1', 'h2', 'h3', 'h4']: buffer += "\n" elif self.nodeName in ['li']: buffer += "\n " # Process children FIRST, then do the attributes childBuffer = "" if self.childNodes or self.nodeName in ['blockquote']: childBuffer += ">" for child in self.childNodes: childBuffer += child.toxml() if self.nodeName == 'p': childBuffer += "\n" elif self.nodeName == 'li': childBuffer += "\n " childBuffer += "" % self.nodeName else: childBuffer += "/>" buffer += "<" + self.nodeName if self.nodeName in ['p', 'li', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6']: if not self.attribute_values.has_key("dir"): if self.bidi: bidi = self.bidi else: bidi = self.doc.bidi if bidi=="rtl": self.setAttribute("dir", "rtl") for attr in self.attributes: value = self.attribute_values[attr] value = self.doc.normalizeEntities(value, avoidDoubleNormalizing=True) buffer += ' %s="%s"' % (attr, value) # Now let's actually append the children buffer += childBuffer if self.nodeName in ['p', 'br ', 'li', 'ul', 'ol', 'h1', 'h2', 'h3', 'h4'] : buffer += "\n" return buffer class TextNode: type = "text" attrRegExp = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123} def __init__ (self, text): self.value = text def attributeCallback(self, match): self.parent.setAttribute(match.group(1), match.group(2)) def handleAttributes(self): self.value = self.attrRegExp.sub(self.attributeCallback, self.value) def toxml(self): text = self.value self.parent.setBidi(getBidiType(text)) if not text.startswith(HTML_PLACEHOLDER_PREFIX): if self.parent.nodeName == "p": text = text.replace("\n", "\n ") elif (self.parent.nodeName == "li" and self.parent.childNodes[0]==self): text = "\n " + text.replace("\n", "\n ") text = self.doc.normalizeEntities(text) return text class EntityReference: type = "entity_ref" def __init__(self, entity): self.entity = entity def handleAttributes(self): pass def toxml(self): return "&" + self.entity + ";" """ ====================================================================== ========================== PRE-PROCESSORS ============================ ====================================================================== Preprocessors munge source text before we start doing anything too complicated. There are two types of preprocessors: TextPreprocessor and Preprocessor. """ class TextPreprocessor: ''' TextPreprocessors are run before the text is broken into lines. Each TextPreprocessor implements a "run" method that takes a pointer to a text string of the document, modifies it as necessary and returns either the same pointer or a pointer to a new string. TextPreprocessors must extend markdown.TextPreprocessor. ''' def run(self, text): pass class Preprocessor: ''' Preprocessors are run after the text is broken into lines. Each preprocessor implements a "run" method that takes a pointer to a list of lines of the document, modifies it as necessary and returns either the same pointer or a pointer to a new list. Preprocessors must extend markdown.Preprocessor. ''' def run(self, lines): pass class HtmlBlockPreprocessor(TextPreprocessor): """Removes html blocks from the source text and stores it.""" def _get_left_tag(self, block): return block[1:].replace(">", " ", 1).split()[0].lower() def _get_right_tag(self, left_tag, block): return block.rstrip()[-len(left_tag)-2:-1].lower() def _equal_tags(self, left_tag, right_tag): if left_tag == 'div' or left_tag[0] in ['?', '@', '%']: # handle PHP, etc. return True if ("/" + left_tag) == right_tag: return True if (right_tag == "--" and left_tag == "--"): return True elif left_tag == right_tag[1:] \ and right_tag[0] != "<": return True else: return False def _is_oneliner(self, tag): return (tag in ['hr', 'hr/']) def run(self, text): new_blocks = [] text = text.split("\n\n") items = [] left_tag = '' right_tag = '' in_tag = False # flag for block in text: if block.startswith("\n"): block = block[1:] if not in_tag: if block.startswith("<"): left_tag = self._get_left_tag(block) right_tag = self._get_right_tag(left_tag, block) if not (isBlockLevel(left_tag) \ or block[1] in ["!", "?", "@", "%"]): new_blocks.append(block) continue if self._is_oneliner(left_tag): new_blocks.append(block.strip()) continue if block[1] == "!": # is a comment block left_tag = "--" right_tag = self._get_right_tag(left_tag, block) # keep checking conditions below and maybe just append if block.rstrip().endswith(">") \ and self._equal_tags(left_tag, right_tag): new_blocks.append( self.stash.store(block.strip())) continue else: #if not block[1] == "!": # if is block level tag and is not complete items.append(block.strip()) in_tag = True continue new_blocks.append(block) else: items.append(block.strip()) right_tag = self._get_right_tag(left_tag, block) if self._equal_tags(left_tag, right_tag): # if find closing tag in_tag = False new_blocks.append( self.stash.store('\n\n'.join(items))) items = [] if items: new_blocks.append(self.stash.store('\n\n'.join(items))) new_blocks.append('\n') return "\n\n".join(new_blocks) HTML_BLOCK_PREPROCESSOR = HtmlBlockPreprocessor() class HeaderPreprocessor(Preprocessor): """ Replaces underlined headers with hashed headers to avoid the need for lookahead later. """ def run (self, lines): i = -1 while i+1 < len(lines): i = i+1 if not lines[i].strip(): continue if lines[i].startswith("#"): lines.insert(i+1, "\n") if (i+1 <= len(lines) and lines[i+1] and lines[i+1][0] in ['-', '=']): underline = lines[i+1].strip() if underline == "="*len(underline): lines[i] = "# " + lines[i].strip() lines[i+1] = "" elif underline == "-"*len(underline): lines[i] = "## " + lines[i].strip() lines[i+1] = "" return lines HEADER_PREPROCESSOR = HeaderPreprocessor() class LinePreprocessor(Preprocessor): """Deals with HR lines (needs to be done before processing lists)""" blockquote_re = re.compile(r'^(> )+') def run (self, lines): for i in range(len(lines)): prefix = '' m = self.blockquote_re.search(lines[i]) if m : prefix = m.group(0) if self._isLine(lines[i][len(prefix):]): lines[i] = prefix + self.stash.store("
", safe=True) return lines def _isLine(self, block): """Determines if a block should be replaced with an
""" if block.startswith(" "): return 0 # a code block text = "".join([x for x in block if not x.isspace()]) if len(text) <= 2: return 0 for pattern in ['isline1', 'isline2', 'isline3']: m = RE.regExp[pattern].match(text) if (m and m.group(1)): return 1 else: return 0 LINE_PREPROCESSOR = LinePreprocessor() class ReferencePreprocessor(Preprocessor): ''' Removes reference definitions from the text and stores them for later use. ''' def run (self, lines): new_text = []; for line in lines: m = RE.regExp['reference-def'].match(line) if m: id = m.group(2).strip().lower() t = m.group(4).strip() # potential title if not t: self.references[id] = (m.group(3), t) elif (len(t) >= 2 and (t[0] == t[-1] == "\"" or t[0] == t[-1] == "\'" or (t[0] == "(" and t[-1] == ")") ) ): self.references[id] = (m.group(3), t[1:-1]) else: new_text.append(line) else: new_text.append(line) return new_text #+ "\n" REFERENCE_PREPROCESSOR = ReferencePreprocessor() """ ====================================================================== ========================== INLINE PATTERNS =========================== ====================================================================== Inline patterns such as *emphasis* are handled by means of auxiliary objects, one per pattern. Pattern objects must be instances of classes that extend markdown.Pattern. Each pattern object uses a single regular expression and needs support the following methods: pattern.getCompiledRegExp() - returns a regular expression pattern.handleMatch(m, doc) - takes a match object and returns a NanoDom node (as a part of the provided doc) or None All of python markdown's built-in patterns subclass from Patter, but you can add additional patterns that don't. Also note that all the regular expressions used by inline must capture the whole block. For this reason, they all start with '^(.*)' and end with '(.*)!'. In case with built-in expression Pattern takes care of adding the "^(.*)" and "(.*)!". Finally, the order in which regular expressions are applied is very important - e.g. if we first replace http://.../ links with tags and _then_ try to replace inline html, we would end up with a mess. So, we apply the expressions in the following order: * escape and backticks have to go before everything else, so that we can preempt any markdown patterns by escaping them. * then we handle auto-links (must be done before inline html) * then we handle inline HTML. At this point we will simply replace all inline HTML strings with a placeholder and add the actual HTML to a hash. * then inline images (must be done before links) * then bracketed links, first regular then reference-style * finally we apply strong and emphasis """ NOBRACKET = r'[^\]\[]*' BRK = ( r'\[(' + (NOBRACKET + r'(\[')*6 + (NOBRACKET+ r'\])*')*6 + NOBRACKET + r')\]' ) NOIMG = r'(?\)' # [text]() IMAGE_LINK_RE = r'\!' + BRK + r'\s*\(([^\)]*)\)' # ![alttxt](http://x.com/) REFERENCE_RE = NOIMG + BRK+ r'\s*\[([^\]]*)\]' # [Google][3] IMAGE_REFERENCE_RE = r'\!' + BRK + '\s*\[([^\]]*)\]' # ![alt text][2] NOT_STRONG_RE = r'( \* )' # stand-alone * or _ AUTOLINK_RE = r'<(http://[^>]*)>' # AUTOMAIL_RE = r'<([^> \!]*@[^> ]*)>' # #HTML_RE = r'(\<[^\>]*\>)' # <...> HTML_RE = r'(\<[a-zA-Z/][^\>]*\>)' # <...> ENTITY_RE = r'(&[\#a-zA-Z0-9]*;)' # & LINE_BREAK_RE = r' \n' # two spaces at end of line LINE_BREAK_2_RE = r' $' # two spaces at end of text class Pattern: def __init__ (self, pattern): self.pattern = pattern self.compiled_re = re.compile("^(.*)%s(.*)$" % pattern, re.DOTALL) def getCompiledRegExp (self): return self.compiled_re BasePattern = Pattern # for backward compatibility class SimpleTextPattern (Pattern): def handleMatch(self, m, doc): return doc.createTextNode(m.group(2)) class SimpleTagPattern (Pattern): def __init__ (self, pattern, tag): Pattern.__init__(self, pattern) self.tag = tag def handleMatch(self, m, doc): el = doc.createElement(self.tag) el.appendChild(doc.createTextNode(m.group(2))) return el class SubstituteTagPattern (SimpleTagPattern): def handleMatch (self, m, doc): return doc.createElement(self.tag) class BacktickPattern (Pattern): def __init__ (self, pattern): Pattern.__init__(self, pattern) self.tag = "code" def handleMatch(self, m, doc): el = doc.createElement(self.tag) text = m.group(2).strip() #text = text.replace("&", "&") el.appendChild(doc.createTextNode(text)) return el class DoubleTagPattern (SimpleTagPattern): def handleMatch(self, m, doc): tag1, tag2 = self.tag.split(",") el1 = doc.createElement(tag1) el2 = doc.createElement(tag2) el1.appendChild(el2) el2.appendChild(doc.createTextNode(m.group(2))) return el1 class HtmlPattern (Pattern): def handleMatch (self, m, doc): rawhtml = m.group(2) inline = True place_holder = self.stash.store(rawhtml) return doc.createTextNode(place_holder) class LinkPattern (Pattern): def handleMatch(self, m, doc): el = doc.createElement('a') el.appendChild(doc.createTextNode(m.group(2))) parts = m.group(9).split('"') # We should now have [], [href], or [href, title] if parts: el.setAttribute('href', parts[0].strip()) else: el.setAttribute('href', "") if len(parts) > 1: # we also got a title title = '"' + '"'.join(parts[1:]).strip() title = dequote(title) #.replace('"', """) el.setAttribute('title', title) return el class ImagePattern (Pattern): def handleMatch(self, m, doc): el = doc.createElement('img') src_parts = m.group(9).split() if src_parts: el.setAttribute('src', src_parts[0]) else: el.setAttribute('src', "") if len(src_parts) > 1: el.setAttribute('title', dequote(" ".join(src_parts[1:]))) if ENABLE_ATTRIBUTES: text = doc.createTextNode(m.group(2)) el.appendChild(text) text.handleAttributes() truealt = text.value el.childNodes.remove(text) else: truealt = m.group(2) el.setAttribute('alt', truealt) return el class ReferencePattern (Pattern): def handleMatch(self, m, doc): if m.group(9): id = m.group(9).lower() else: # if we got something like "[Google][]" # we'll use "google" as the id id = m.group(2).lower() if not self.references.has_key(id): # ignore undefined refs return None href, title = self.references[id] text = m.group(2) return self.makeTag(href, title, text, doc) def makeTag(self, href, title, text, doc): el = doc.createElement('a') el.setAttribute('href', href) if title: el.setAttribute('title', title) el.appendChild(doc.createTextNode(text)) return el class ImageReferencePattern (ReferencePattern): def makeTag(self, href, title, text, doc): el = doc.createElement('img') el.setAttribute('src', href) if title: el.setAttribute('title', title) el.setAttribute('alt', text) return el class AutolinkPattern (Pattern): def handleMatch(self, m, doc): el = doc.createElement('a') el.setAttribute('href', m.group(2)) el.appendChild(doc.createTextNode(m.group(2))) return el class AutomailPattern (Pattern): def handleMatch(self, m, doc): el = doc.createElement('a') email = m.group(2) if email.startswith("mailto:"): email = email[len("mailto:"):] for letter in email: entity = doc.createEntityReference("#%d" % ord(letter)) el.appendChild(entity) mailto = "mailto:" + email mailto = "".join(['&#%d;' % ord(letter) for letter in mailto]) el.setAttribute('href', mailto) return el ESCAPE_PATTERN = SimpleTextPattern(ESCAPE_RE) NOT_STRONG_PATTERN = SimpleTextPattern(NOT_STRONG_RE) BACKTICK_PATTERN = BacktickPattern(BACKTICK_RE) DOUBLE_BACKTICK_PATTERN = BacktickPattern(DOUBLE_BACKTICK_RE) STRONG_PATTERN = SimpleTagPattern(STRONG_RE, 'strong') STRONG_PATTERN_2 = SimpleTagPattern(STRONG_2_RE, 'strong') EMPHASIS_PATTERN = SimpleTagPattern(EMPHASIS_RE, 'em') EMPHASIS_PATTERN_2 = SimpleTagPattern(EMPHASIS_2_RE, 'em') STRONG_EM_PATTERN = DoubleTagPattern(STRONG_EM_RE, 'strong,em') STRONG_EM_PATTERN_2 = DoubleTagPattern(STRONG_EM_2_RE, 'strong,em') LINE_BREAK_PATTERN = SubstituteTagPattern(LINE_BREAK_RE, 'br ') LINE_BREAK_PATTERN_2 = SubstituteTagPattern(LINE_BREAK_2_RE, 'br ') LINK_PATTERN = LinkPattern(LINK_RE) LINK_ANGLED_PATTERN = LinkPattern(LINK_ANGLED_RE) IMAGE_LINK_PATTERN = ImagePattern(IMAGE_LINK_RE) IMAGE_REFERENCE_PATTERN = ImageReferencePattern(IMAGE_REFERENCE_RE) REFERENCE_PATTERN = ReferencePattern(REFERENCE_RE) HTML_PATTERN = HtmlPattern(HTML_RE) ENTITY_PATTERN = HtmlPattern(ENTITY_RE) AUTOLINK_PATTERN = AutolinkPattern(AUTOLINK_RE) AUTOMAIL_PATTERN = AutomailPattern(AUTOMAIL_RE) """ ====================================================================== ========================== POST-PROCESSORS =========================== ====================================================================== Markdown also allows post-processors, which are similar to preprocessors in that they need to implement a "run" method. However, they are run after core processing. There are two types of post-processors: Postprocessor and TextPostprocessor """ class Postprocessor: ''' Postprocessors are run before the dom it converted back into text. Each Postprocessor implements a "run" method that takes a pointer to a NanoDom document, modifies it as necessary and returns a NanoDom document. Postprocessors must extend markdown.Postprocessor. There are currently no standard post-processors, but the footnote extension uses one. ''' def run(self, dom): pass class TextPostprocessor: ''' TextPostprocessors are run after the dom it converted back into text. Each TextPostprocessor implements a "run" method that takes a pointer to a text string, modifies it as necessary and returns a text string. TextPostprocessors must extend markdown.TextPostprocessor. ''' def run(self, text): pass class RawHtmlTextPostprocessor(TextPostprocessor): def __init__(self): pass def run(self, text): for i in range(self.stash.html_counter): html, safe = self.stash.rawHtmlBlocks[i] if self.safeMode and not safe: if str(self.safeMode).lower() == 'escape': html = self.escape(html) elif str(self.safeMode).lower() == 'remove': html = '' else: html = HTML_REMOVED_TEXT text = text.replace("

%s\n

" % (HTML_PLACEHOLDER % i), html + "\n") text = text.replace(HTML_PLACEHOLDER % i, html) return text def escape(self, html): ''' Basic html escaping ''' html = html.replace('&', '&') html = html.replace('<', '<') html = html.replace('>', '>') return html.replace('"', '"') RAWHTMLTEXTPOSTPROCESSOR = RawHtmlTextPostprocessor() """ ====================================================================== ========================== MISC AUXILIARY CLASSES ==================== ====================================================================== """ class HtmlStash: """This class is used for stashing HTML objects that we extract in the beginning and replace with place-holders.""" def __init__ (self): self.html_counter = 0 # for counting inline html segments self.rawHtmlBlocks=[] def store(self, html, safe=False): """Saves an HTML segment for later reinsertion. Returns a placeholder string that needs to be inserted into the document. @param html: an html segment @param safe: label an html segment as safe for safemode @param inline: label a segmant as inline html @returns : a placeholder string """ self.rawHtmlBlocks.append((html, safe)) placeholder = HTML_PLACEHOLDER % self.html_counter self.html_counter += 1 return placeholder class BlockGuru: def _findHead(self, lines, fn, allowBlank=0): """Functional magic to help determine boundaries of indented blocks. @param lines: an array of strings @param fn: a function that returns a substring of a string if the string matches the necessary criteria @param allowBlank: specifies whether it's ok to have blank lines between matching functions @returns: a list of post processes items and the unused remainder of the original list""" items = [] item = -1 i = 0 # to keep track of where we are for line in lines: if not line.strip() and not allowBlank: return items, lines[i:] if not line.strip() and allowBlank: # If we see a blank line, this _might_ be the end i += 1 # Find the next non-blank line for j in range(i, len(lines)): if lines[j].strip(): next = lines[j] break else: # There is no more text => this is the end break # Check if the next non-blank line is still a part of the list part = fn(next) if part: items.append("") continue else: break # found end of the list part = fn(line) if part: items.append(part) i += 1 continue else: return items, lines[i:] else: i += 1 return items, lines[i:] def detabbed_fn(self, line): """ An auxiliary method to be passed to _findHead """ m = RE.regExp['tabbed'].match(line) if m: return m.group(4) else: return None def detectTabbed(self, lines): return self._findHead(lines, self.detabbed_fn, allowBlank = 1) def print_error(string): """Print an error string to stderr""" sys.stderr.write(string +'\n') def dequote(string): """ Removes quotes from around a string """ if ( ( string.startswith('"') and string.endswith('"')) or (string.startswith("'") and string.endswith("'")) ): return string[1:-1] else: return string """ ====================================================================== ========================== CORE MARKDOWN ============================= ====================================================================== This stuff is ugly, so if you are thinking of extending the syntax, see first if you can do it via pre-processors, post-processors, inline patterns or a combination of the three. """ class CorePatterns: """This class is scheduled for removal as part of a refactoring effort.""" patterns = { 'header': r'(#*)([^#]*)(#*)', # # A title 'reference-def': r'(\ ?\ ?\ ?)\[([^\]]*)\]:\s*([^ ]*)(.*)', # [Google]: http://www.google.com/ 'containsline': r'([-]*)$|^([=]*)', # -----, =====, etc. 'ol': r'[ ]{0,3}[\d]*\.\s+(.*)', # 1. text 'ul': r'[ ]{0,3}[*+-]\s+(.*)', # "* text" 'isline1': r'(\**)', # *** 'isline2': r'(\-*)', # --- 'isline3': r'(\_*)', # ___ 'tabbed': r'((\t)|( ))(.*)', # an indented line 'quoted': r'> ?(.*)', # a quoted block ("> ...") } def __init__ (self): self.regExp = {} for key in self.patterns.keys(): self.regExp[key] = re.compile("^%s$" % self.patterns[key], re.DOTALL) self.regExp['containsline'] = re.compile(r'^([-]*)$|^([=]*)$', re.M) RE = CorePatterns() class Markdown: """ Markdown formatter class for creating an html document from Markdown text """ def __init__(self, source=None, # depreciated extensions=[], extension_configs=None, safe_mode = False): """Creates a new Markdown instance. @param source: The text in Markdown format. Depreciated! @param extensions: A list if extensions. @param extension-configs: Configuration setting for extensions. @param safe_mode: Disallow raw html. """ self.source = source if source is not None: message(WARN, "The `source` arg of Markdown.__init__() is depreciated and will be removed in the future. Use `instance.convert(source)` instead.") self.safeMode = safe_mode self.blockGuru = BlockGuru() self.registeredExtensions = [] self.stripTopLevelTags = 1 self.docType = "" self.textPreprocessors = [HTML_BLOCK_PREPROCESSOR] self.preprocessors = [HEADER_PREPROCESSOR, LINE_PREPROCESSOR, # A footnote preprocessor will # get inserted here REFERENCE_PREPROCESSOR] self.postprocessors = [] # a footnote postprocessor will get # inserted later self.textPostprocessors = [# a footnote postprocessor will get # inserted here RAWHTMLTEXTPOSTPROCESSOR] self.prePatterns = [] self.inlinePatterns = [DOUBLE_BACKTICK_PATTERN, BACKTICK_PATTERN, ESCAPE_PATTERN, REFERENCE_PATTERN, LINK_ANGLED_PATTERN, LINK_PATTERN, IMAGE_LINK_PATTERN, IMAGE_REFERENCE_PATTERN, AUTOLINK_PATTERN, AUTOMAIL_PATTERN, LINE_BREAK_PATTERN_2, LINE_BREAK_PATTERN, HTML_PATTERN, ENTITY_PATTERN, NOT_STRONG_PATTERN, STRONG_EM_PATTERN, STRONG_EM_PATTERN_2, STRONG_PATTERN, STRONG_PATTERN_2, EMPHASIS_PATTERN, EMPHASIS_PATTERN_2 # The order of the handlers matters!!! ] self.registerExtensions(extensions = extensions, configs = extension_configs) self.reset() def registerExtensions(self, extensions, configs): if not configs: configs = {} for ext in extensions: extension_module_name = "mdx_" + ext try: module = __import__(extension_module_name) except: message(CRITICAL, "couldn't load extension %s (looking for %s module)" % (ext, extension_module_name) ) else: if configs.has_key(ext): configs_for_ext = configs[ext] else: configs_for_ext = [] extension = module.makeExtension(configs_for_ext) extension.extendMarkdown(self, globals()) def registerExtension(self, extension): """ This gets called by the extension """ self.registeredExtensions.append(extension) def reset(self): """Resets all state variables so that we can start with a new text.""" self.references={} self.htmlStash = HtmlStash() HTML_BLOCK_PREPROCESSOR.stash = self.htmlStash LINE_PREPROCESSOR.stash = self.htmlStash REFERENCE_PREPROCESSOR.references = self.references HTML_PATTERN.stash = self.htmlStash ENTITY_PATTERN.stash = self.htmlStash REFERENCE_PATTERN.references = self.references IMAGE_REFERENCE_PATTERN.references = self.references RAWHTMLTEXTPOSTPROCESSOR.stash = self.htmlStash RAWHTMLTEXTPOSTPROCESSOR.safeMode = self.safeMode for extension in self.registeredExtensions: extension.reset() def _transform(self): """Transforms the Markdown text into a XHTML body document @returns: A NanoDom Document """ # Setup the document self.doc = Document() self.top_element = self.doc.createElement("span") self.top_element.appendChild(self.doc.createTextNode('\n')) self.top_element.setAttribute('class', 'markdown') self.doc.appendChild(self.top_element) # Fixup the source text text = self.source text = text.replace("\r\n", "\n").replace("\r", "\n") text += "\n\n" text = text.expandtabs(TAB_LENGTH) # Split into lines and run the preprocessors that will work with # self.lines self.lines = text.split("\n") # Run the pre-processors on the lines for prep in self.preprocessors : self.lines = prep.run(self.lines) # Create a NanoDom tree from the lines and attach it to Document buffer = [] for line in self.lines: if line.startswith("#"): self._processSection(self.top_element, buffer) buffer = [line] else: buffer.append(line) self._processSection(self.top_element, buffer) #self._processSection(self.top_element, self.lines) # Not sure why I put this in but let's leave it for now. self.top_element.appendChild(self.doc.createTextNode('\n')) # Run the post-processors for postprocessor in self.postprocessors: postprocessor.run(self.doc) return self.doc def _processSection(self, parent_elem, lines, inList = 0, looseList = 0): """Process a section of a source document, looking for high level structural elements like lists, block quotes, code segments, html blocks, etc. Some those then get stripped of their high level markup (e.g. get unindented) and the lower-level markup is processed recursively. @param parent_elem: A NanoDom element to which the content will be added @param lines: a list of lines @param inList: a level @returns: None""" # Loop through lines until none left. while lines: # Check if this section starts with a list, a blockquote or # a code block processFn = { 'ul': self._processUList, 'ol': self._processOList, 'quoted': self._processQuote, 'tabbed': self._processCodeBlock} for regexp in ['ul', 'ol', 'quoted', 'tabbed']: m = RE.regExp[regexp].match(lines[0]) if m: processFn[regexp](parent_elem, lines, inList) return # We are NOT looking at one of the high-level structures like # lists or blockquotes. So, it's just a regular paragraph # (though perhaps nested inside a list or something else). If # we are NOT inside a list, we just need to look for a blank # line to find the end of the block. If we ARE inside a # list, however, we need to consider that a sublist does not # need to be separated by a blank line. Rather, the following # markup is legal: # # * The top level list item # # Another paragraph of the list. This is where we are now. # * Underneath we might have a sublist. # if inList: start, lines = self._linesUntil(lines, (lambda line: RE.regExp['ul'].match(line) or RE.regExp['ol'].match(line) or not line.strip())) self._processSection(parent_elem, start, inList - 1, looseList = looseList) inList = inList-1 else: # Ok, so it's just a simple block paragraph, lines = self._linesUntil(lines, lambda line: not line.strip()) if len(paragraph) and paragraph[0].startswith('#'): self._processHeader(parent_elem, paragraph) elif paragraph: self._processParagraph(parent_elem, paragraph, inList, looseList) if lines and not lines[0].strip(): lines = lines[1:] # skip the first (blank) line def _processHeader(self, parent_elem, paragraph): m = RE.regExp['header'].match(paragraph[0]) if m: level = len(m.group(1)) h = self.doc.createElement("h%d" % level) parent_elem.appendChild(h) for item in self._handleInline(m.group(2).strip()): h.appendChild(item) else: message(CRITICAL, "We've got a problem header!") def _processParagraph(self, parent_elem, paragraph, inList, looseList): list = self._handleInline("\n".join(paragraph)) if ( parent_elem.nodeName == 'li' and not (looseList or parent_elem.childNodes)): # If this is the first paragraph inside "li", don't # put

around it - append the paragraph bits directly # onto parent_elem el = parent_elem else: # Otherwise make a "p" element el = self.doc.createElement("p") parent_elem.appendChild(el) for item in list: el.appendChild(item) def _processUList(self, parent_elem, lines, inList): self._processList(parent_elem, lines, inList, listexpr='ul', tag = 'ul') def _processOList(self, parent_elem, lines, inList): self._processList(parent_elem, lines, inList, listexpr='ol', tag = 'ol') def _processList(self, parent_elem, lines, inList, listexpr, tag): """Given a list of document lines starting with a list item, finds the end of the list, breaks it up, and recursively processes each list item and the remainder of the text file. @param parent_elem: A dom element to which the content will be added @param lines: a list of lines @param inList: a level @returns: None""" ul = self.doc.createElement(tag) # ul might actually be '

    ' parent_elem.appendChild(ul) looseList = 0 # Make a list of list items items = [] item = -1 i = 0 # a counter to keep track of where we are for line in lines: loose = 0 if not line.strip(): # If we see a blank line, this _might_ be the end of the list i += 1 loose = 1 # Find the next non-blank line for j in range(i, len(lines)): if lines[j].strip(): next = lines[j] break else: # There is no more text => end of the list break # Check if the next non-blank line is still a part of the list if ( RE.regExp['ul'].match(next) or RE.regExp['ol'].match(next) or RE.regExp['tabbed'].match(next) ): # get rid of any white space in the line items[item].append(line.strip()) looseList = loose or looseList continue else: break # found end of the list # Now we need to detect list items (at the current level) # while also detabing child elements if necessary for expr in ['ul', 'ol', 'tabbed']: m = RE.regExp[expr].match(line) if m: if expr in ['ul', 'ol']: # We are looking at a new item #if m.group(1) : # Removed the check to allow for a blank line # at the beginning of the list item items.append([m.group(1)]) item += 1 elif expr == 'tabbed': # This line needs to be detabbed items[item].append(m.group(4)) #after the 'tab' i += 1 break else: items[item].append(line) # Just regular continuation i += 1 # added on 2006.02.25 else: i += 1 # Add the dom elements for item in items: li = self.doc.createElement("li") ul.appendChild(li) self._processSection(li, item, inList + 1, looseList = looseList) # Process the remaining part of the section self._processSection(parent_elem, lines[i:], inList) def _linesUntil(self, lines, condition): """ A utility function to break a list of lines upon the first line that satisfied a condition. The condition argument should be a predicate function. """ i = -1 for line in lines: i += 1 if condition(line): break else: i += 1 return lines[:i], lines[i:] def _processQuote(self, parent_elem, lines, inList): """Given a list of document lines starting with a quote finds the end of the quote, unindents it and recursively processes the body of the quote and the remainder of the text file. @param parent_elem: DOM element to which the content will be added @param lines: a list of lines @param inList: a level @returns: None """ dequoted = [] i = 0 blank_line = False # allow one blank line between paragraphs for line in lines: m = RE.regExp['quoted'].match(line) if m: dequoted.append(m.group(1)) i += 1 blank_line = False elif not blank_line and line.strip() != '': dequoted.append(line) i += 1 elif not blank_line and line.strip() == '': dequoted.append(line) i += 1 blank_line = True else: break blockquote = self.doc.createElement('blockquote') parent_elem.appendChild(blockquote) self._processSection(blockquote, dequoted, inList) self._processSection(parent_elem, lines[i:], inList) def _processCodeBlock(self, parent_elem, lines, inList): """Given a list of document lines starting with a code block finds the end of the block, puts it into the dom verbatim wrapped in ("
    ") and recursively processes the
               the remainder of the text file.
    
               @param parent_elem: DOM element to which the content will be added
               @param lines: a list of lines
               @param inList: a level
               @returns: None"""
    
            detabbed, theRest = self.blockGuru.detectTabbed(lines)
    
            pre = self.doc.createElement('pre')
            code = self.doc.createElement('code')
            parent_elem.appendChild(pre)
            pre.appendChild(code)
            text = "\n".join(detabbed).rstrip()+"\n"
            #text = text.replace("&", "&")
            code.appendChild(self.doc.createTextNode(text))
            self._processSection(parent_elem, theRest, inList)
    
    
    
        def _handleInline (self, line, patternIndex=0):
            """Transform a Markdown line with inline elements to an XHTML
            fragment.
    
            This function uses auxiliary objects called inline patterns.
            See notes on inline patterns above.
    
            @param line: A line of Markdown text
            @param patternIndex: The index of the inlinePattern to start with
            @return: A list of NanoDom nodes """
    
    
            parts = [line]
    
            while patternIndex < len(self.inlinePatterns):
    
                i = 0
    
                while i < len(parts):
                    
                    x = parts[i]
    
                    if isinstance(x, (str, unicode)):
                        result = self._applyPattern(x, \
                                    self.inlinePatterns[patternIndex], \
                                    patternIndex)
    
                        if result:
                            i -= 1
                            parts.remove(x)
                            for y in result:
                                parts.insert(i+1,y)
    
                    i += 1
                patternIndex += 1
    
            for i in range(len(parts)):
                x = parts[i]
                if isinstance(x, (str, unicode)):
                    parts[i] = self.doc.createTextNode(x)
    
            return parts
            
    
        def _applyPattern(self, line, pattern, patternIndex):
    
            """ Given a pattern name, this function checks if the line
            fits the pattern, creates the necessary elements, and returns
            back a list consisting of NanoDom elements and/or strings.
            
            @param line: the text to be processed
            @param pattern: the pattern to be checked
    
            @returns: the appropriate newly created NanoDom element if the
                      pattern matches, None otherwise.
            """
    
            # match the line to pattern's pre-compiled reg exp.
            # if no match, move on.
    
    
    
            m = pattern.getCompiledRegExp().match(line)
            if not m:
                return None
    
            # if we got a match let the pattern make us a NanoDom node
            # if it doesn't, move on
            node = pattern.handleMatch(m, self.doc)
    
            # check if any of this nodes have children that need processing
    
            if isinstance(node, Element):
    
                if not node.nodeName in ["code", "pre"]:
                    for child in node.childNodes:
                        if isinstance(child, TextNode):
                            
                            result = self._handleInline(child.value, patternIndex+1)
                            
                            if result:
    
                                if result == [child]:
                                    continue
                                    
                                result.reverse()
                                #to make insertion easier
    
                                position = node.childNodes.index(child)
                                
                                node.removeChild(child)
    
                                for item in result:
    
                                    if isinstance(item, (str, unicode)):
                                        if len(item) > 0:
                                            node.insertChild(position,
                                                 self.doc.createTextNode(item))
                                    else:
                                        node.insertChild(position, item)
                    
    
    
    
            if node:
                # Those are in the reverse order!
                return ( m.groups()[-1], # the string to the left
                         node,           # the new node
                         m.group(1))     # the string to the right of the match
    
            else:
                return None
    
        def convert (self, source = None):
            """Return the document in XHTML format.
    
            @returns: A serialized XHTML body."""
    
            if source is not None: #Allow blank string
                self.source = source
    
            if not self.source:
                return u""
    
            try:
                self.source = unicode(self.source)
            except UnicodeDecodeError:
                message(CRITICAL, 'UnicodeDecodeError: Markdown only accepts unicode or ascii  input.')
                return u""
    
            for pp in self.textPreprocessors:
                self.source = pp.run(self.source)
    
            doc = self._transform()
            xml = doc.toxml()
    
    
            # Return everything but the top level tag
    
            if self.stripTopLevelTags:
                xml = xml.strip()[23:-7] + "\n"
    
            for pp in self.textPostprocessors:
                xml = pp.run(xml)
    
            return (self.docType + xml).strip()
    
    
        def __str__(self):
            ''' Report info about instance. Markdown always returns unicode. '''
            if self.source is None:
                status = 'in which no source text has been assinged.'
            else:
                status = 'which contains %d chars and %d line(s) of source.'%\
                         (len(self.source), self.source.count('\n')+1)
            return 'An instance of "%s" %s'% (self.__class__, status)
    
        __unicode__ = convert # markdown should always return a unicode string
    
    
    
    
    
    # ====================================================================
    
    def markdownFromFile(input = None,
                         output = None,
                         extensions = [],
                         encoding = None,
                         message_threshold = CRITICAL,
                         safe = False):
    
        global console_hndlr
        console_hndlr.setLevel(message_threshold)
    
        message(DEBUG, "input file: %s" % input)
    
        if not encoding:
            encoding = "utf-8"
    
        input_file = codecs.open(input, mode="r", encoding=encoding)
        text = input_file.read()
        input_file.close()
    
        text = removeBOM(text, encoding)
    
        new_text = markdown(text, extensions, safe_mode = safe)
    
        if output:
            output_file = codecs.open(output, "w", encoding=encoding)
            output_file.write(new_text)
            output_file.close()
    
        else:
            sys.stdout.write(new_text.encode(encoding))
    
    def markdown(text,
                 extensions = [],
                 safe_mode = False):
        
        message(DEBUG, "in markdown.markdown(), received text:\n%s" % text)
    
        extension_names = []
        extension_configs = {}
        
        for ext in extensions:
            pos = ext.find("(") 
            if pos == -1:
                extension_names.append(ext)
            else:
                name = ext[:pos]
                extension_names.append(name)
                pairs = [x.split("=") for x in ext[pos+1:-1].split(",")]
                configs = [(x.strip(), y.strip()) for (x, y) in pairs]
                extension_configs[name] = configs
    
        md = Markdown(extensions=extension_names,
                      extension_configs=extension_configs,
                      safe_mode = safe_mode)
    
        return md.convert(text)
            
    
    class Extension:
    
        def __init__(self, configs = {}):
            self.config = configs
    
        def getConfig(self, key):
            if self.config.has_key(key):
                return self.config[key][0]
            else:
                return ""
    
        def getConfigInfo(self):
            return [(key, self.config[key][1]) for key in self.config.keys()]
    
        def setConfig(self, key, value):
            self.config[key][0] = value
    
    
    OPTPARSE_WARNING = """
    Python 2.3 or higher required for advanced command line options.
    For lower versions of Python use:
    
          %s INPUT_FILE > OUTPUT_FILE
        
    """ % EXECUTABLE_NAME_FOR_USAGE
    
    def parse_options():
    
        try:
            optparse = __import__("optparse")
        except:
            if len(sys.argv) == 2:
                return {'input': sys.argv[1],
                        'output': None,
                        'message_threshold': CRITICAL,
                        'safe': False,
                        'extensions': [],
                        'encoding': None }
    
            else:
                print OPTPARSE_WARNING
                return None
    
        parser = optparse.OptionParser(usage="%prog INPUTFILE [options]")
    
        parser.add_option("-f", "--file", dest="filename",
                          help="write output to OUTPUT_FILE",
                          metavar="OUTPUT_FILE")
        parser.add_option("-e", "--encoding", dest="encoding",
                          help="encoding for input and output files",)
        parser.add_option("-q", "--quiet", default = CRITICAL,
                          action="store_const", const=60, dest="verbose",
                          help="suppress all messages")
        parser.add_option("-v", "--verbose",
                          action="store_const", const=INFO, dest="verbose",
                          help="print info messages")
        parser.add_option("-s", "--safe", dest="safe", default=False,
                          metavar="SAFE_MODE",
                          help="same mode ('replace', 'remove' or 'escape'  user's HTML tag)")
        
        parser.add_option("--noisy",
                          action="store_const", const=DEBUG, dest="verbose",
                          help="print debug messages")
        parser.add_option("-x", "--extension", action="append", dest="extensions",
                          help = "load extension EXTENSION", metavar="EXTENSION")
    
        (options, args) = parser.parse_args()
    
        if not len(args) == 1:
            parser.print_help()
            return None
        else:
            input_file = args[0]
    
        if not options.extensions:
            options.extensions = []
    
        return {'input': input_file,
                'output': options.filename,
                'message_threshold': options.verbose,
                'safe': options.safe,
                'extensions': options.extensions,
                'encoding': options.encoding }
    
    if __name__ == '__main__':
        """ Run Markdown from the command line. """
    
        options = parse_options()
    
        #if os.access(inFile, os.R_OK):
    
        if not options:
            sys.exit(0)
        
        markdownFromFile(**options)
    
    
    
    
    
    
    
    
    
    
    WebHelpers-1.3/webhelpers/text.py0000664000175000017500000003171711471126331016507 0ustar  sluggosluggo# coding: utf-8
    """Functions that output text (not HTML).
    
    Helpers for filtering, formatting, and transforming strings.
    """
    
    import re
    import textwrap
    import urllib
    
    from webhelpers.html.tools import strip_tags
    
    try:
        from unidecode import unidecode
    except ImportError:
        unidecode = None
    
    __all__ = [
        "chop_at",
        "collapse",
        "convert_accented_entities",
        #"convert_misc_characters",   # DISABLED
        "convert_misc_entities",
        "excerpt",
        "lchop",
        "plural",
        "rchop",
        "remove_formatting",
        "replace_whitespace",
        "series",
        "strip_leading_whitespace",
        "truncate", 
        "urlify",
        "wrap_paragraphs",
        ]
    
    def truncate(text, length=30, indicator='...', whole_word=False):
        """Truncate ``text`` with replacement characters.
        
        ``length``
            The maximum length of ``text`` before replacement
        ``indicator``
            If ``text`` exceeds the ``length``, this string will replace
            the end of the string
        ``whole_word``
            If true, shorten the string further to avoid breaking a word in the
            middle.  A word is defined as any string not containing whitespace.
            If the entire text before the break is a single word, it will have to
            be broken.
    
        Example::
    
            >>> truncate('Once upon a time in a world far far away', 14)
            'Once upon a...'
            
        """
        if not text: 
            return ""
        if len(text) <= length:
            return text
        short_length = length - len(indicator)
        if not whole_word:
            return text[:short_length] + indicator
        # Go back to end of previous word.
        i = short_length
        while i >= 0 and not text[i].isspace():
            i -= 1
        while i >= 0 and text[i].isspace():
            i -= 1
        #if i < short_length:
        #    i += 1   # Set to one after the last char we want to keep.
        if i <= 0:
            # Entire text before break is one word, or we miscalculated.
            return text[:short_length] + indicator
        return text[:i+1] + indicator
    
    
    def excerpt(text, phrase, radius=100, excerpt_string="..."):
        """Extract an excerpt from the ``text``, or '' if the phrase isn't
        found.
    
        ``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), re.I)
        match = pat.search(text)
        if not match:
            return ""
        excerpt = match.expand(r'\1')
        if match.start(1) > 0:
            excerpt = excerpt_string + excerpt
        if match.end(1) < len(text):
            excerpt = excerpt + excerpt_string
        if hasattr(text, '__html__'):
            return literal(excertp)
        else:
            return excerpt
    
    
    def plural(n, singular, plural, with_number=True):
        """Return the singular or plural form of a word, according to the number.
    
        If ``with_number`` is true (default), the return value will be the number
        followed by the word. Otherwise the word alone will be returned.
    
        Usage:
    
        >>> plural(2, "ox", "oxen")
        '2 oxen'
        >>> plural(2, "ox", "oxen", False)
        'oxen'
        """
        if n == 1:
            form = singular
        else:
            form = plural
        if with_number:
            return "%s %s" % (n, form)
        else:
            return form
    
    def chop_at(s, sub, inclusive=False):
        """Truncate string ``s`` at the first occurrence of ``sub``.
    
        If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
    
        >>> chop_at("plutocratic brats", "rat")
        'plutoc'
        >>> chop_at("plutocratic brats", "rat", True)
        'plutocrat'
        """
        pos = s.find(sub)
        if pos == -1:
            return s
        if inclusive:
            return s[:pos+len(sub)]
        return s[:pos]
    
    def lchop(s, sub):
        """Chop ``sub`` off the front of ``s`` if present.
    
        >>> lchop("##This is a comment.##", "##")
        'This is a comment.##'
    
        The difference between ``lchop`` and ``s.lstrip`` is that ``lchop`` strips
        only the exact prefix, while ``s.lstrip`` treats the argument as a set of
        leading characters to delete regardless of order.
        """
        if s.startswith(sub):
            s = s[len(sub):]
        return s
        
    def rchop(s, sub):
        """Chop ``sub`` off the end of ``s`` if present.
        
        >>> rchop("##This is a comment.##", "##")
        '##This is a comment.'
    
        The difference between ``rchop`` and ``s.rstrip`` is that ``rchop`` strips
        only the exact suffix, while ``s.rstrip`` treats the argument as a set of
        trailing characters to delete regardless of order.
        """
        if s.endswith(sub):
            s = s[:-len(sub)]
        return s
    
    def strip_leading_whitespace(s):
        """Strip the leading whitespace in all lines in ``s``.
        
        This deletes *all* leading whitespace.  ``textwrap.dedent`` deletes only
        the whitespace common to all lines.
        """
        ret = [x.lstrip() for x in s.splitlines(True)]
        return "".join(ret)
    
    def wrap_paragraphs(text, width=72):
        """Wrap all paragraphs in a text string to the specified width.
    
        ``width`` may be an int or a ``textwrap.TextWrapper`` instance.  
        The latter allows you to set other options besides the width, and is more
        efficient when wrapping many texts.  
        """
        if isinstance(width, textwrap.TextWrapper):
            wrapper = width
        else:
            wrapper = textwrap.TextWrapper(width=width)
        result = []
        lines = text.splitlines(True)
        lines_len = len(lines)
        start = 0
        end = None
        while start < lines_len:
            # Leave short lines as-is.
            if len(lines[start]) <= width:
                result.append(lines[start])
                start += 1
                continue
            # Found a long line, peek forward to end of paragraph.
            end = start + 1
            while end < lines_len and not lines[end].isspace():
                end += 1
            # 'end' is one higher than last long lone.
            paragraph = ''.join(lines[start:end])
            paragraph = wrapper.fill(paragraph) + "\n"
            result.append(paragraph)
            start = end
            end = None
        return "".join(result)
    
    def series(items, conjunction="and", strict_commas=True):
        """Join strings using commas and a conjunction such as "and" or "or".
    
        Examples:
    
        >>> series(["A", "B", "C"])
        'A, B, and C'
        >>> series(["A", "B", "C"], "or")
        'A, B, or C'
        >>> series(["A", "B", "C"], strict_commas=False)
        'A, B and C'
        >>> series(["A", "B"])
        'A and B'
        >>> series(["A"])
        'A'
        >>> series([])
        ''
        """
        items = list(items)
        length = len(items)
        if length == 0:
            return ""
        if length == 1:
            return items[0]
        if length == 2:
            strict_commas = False
        nonlast = ", ".join(items[:-1])
        last = items[-1]
        comma = strict_commas and "," or ""
        return "%s%s %s %s" % (nonlast, comma, conjunction, last)
    
    def urlify(string):
        """Create a URI-friendly representation of the string
        
        Can be called manually in order to generate an URI-friendly version
        of any string.
    
        If the ``unidecode`` package is installed, it will also transliterate 
        non-ASCII Unicode characters to their nearest pronounciation equivalent in
        ASCII.
    
        Examples::
            >>> urlify("Mighty Mighty Bosstones")
            'mighty-mighty-bosstones'
    
        Based on Ruby's stringex package
        (http://github.com/rsl/stringex/tree/master)
    
        Changed in WebHelpers 1.2: urlecode the result in case it contains special
        characters like "?". 
        """
        s = remove_formatting(string).lower()
        s = replace_whitespace(s, '-')
        s = collapse(s, '-')
        return urllib.quote(s)
    
    
    def remove_formatting(string):
        """Simplify HTML text by removing tags and several kinds of formatting.
        
        If the ``unidecode`` package is installed, it will also transliterate 
        non-ASCII Unicode characters to their nearest pronunciation equivalent in
        ASCII.
    
        Based on Ruby's stringex package
        (http://github.com/rsl/stringex/tree/master)
        """
        s = strip_tags(string)
        s = convert_accented_entities(s)
        s = convert_misc_entities(s)
        #s = convert_misc_characters(s)
        if unidecode:
            s = unidecode(s)
        return collapse(s)
    
    
    def convert_accented_entities(string):
        """Converts HTML entities into the respective non-accented letters.
        
        Examples::
        
          >>> convert_accented_entities("á")
          'a'
          >>> convert_accented_entities("ç")
          'c'
          >>> convert_accented_entities("è")
          'e'
          >>> convert_accented_entities("î")
          'i'
          >>> convert_accented_entities("ø")
          'o'
          >>> convert_accented_entities("ü")
          'u'
        
        Note: This does not do any conversion of Unicode/ASCII
        accented-characters. For that functionality please use unidecode.
        
        Based on Ruby's stringex package
        (http://github.com/rsl/stringex/tree/master)
        """
        return re.sub(r'\&([A-Za-z])(grave|acute|circ|tilde|uml|ring|cedil|slash);',
                      r'\1', string)
    
    
    def convert_misc_entities(string):
        """Converts HTML entities (taken from common Textile formattings) 
        into plain text formats
        
        Note: This isn't an attempt at complete conversion of HTML
        entities, just those most likely to be generated by Textile.
        
        Based on Ruby's stringex package
        (http://github.com/rsl/stringex/tree/master)
        """
        replace_dict = {
            "#822[01]": "\"",
            "#821[67]": "'",
            "#8230": "...",
            "#8211": "-",
            "#8212": "--",
            "#215": "x",
            "gt": ">",
            "lt": "<",
            "(#8482|trade)": "(tm)",
            "(#174|reg)": "(r)",
            "(#169|copy)": "(c)",
            "(#38|amp)": "and",
            "nbsp": " ",
            "(#162|cent)": " cent",
            "(#163|pound)": " pound",
            "(#188|frac14)": "one fourth",
            "(#189|frac12)": "half",
            "(#190|frac34)": "three fourths",
            "(#176|deg)": " degrees"
        }
        for textiled, normal in replace_dict.items():
            string = re.sub(r'\&%s;' % textiled, normal, string)
        return re.sub(r'\&[^;]+;', '', string)
    
    
    '''*** DISABLED convert_misc_characters: fails doc tests.
    Confirming what behavior should be.
    
    def convert_misc_characters(string):
        """Converts various common plaintext characters to a more
        URI-friendly representation
        
        Examples::
          
            >>> convert_misc_characters("foo & bar")
            'foo and bar'
            >>> convert_misc_characters("Chanel #9")
            'Chanel number nine'
            >>> convert_misc_characters("user@host")
            'user at host'
            >>> convert_misc_characters("google.com")
            'google dot com'
            >>> convert_misc_characters("$10")
            '10 dollars'
            >>> convert_misc_characters("*69")
            'star 69'
            >>> convert_misc_characters("100%")
            '100 percent'
            >>> convert_misc_characters("windows/mac/linux")
            'windows slash mac slash linux'
          
        Note: Because this method will convert any '&' symbols to the string
        "and", you should run any methods which convert HTML entities 
        (convert_html_entities and convert_misc_entities) before running
        this method.
        
        Based on Ruby's stringex package
        (http://github.com/rsl/stringex/tree/master)
        """
        s = re.sub(r'\.{3,}', " dot dot dot ", string)
        
        # Special rules for money
        money_replace = {
            r'(\s|^)\$(\d+)\.(\d+)(\s|\$)?': r'\2 dollars \3 cents',
            r'(\s|^)£(\d+)\.(\d+)(\s|\$)?': r'\2 pounds \3 pence',
        }
        for repl, subst in money_replace.items():
            s = re.sub(repl, r' %s ' % subst, s)
        
        # Back to normal rules
        repls =  {
            r'\s*&\s*': "and",
            r'\s*#': "number",
            r'\s*@\s*': "at",
            r'(\S|^)\.(\S)': r'\1 dot \2',
            r'(\s|^)\$(\d*)(\s|$)': r'\2 dollars',
            r'(\s|^)£(\d*)(\s|$)': r'\2 pounds',
            r'(\s|^)¥(\d*)(\s|$)': r'\2 yen',
            r'\s*\*\s*': "star",
            r'\s*%\s*': "percent",
            r'\s*(\\|\/)\s*': "slash",
        }
        for repl, subst in repls.items():
            s = re.sub(repl, r' %s ' % subst, s)
        s = re.sub(r"(^|\w)'(\w|$)", r'\1\2', s)
        return re.sub(r"[\.\,\:\;\(\)\[\]\/\?\!\^'\"_]", " ", s)
    '''
    
    
    def replace_whitespace(string, replace=" "):
        """Replace runs of whitespace in string
        
        Defaults to a single space but any replacement string may be
        specified as an argument. Examples::
    
            >>> replace_whitespace("Foo       bar")
            'Foo bar'
            >>> replace_whitespace("Foo       bar", "-")
            'Foo-bar'
        
        Based on Ruby's stringex package
        (http://github.com/rsl/stringex/tree/master)
        """
        return re.sub(r'\s+', replace, string)
     
    def collapse(string, character=" "):
        """Removes specified character from the beginning and/or end of the
        string and then condenses runs of the character within the string.
        
        Based on Ruby's stringex package
        (http://github.com/rsl/stringex/tree/master)
        """
        reg = re.compile('(%s){2,}' % character)
        return re.sub(reg, character, string.strip(character))
    WebHelpers-1.3/webhelpers/public/0000775000175000017500000000000011542603374016424 5ustar  sluggosluggoWebHelpers-1.3/webhelpers/public/stylesheets/0000775000175000017500000000000011542603374021000 5ustar  sluggosluggoWebHelpers-1.3/webhelpers/public/stylesheets/grid.css0000664000175000017500000000416311373653355022451 0ustar  sluggosluggo/******************* tables ****************/
    table.stylized {
    	background-color: #ffffff;
    	border-collapse: separate;
    	border-spacing: 1px;
    	border-bottom: 2px solid #666666;
    	margin: 1px 5px 5px 5px;
    	-moz-border-radius: 5px;
    	-webkit-border-radius: 5px;
    	width: 100%;
    	border-collapse: collapse;
    }
    
    table.stylized caption {
    	color: #ffffff;
    	background-color: #444466;
    	padding: 5px;
    	font-size: 1.3em;
    	font-weight: bold;
    	margin: 5px 0px 0px 0px;
    	-moz-border-radius: 5px;
    	-webkit-border-radius: 5px;
    }
    
    
    
    table.stylized caption a:link,table.stylized caption a:visited {
    	color: #ffffff;
    	text-decoration: none;
    	font-weight: bold;
    }
    
    table.stylized caption a:link,table.stylized caption a:hover {
    	color: #ffcc00;
    	text-decoration: none;
    	font-weight: bold;
    }
    	
    table.stylized thead {
    	background-color: #ffffff;
    }
    
    table.stylized tbody {
    	background-color: #ffffff;
    }
    
    table.stylized tfooter {
    	background-color: #ffffff;
    }
    
    table.stylized th {
    	text-align: center;
    }
    
    table.stylized tr.header {
    	text-align: center;
    }
    
    table.stylized tr.header td, table.stylized th {
    	text-align: center;
    	color: #ffffff;
    	background-color: #444466;
    }
    
    table.stylized td {
    	padding: 5px 5px 5px 5px;
    	border: 1px solid #dcdcdc;
    }
    
    
    table.stylized tr.odd td {
    	border-top: 1px solid #999 !important;
    	background-color: #ffffff;
    }
    
    table.stylized tr.even td {
    	border-top: 1px solid #999 !important;
    	background-color: #f6f6f6;
    }
    
    table.stylized .no {
    	width: 30px;
    }
    
    table.stylized td.ordering{
    	background-color: #666666 !important;
    	padding-right: 20px;
    }
    
    table.stylized td.ordering.dsc .marker {
    	height: 20px;
    	width: 20px;
    	display: block;
    	float: right;
    	margin: 0px -18px;
    /* background-image for neutral marker here */
    }
    
    table.stylized td.ordering.dsc .marker {
    /* background-image for dsc marker here */
    }
    
    table.stylized td.ordering.asc .marker {
    /* background-image for asc marker here */
    }
    
    table.stylized .header a:link,table.stylized .header a:visited {
    	color: #ffffff;
    	text-decoration: none;
    	font-weight: bold;
    }
    
    table.stylized td.ordering a:link,table.stylized td.ordering a:visited {
    	color: #ffcc00;
    	text-decoration: none;
    	font-weight: bold;
    }
    WebHelpers-1.3/webhelpers/public/stylesheets/webhelpers.css0000644000175000017500000000230611373653355023657 0ustar  sluggosluggo/* Sample stylesheet for WebHelpers */
    
    /* === STYLES FOR webhelpers.html.tags === */
    
    /* Display the required symbol "*" in red.
     * Used by title() and required_legend().
     */
    span.required-symbol {
        color: red;
        }
    
    /* Display the title for required fields in boldface.
     * Used by title() and required_legend().
     */
    span.required {
        font-weight: bold;
        }
    
    /* Display the title for non-required fields in normal type.
     * (Some sites may prefer to set this to boldface too.)
     * Used by title().
     */
    span.not-required {
        }
    
    /* === EXTRA STYLES (not currently produced by any helpers) === */
    
    /* Make field error messages highly visible:
     * big and red on a light red background.
     * 'error-message' is the default class used by ``formencode.htmlfill``
     * and by any future field helper.
     */
    .error-message {
        color: #cc0000;
        background-color: #ffeeee;
        font-size: large;
        font-weight: bold;
        font-style: italic;
        }
    
    /* Make field help text green italic. */
    .hint {
        color: #006400;
        font-style: italic;
        }
    
    /* Standard forms have a distinct background and space around them. */
    form.standard {
        background-color: #F2F2F2;
        margin: 1em 0;
        padding: 0.5em 1em 1em 1em;
        }
    
    WebHelpers-1.3/webhelpers/number.py0000664000175000017500000003410711535740723017017 0ustar  sluggosluggo"""Number formatting, numeric helpers, and numeric statistics."""
    
    import math
    import re
    
    #### Calculations ####
    
    def percent_of(part, whole):
        """What percent of ``whole`` is ``part``?
    
        >>> percent_of(5, 100)
        5.0
        >>> percent_of(13, 26)
        50.0
        """
        # Use float to force true division.
        return float(part * 100) / whole
    
    #### Statistics ####
    
    def mean(r):
        """Return the mean (i.e., average) of a sequence of numbers.
    
        >>> mean([5, 10])
        7.5
        """
        try:
            return float(sum(r)) / len(r)
        except ZeroDivisionError:
            raise ValueError("can't calculate mean of empty collection")
    
    average = mean
    
    def median(r):
        """Return the median of an iterable of numbers.
    
        The median is the point at which half the numbers are lower than it and
        half the numbers are higher.  This gives a better sense of the majority
        level than the mean (average) does, because the mean can be skewed by a few
        extreme numbers at either end.  For instance, say you want to calculate
        the typical household income in a community and you've sampled four
        households:
    
        >>> incomes = [18000]       # Fast food crew
        >>> incomes.append(24000)   # Janitor
        >>> incomes.append(32000)   # Journeyman
        >>> incomes.append(44000)   # Experienced journeyman
        >>> incomes.append(67000)   # Manager
        >>> incomes.append(9999999) # Bill Gates
        >>> median(incomes)
        49500.0
        >>> mean(incomes)
        1697499.8333333333
    
        The median here is somewhat close to the majority of incomes, while the
        mean is far from anybody's income.
        
        This implementation makes a temporary list of all numbers in memory.
        """
        s = list(r)
        s_len = len(s)
        if s_len == 0:
            raise ValueError("can't calculate mean of empty collection")
        s.sort()
        center = s_len // 2
        is_odd = s_len % 2
        if is_odd:
            return s[center]   # Return the center element.
        # Return the average of the two elements nearest the center.
        low = s[center-1]
        high = s[center+1]
        return mean([low, high])
    
    def standard_deviation(r, sample=True):
        """Standard deviation. 
        
        `From the Python Cookbook
        `_.
        Population mode contributed by Lorenzo Catucci.
    
        Standard deviation shows the variability within a sequence of numbers.
        A small standard deviation means the numbers are close to each other.  A
        large standard deviation shows they are widely different.  In fact it
        shows how far the numbers tend to deviate from the average.  This can be
        used to detect whether the average has been skewed by a few extremely high
        or extremely low values.
    
        Most natural and random phenomena follow the normal distribution (aka the
        bell curve), which says that most values are close to average but a few are
        extreme.  E.g., most people are close to 5'9" tall but a few are very tall
        or very short.  If the data does follow the bell curve, 68% of the values
        will be within 1 standard deviation (stdev) of the average, and 95% will be
        within 2 standard deviations.  So a university professor grading exams on a
        curve might give a "C" (mediocre) grade to students within 1 stdev of the
        average score, "B" (better than average) to those within 2 stdevs above,
        and "A" (perfect) to the 0.25% higher than 2 stdevs.  Those between 1 and 2
        stdevs below get a "D" (poor), and those below 2 stdevs... we won't talk
        about them.
    
        By default the helper computes the unbiased estimate
        for the population standard deviation, by applying an unbiasing
        factor of sqrt(N/(N-1)).
    
        If you'd rather have the function compute the population standard
        deviation, pass ``sample=False``.
    
        The following examples are taken from Wikipedia.
        http://en.wikipedia.org/wiki/Standard_deviation
    
            >>> standard_deviation([0, 0, 14, 14]) # doctest: +ELLIPSIS
            8.082903768654761...
            >>> standard_deviation([0, 6, 8, 14]) # doctest: +ELLIPSIS
            5.773502691896258...
            >>> standard_deviation([6, 6, 8, 8])
            1.1547005383792515
            >>> standard_deviation([0, 0, 14, 14], sample=False)
            7.0
            >>> standard_deviation([0, 6, 8, 14], sample=False)
            5.0
            >>> standard_deviation([6, 6, 8, 8], sample=False)
            1.0
    
        (The results reported in Wikipedia are those expected for whole
        population statistics and therefore are equal to the ones we get
        by setting ``sample=False`` in the later tests.)
        
        .. code-block:: pycon
        
            # Fictitious average monthly temperatures in Southern California.
            #                       Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec
            >>> standard_deviation([70, 70, 70, 75, 80, 85, 90, 95, 90, 80, 75, 70]) # doctest: +ELLIPSIS
            9.003366373785...
            >>> standard_deviation([70, 70, 70, 75, 80, 85, 90, 95, 90, 80, 75, 70], sample=False) # doctest: +ELLIPSIS
            8.620067027323...
    
            # Fictitious average monthly temperatures in Montana.
            #                       Jan  Feb  Mar Apr May Jun Jul  Aug Sep Oct Nov Dec
            >>> standard_deviation([-32, -10, 20, 30, 60, 90, 100, 80, 60, 30, 10, -32]) # doctest: +ELLIPSIS
            45.1378360405574...
            >>> standard_deviation([-32, -10, 20, 30, 60, 90, 100, 80, 60, 30, 10, -32], sample=False) # doctest: +ELLIPSIS
            43.2161878106906...
        """
        avg = average(r)
        sdsq = sum([(i - avg) ** 2 for i in r])
        if sample:
            normal_denom=len(r) - 1 or 1
        else:
            normal_denom=len(r)
        return (sdsq / normal_denom) ** 0.5
    
    class SimpleStats(object):
        """Calculate a few simple statistics on data.
        
        This class calculates the minimum, maximum, and count of all the values
        given to it.  The values are not saved in the object.  Usage::
    
            >>> stats = SimpleStats()
            >>> stats(2)               # Add one data value.
            >>> stats.extend([6, 4])   # Add several data values at once.  
    
        The statistics are available as instance attributes::
    
            >>> stats.count
            3
            >>> stats.min
            2
            >>> stats.max
            6
    
        Non-numeric data is also allowed:
    
        >>> stats2 = SimpleStats()
        >>> stats2("foo")
        >>> stats2("bar")
        >>> stats2.count
        2
        >>> stats2.min
        'bar'
        >>> stats2.max
        'foo'
    
        ``.min`` and ``.max`` are ``None`` until the first data value is
        registered.
    
        Subclasses can override ``._init_stats`` and ``._update_stats`` to add
        additional statistics. 
        
        The constructor accepts one optional argument, ``numeric``. If true, the
        instance accepts only values that are ``int``, ``long``, or ``float``.
        The default is false, which accepts any value. This is meant for instances
        or subclasses that don't want non-numeric values.
        """
        __version__ = 1
    
        def __init__(self, numeric=False):
            self.numeric = numeric
            self.count = 0
            self.min = None
            self.max = None
            self._init_stats()
            
        def __nonzero__(self):
            """The instance is true if it has seen any data."""
            return bool(self.count)
    
        def __call__(self, value):
            """Add a data value."""
            if self.numeric:
                value + 0   # Raises TypeError if value is not numeric.
            if self.count == 0:
                self.min = self.max = value
            else:
                self.min = min(self.min, value)
                self.max = max(self.max, value)
            self.count += 1
            self._update_stats(value)
    
        def extend(self, values):
            """Add several data values at once, akin to ``list.extend``."""
            for value in values:
                self(value)
    
        ### Hooks for subclasses
        def _init_stats(self):
            """Initialize state data used by subclass statistics."""
            pass
    
        def _update_stats(self, value):
            """Add a value to the subclass statistics."""
            pass
    
    
    class Stats(SimpleStats):
        """A container for data and statistics.
    
        This class extends ``SimpleStats`` by calculating additional statistics,
        and by storing all data seen.  All values must be numeric (``int``,
        ``long``, and/or ``float``), and you must call ``.finish()`` to generate
        the additional statistics.  That's because the statistics here cannot be
        calculated incrementally, but only after all data is known.
    
        
        >>> stats = Stats()
        >>> stats.extend([5, 10, 10])
        >>> stats.count
        3
        >>> stats.finish()
        >>> stats.mean # doctest: +ELLIPSIS
        8.33333333333333...
        >>> stats.median
        10
        >>> stats.standard_deviation
        2.8867513459481287
    
        All data is stored in a list and a set for later use::
    
            >>> stats.list
            [5, 10, 10]
    
            >>  stats.set
            set([5, 10])
    
        (The double prompt ">>" is used to hide the example from doctest.)
    
        The stat attributes are ``None`` until you call ``.finish()``.  It's
        permissible -- though not recommended -- to add data after calling
        ``.finish()`` and then call ``.finish()`` again.  This recalculates the
        stats over the entire data set.
    
        In addition to the hook methods provided by ``SimpleStats``, subclasses
        can override ``._finish-stats`` to provide additional statistics.
        """
        __version__ = 1
    
        def __init__(self):
            SimpleStats.__init__(self, numeric=True)
            self.list = []
            self.set = set()
            self.mean = None
            self.median = None
            self.standard_deviation = None
            self._init_stats()
    
        def __call__(self, value):
            """Add a data value."""
            if self.count == 0:
                self.min = self.max = value
            else:
                self.min = min(self.min, value)
                self.max = max(self.max, value)
            self.count += 1
            self._update_stats(value)
            self.list.append(value)
            self.set.add(value)
    
        def finish(self):
            """Finish calculations. (Call after adding all data values.)
            
            Call this after adding all data values, or the results will be
            incomplete.
            """
            self.mean = mean(self.list)
            self.median = median(self.list)
            self.standard_deviation = standard_deviation(self.list)
            self._finish_stats()
    
        ### Hooks for subclasses.
        def _finish_stats(self):
            """Finish the subclass statistics now that all data are known."""
            pass
    
    #### Number formatting ####
    
    def format_number(n, thousands=",", decimal="."):
        """Format a number with a thousands separator and decimal delimiter.
    
        ``n`` may be an int, long, float, or numeric string.
        ``thousands`` is a separator to put after each thousand.
        ``decimal`` is the delimiter to put before the fractional portion if any.
    
        The default style has a thousands comma and decimal point per American
        usage:
    
        >>> format_number(1234567.89)
        '1,234,567.89'
        >>> format_number(123456)
        '123,456'
        >>> format_number(-123)
        '-123'
    
        Various European and international styles are also possible:
    
        >>> format_number(1234567.89, " ")
        '1 234 567.89'
        >>> format_number(1234567.89, " ", ",")
        '1 234 567,89'
        >>> format_number(1234567.89, ".", ",")
        '1.234.567,89'
        """
        parts = str(n).split(".")
        parts[0] = re.sub(
            R"(\d)(?=(\d\d\d)+(?!\d))", 
            R"\1%s" % thousands, 
            parts[0])
        return decimal.join(parts)
    
    def format_data_size(size, unit, precision=1, binary=False, full_name=False):
        """Format a number using SI units (kilo, mega, etc.).
    
        ``size``: The number as a float or int.
    
        ``unit``: The unit name in plural form. Examples: "bytes", "B".
    
        ``precision``: How many digits to the right of the decimal point. Default
        is 1.  0 suppresses the decimal point.
    
        ``binary``: If false, use base-10 decimal prefixes (kilo = K = 1000).  
        If true, use base-2 binary prefixes (kibi = Ki = 1024).  
    
        ``full_name``: If false (default), use the prefix abbreviation ("k" or
        "Ki").  If true, use the full prefix ("kilo" or "kibi"). If false,
        use abbreviation ("k" or "Ki").
    
        Examples:
    
        >>> format_data_size(1024, "B")
        '1.0 kB'
        >>> format_data_size(1024, "B", 2)
        '1.02 kB'
        >>> format_data_size(1024, "B", 2, binary=True)
        '1.00 KiB'
        >>> format_data_size(54000, "Wh", 0)
        '54 kWh'
        >>> format_data_size(85000, "m/h", 0)
        '85 km/h'
        >>> format_data_size(85000, "m/h", 0).replace("km/h", "klicks")
        '85 klicks'
        """
        # Contributed by Wojciech Malinowski
        if full_name is None:
            full_name = len(unit) > 1
            
        if not binary:
            base = 1000
            if full_name:
                multiples = ('', 'kilo', 'mega', 'giga', 'tera', 'peta', 'exa', 'zetta', 'yotta')
            else:
                multiples = ('', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y')
        else:
            base = 1024
            if full_name:
                multiples = ('', 'kibi', 'mebi', 'gibi', 'tebi', 'pebi', 'exbi', 'zebi', 'yobi')
            else:
                multiples = ('', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi', 'Yi')
                
        if size <= 0:
            m = 0
        else:
            m = int(math.log(size) / math.log(base))
        if m > 8:
            m = 8
    
        if m == 0:
            precision = '%.0f'
        else:
            precision = '%%.%df' % precision
            
        size = precision % (size / math.pow(base, m))
    
        return '%s %s%s' % (size.strip(), multiples[m], unit)
    
    def format_byte_size(size, precision=1, binary=False, full_name=False):
        """Same as ``format_data_size`` but specifically for bytes.
        
        Examples:
    
        >>> format_byte_size(2048)
        '2.0 kB'
        >>> format_byte_size(2048, full_name=True)
        '2.0 kilobytes'
        """
        if full_name:
            return format_data_size(size, "bytes", precision, binary, True)
        else:
            return format_data_size(size, "B", precision, binary, False)
    
    def format_bit_size(size, precision=1, binary=False, full_name=False):
        """Same as ``format_data_size`` but specifically for bits.
    
        Examples:
    
        >>> format_bit_size(2048)
        '2.0 kb'
        >>> format_bit_size(2048, full_name=True)
        '2.0 kilobits'
        """
        if full_name:
            return format_data_size(size, "bits", precision, binary, True)
        else:
            return format_data_size(size, "b", precision, binary, False)
    
    if __name__ == "__main__":
        import doctest
        doctest.testmod()
    WebHelpers-1.3/webhelpers/__init__.py0000664000175000017500000000016111445174513017255 0ustar  sluggosluggo"""WebHelpers is wide variety of functions for web applications and other
    applications.
    """
    
    __version__ = "1.3"
    WebHelpers-1.3/webhelpers/containers.py0000664000175000017500000004417411471126331017671 0ustar  sluggosluggo"""Container objects, and helpers for lists and dicts.
    
    This would have been called this "collections" except that Python 2 can't
    import a top-level module that's the same name as a module in the current
    package.
    """
    
    import sys
    
    from webhelpers.misc import NotGiven
    
    try:
        from collections import defaultdict
    except ImportError:   # Python < 2.5
        class defaultdict(dict):
            """Backport of Python 2.5's ``defaultdict``.
    
            From the Python Cookbook.  Written by Jason Kirtland.
            http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/523034
    
            """
            def __init__(self, default_factory=None, *a, **kw):
                if (default_factory is not None and
                    not hasattr(default_factory, '__call__')):
                    raise TypeError('first argument must be callable')
                dict.__init__(self, *a, **kw)
                self.default_factory = default_factory
            def __getitem__(self, key):
                try:
                    return dict.__getitem__(self, key)
                except KeyError:
                    return self.__missing__(key)
            def __missing__(self, key):
                if self.default_factory is None:
                    raise KeyError(key)
                self[key] = value = self.default_factory()
                return value
            def __reduce__(self):
                if self.default_factory is None:
                    args = tuple()
                else:
                    args = self.default_factory,
                return type(self), args, None, None, self.items()
            def copy(self):
                return self.__copy__()
            def __copy__(self):
                return type(self)(self.default_factory, self)
            def __deepcopy__(self, memo):
                import copy
                return type(self)(self.default_factory,
                                  copy.deepcopy(self.items()))
            def __repr__(self):
                return 'defaultdict(%s, %s)' % (self.default_factory,
                                                dict.__repr__(self))
    
    class DumbObject(object):
        """A container for arbitrary attributes.
    
        Usage::
        
            >>> do = DumbObject(a=1, b=2)
            >>> do.b
            2
        
        Alternatives to this class include ``collections.namedtuple`` in Python
        2.6, and ``formencode.declarative.Declarative`` in Ian Bicking's FormEncode
        package.  Both alternatives offer more features, but ``DumbObject``
        shines in its simplicity and lack of dependencies.
    
        """
        def __init__(self, **kw):
            self.__dict__.update(kw)
    
    
    class Counter(object):
        """I count the number of occurrences of each value registered with me.
        
        Call the instance to register a value. The result is available as the
        ``.result`` attribute.  Example::
    
            >>> counter = Counter()
            >>> counter("foo")
            >>> counter("bar")
            >>> counter("foo")
            >>> sorted(counter.result.items())
            [('bar', 1), ('foo', 2)]
    
            >> counter.result
            {'foo': 2, 'bar': 1}
    
        To see the most frequently-occurring items in order::
    
            >>> counter.get_popular(1)
            [(2, 'foo')]
            >>> counter.get_popular()
            [(2, 'foo'), (1, 'bar')]
    
        Or if you prefer the list in item order::
    
            >>> counter.get_sorted_items()
            [('bar', 1), ('foo', 2)]
        """
    
        def __init__(self):
            self.result = defaultdict(int)
            self.total = 0  # Number of times instance has been called.
    
        def __call__(self, item):
            """Register an item with the counter."""
            self.result[item] += 1
            self.total += 1
    
        def get_popular(self, max_items=None):
            """Return the results as as a list of ``(count, item)`` pairs, with the
            most frequently occurring items first.
    
            If ``max_items`` is provided, return no more than that many items.
            """
            data = [(x[1], x[0]) for x in self.result.iteritems()]
            data.sort(key=lambda x: (sys.maxint - x[0], x[1]))
            if max_items:
                return data[:max_items]
            else:
                return data
    
        def get_sorted_items(self):
            """Return the result as a list of ``(item, count)`` pairs sorted by item.
            """
            data = self.result.items()
            data.sort()
            return data
    
        def correlate(class_, iterable):
            """Build a Counter from an iterable in one step.
    
            This is the same as adding each item individually.
    
            ::
    
                >>> counter = Counter.correlate(["A", "B", "A"])
                >>> counter.result["A"]
                2
                >>> counter.result["B"]
                1
            """
            counter = class_()
            for elm in iterable:
                counter(elm)
            return counter
        correlate = classmethod(correlate)
    
    
    class Accumulator(object):
        """Accumulate a dict of all values for each key.
    
        Call the instance to register a value. The result is available as the
        ``.result`` attribute.  Example::
    
            >>> bowling_scores = Accumulator()
            >>> bowling_scores("Fred", 0)
            >>> bowling_scores("Barney", 10)
            >>> bowling_scores("Fred", 1)
            >>> bowling_scores("Barney", 9)
            >>> sorted(bowling_scores.result.items())
            [('Barney', [10, 9]), ('Fred', [0, 1])]
    
            >> bowling_scores.result
            {'Fred': [0, 1], 'Barney': [10, 9]}
    
        The values are stored in the order they're registered.
    
        Alternatives to this class include ``paste.util. multidict.MultiDict``
        in Ian Bicking's Paste package.
        """
    
        def __init__(self):
            self.result = defaultdict(list)
    
        def __call__(self, key, value):
            """Register a key-value pair."""
            self.result[key].append(value)
    
        def correlate(class_, iterable, key):
            """Create an Accumulator based on several related values.
    
            ``key`` is a function to calculate the key for each item, akin to
            ``list.sort(key=)``.
    
            This is the same as adding each item individually.
            """
            accumulator = class_()
            for v in iterable:
                k = key(v)
                accumulator(k, v)
            return accumulator
        correlate = classmethod(correlate)
    
    class UniqueAccumulator(object):
        """Accumulate a dict of unique values for each key.
    
        The values are stored in an unordered set.
    
        Call the instance to register a value. The result is available as the
        ``.result`` attribute.
        """
    
        def __init__(self):
            self.result = defaultdict(set)
    
        def __call__(self, key, value):
            """Register a key-value pair."""
            self.result[key].add(value)
    
    
    def unique(it):
        """Return a list of unique elements in the iterable, preserving the order.
    
        Usage::
    
            >>> unique([None, "spam", 2, "spam", "A", "spam", "spam", "eggs", "spam"])
            [None, 'spam', 2, 'A', 'eggs']
        """
        seen = set()
        ret = []
        for elm in it:
            if elm not in seen:
                ret.append(elm)
                seen.add(elm)
        return ret
    
    def only_some_keys(dic, keys):
        """Return a copy of the dict with only the specified keys present.  
        
        ``dic`` may be any mapping. The return value is always a Python dict.
    
        ::
    
            >> only_some_keys({"A": 1, "B": 2, "C": 3}, ["A", "C"])
            >>> sorted(only_some_keys({"A": 1, "B": 2, "C": 3}, ["A", "C"]).items())
            [('A', 1), ('C', 3)]
        """
        ret = {}
        for key in keys:
            ret[key] = dic[key]   # Raises KeyError.
        return ret
    
    def except_keys(dic, keys):
        """Return a copy of the dict without the specified keys.
    
        ::
    
            >>> except_keys({"A": 1, "B": 2, "C": 3}, ["A", "C"])
            {'B': 2}
        """
        ret = dic.copy()
        for key in keys:
            try:
                del ret[key]
            except KeyError:
                pass
        return ret
    
    def extract_keys(dic, keys):
        """Return two copies of the dict.  The first has only the keys specified.
        The second has all the *other* keys from the original dict.
    
        ::
    
            >> extract_keys({"From": "F", "To": "T", "Received", R"}, ["To", "From"]) 
            ({"From": "F", "To": "T"}, {"Received": "R"})
            >>> regular, extra = extract_keys({"From": "F", "To": "T", "Received": "R"}, ["To", "From"]) 
            >>> sorted(regular.keys())
            ['From', 'To']
            >>> sorted(extra.keys())
            ['Received']
        """
        for k in keys:
            if k not in dic:
                raise KeyError("key %r is not in original mapping" % k)
        r1 = {}
        r2 = {}
        for k, v in dic.items():
            if k in keys:
                r1[k] = v
            else:
                r2[k] = v
        return r1, r2
    
    def ordered_items(dic, key_order, other_keys=True, default=NotGiven):
        """Like ``dict.iteritems()`` but with a specified key order.
    
        Arguments:
    
        * ``dic`` is any mapping.
        * ``key_order`` is a list of keys.  Items will be yielded in this order.
        * ``other_keys`` is a boolean.
        * ``default`` is a value returned if the key is not in the dict.
    
        This yields the items listed in ``key_order``.  If a key does not exist
        in the dict, yield the default value if specified, otherwise skip the
        missing key.  Afterwards, if ``other_keys`` is true, yield the remaining
        items in an arbitrary order.
    
        Usage::
    
            >>> dic = {"To": "you", "From": "me", "Date": "2008/1/4", "Subject": "X"}
            >>> dic["received"] = "..."
            >>> order = ["From", "To", "Subject"]
            >>> list(ordered_items(dic, order, False))
            [('From', 'me'), ('To', 'you'), ('Subject', 'X')]
        """
        d = dict(dic)
        for key in key_order:
            if key in d:
                yield key, d.pop(key)
            elif default is not NotGiven:
                yield key, default
        if other_keys:
            for key, value in d.iteritems():
                yield key, value
    
    def get_many(d, required=None, optional=None, one_of=None):
        """Extract values from a dict for unpacking into simple variables.
    
        ``d`` is a dict.
    
        ``required`` is a list of keys that must be in the dict. The corresponding
        values will be the first elements in the return list. Raise KeyError if any
        of the keys are missing.
    
        ``optional`` is a list of optional keys. The corresponding values will be
        appended to the return list, substituting None for missing keys.
    
        ``one_of`` is a list of alternative keys. Take the first key that exists 
        and append its value to the list. Raise KeyError if none of the keys exist.
        This argument will append exactly one value if specified, or will do
        nothing if not specified.
    
        Example::
    
            uid, action, limit, offset = get_many(request.params, 
                required=['uid', 'action'], optional=['limit', 'offset'])
    
        Contributed by Shazow.
    
        """
        r = []
        if required:
            for k in required:
                r.append(d[k])
        if optional:
            for k in optional:
                r.append(d.get(k))
        if one_of:
            for k in one_of:
                if k in d:
                    r.append(d[k])
                    break
            else:
                raise KeyError("none of these keys found: %s" % one_of)
        return r
    
    def del_quiet(dic, keys):
        """Delete several keys from a dict, ignoring those that don't exist.
        
        This modifies the dict in place.
    
        ::
    
            >>> d ={"A": 1, "B": 2, "C": 3}
            >>> del_quiet(d, ["A", "C"])
            >>> d
            {'B': 2}
        """
        for key in keys:
            try:
                del dic[key]
            except KeyError:
                pass
    
    def correlate_dicts(dicts, key):
        """Correlate several dicts under one superdict.
    
        If you have several dicts each with a 'name' key, this 
        puts them in a container dict keyed by name.
    
        ::
    
            >>> d1 = {"name": "Fred", "age": 41}
            >>> d2 = {"name": "Barney", "age": 31}
            >>> flintstones = correlate_dicts([d1, d2], "name")
            >>> sorted(flintstones.keys())
            ['Barney', 'Fred']
            >>> flintstones["Fred"]["age"]
            41
    
        If you're having trouble spelling this method correctly, remember:
        "relate" has one 'l'.  The 'r' is doubled because it occurs after a prefix.
        Thus "correlate".
        """
        ret = {}
        i = 0
        for d in dicts:
            try:
                my_key = d[key]
            except KeyError:
                msg = "'dicts' element %d contains no key '%s'"
                tup = i, key 
                raise KeyError(msg % tup)
            ret[my_key] = d
            i += 1
        return ret
    
    
    
    def correlate_objects(objects, attr):
        """Correlate several objects under one dict.
    
        If you have several objects each with a 'name' attribute, this
        puts them in a dict keyed by name.
    
        ::
    
            >>> class Flintstone(DumbObject):
            ...    pass
            ...
            >>> fred = Flintstone(name="Fred", age=41)
            >>> barney = Flintstone(name="Barney", age=31)
            >>> flintstones = correlate_objects([fred, barney], "name")
            >>> sorted(flintstones.keys())
            ['Barney', 'Fred']
            >>> flintstones["Barney"].age
            31
    
        If you're having trouble spelling this method correctly, remember:
        "relate" has one 'l'.  The 'r' is doubled because it occurs after a prefix.
        Thus "correlate".
        """
        ret = {}
        i = 0
        for obj in objects:
            try:
                my_key = getattr(obj, attr)
            except AttributeError:
                msg = "'%s' object at 'objects[%d]' contains no attribute '%s'"
                tup = type(obj).__name__, i, attr 
                raise AttributeError(msg % tup)
            ret[my_key] = obj
            i += 1
        return ret
    
    
    def distribute(lis, columns, direction, fill=None):
        """Distribute a list into a N-column table (list of lists).
    
        ``lis`` is a list of values to distribute.
    
        ``columns`` is an int greater than 1, specifying the number of columns in
        the table.
    
        ``direction`` is a string beginning with "H" (horizontal) or "V"
        (vertical), case insensitive.  This affects how values are distributed in
        the table, as described below.
    
        ``fill`` is a value that will be placed in any remaining cells if the data
        runs out before the last row or column is completed.  This must be an 
        immutable value such as ``None`` , ``""``, 0, " ", etc.  If you
        use a mutable value like ``[]`` and later change any cell containing the
        fill value, all other cells containing the fill value will also be changed.
    
        The return value is a list of lists, where each sublist represents a row in
        the table.
        ``table[0]`` is the first row.
        ``table[0][0]`` is the first column in the first row.
        ``table[0][1]`` is the second column in the first row.
    
        This can be displayed in an HTML table via the following Mako template:
    
        .. code-block:: html+mako
    
            
            % for row in table:
              
            % for cell in row:
                
            % endfor   cell
              
            % endfor   row
            
    ${cell}
    In a horizontal table, each row is filled before going on to the next row. This is the same as dividing the list into chunks:: >>> distribute([1, 2, 3, 4, 5, 6, 7, 8], 3, "H") [[1, 2, 3], [4, 5, 6], [7, 8, None]] In a vertical table, the first element of each sublist is filled before going on to the second element. This is useful for displaying an alphabetical list in columns, or when the entire column will be placed in a single with a
    between each element:: >>> food = ["apple", "banana", "carrot", "daikon", "egg", "fish", "gelato", "honey"] >>> table = distribute(food, 3, "V", "") >>> table [['apple', 'daikon', 'gelato'], ['banana', 'egg', 'honey'], ['carrot', 'fish', '']] >>> for row in table: ... for item in row: ... print "%-9s" % item, ... print "." # To show where the line ends. ... apple daikon gelato . banana egg honey . carrot fish . Alternatives to this function include a NumPy matrix of objects. """ if columns < 1: raise ValueError("arg 'columns' must be >= 1") dir = direction[0].upper() if dir == "H": # Horizontal table (row-wise) table = [] for i in range(0, len(lis), columns): row = lis[i:i+columns] row_len = len(row) if row_len < columns: extra = [fill] * (columns - row_len) row.extend(extra) table.append(row) return table elif dir == "V": # Vertical table (column-wise) total = len(lis) rows, remainder = divmod(total, columns) if remainder: rows += 1 table = [[fill] * columns for x in range(rows)] #print table for i, elm in enumerate(lis): col, row = divmod(i, rows) #print "i=%d, row=%d, col=%d, element=%r" % (i, row, col, elm) table[row][col] = elm return table else: raise ValueError("arg ``direction`` must start with 'H' or 'V'") def transpose(array): """Turn a list of lists sideways, making columns into rows and vice-versa. ``array`` must be rectangular; i.e., all elements must be the same length. Otherwise the behavior is undefined: you may get ``IndexError`` or missing items. Examples:: >>> transpose([["A", "B", "C"], ["D", "E", "F"]]) [['A', 'D'], ['B', 'E'], ['C', 'F']] >>> transpose([["A", "B"], ["C", "D"], ["E", "F"]]) [['A', 'C', 'E'], ['B', 'D', 'F']] >>> transpose([]) [] Here's a pictoral view of the first example:: A B C => A D D E F B E C F This can be used to turn an HTML table into a group of div columns. An HTML table is row major: it consists of several rows, each containing several cells. But a
    layout consists of only one row, each containing an entire subarray. The
    s have style "float:left", which makes them appear horizontally. The items within each
    are placed in their own
    's or separated by
    , which makes them appear vertically. The point is that an HTML table is row major (``array[0]`` is the first row), while a group of div columns is column major (``array[0]`` is the first column). ``transpose()`` can be used to switch between the two. """ if not array: return [] ret = [] for c in range(len(array[0])): col = [row[c] for row in array] ret.append(col) return ret if __name__ == "__main__": import doctest doctest.testmod() WebHelpers-1.3/webhelpers/mimehelper.py0000664000175000017500000001131711471126331017644 0ustar sluggosluggo"""MIME Type helpers This helper depends on the WebOb package, and has optional Pylons support. """ import mimetypes class MIMETypes(object): """MIMETypes registration mapping The MIMETypes object class provides a single point to hold onto all the registered mimetypes, and their association extensions. It's used by the mimetypes method to determine the appropriate content type to return to a client. """ aliases = {} def init(cls): """Loads a default mapping of extensions and mimetypes These are suitable for most web applications by default. Additional types can be added by using the mimetypes module. """ mimetypes.init() init = classmethod(init) def add_alias(cls, alias, mimetype): """Create a MIMEType alias to a full mimetype. Examples: - ``add_alias('html', 'text/html')`` - ``add_alias('xml', 'application/xml')`` An ``alias`` may not contain the ``/`` character. """ if '/' in alias: raise ValueError("MIMEType aliases may not contain '/'") cls.aliases[alias] = mimetype add_alias = classmethod(add_alias) def __init__(self, environ): """``environ`` is the WSGI environment of the current request.""" self.env = environ def _set_response_content_type(self, mimetype): if 'pylons.pylons' in self.env: self.env['pylons.pylons'].response.content_type = mimetype return mimetype def mimetype(self, content_type): """Check the PATH_INFO of the current request and client's HTTP Accept to attempt to use the appropriate mime-type. If a content-type is matched, return the appropriate response content type, and if running under Pylons, set the response content type directly. If a content-type is not matched, return ``False``. This works best with URLs that end in extensions that differentiate content-type. Examples: ``http://example.com/example``, ``http://example.com/example.xml``, ``http://example.com/example.csv`` Since browsers generally allow for any content-type, but should be sent HTML when possible, the html mimetype check should always come first, as shown in the example below. Example:: # some code likely in environment.py MIMETypes.init() MIMETypes.add_alias('html', 'text/html') MIMETypes.add_alias('xml', 'application/xml') MIMETypes.add_alias('csv', 'text/csv') # code in a Pylons controller def someaction(self): # prepare a bunch of data # ...... # prepare MIMETypes object m = MIMETypes(request.environ) if m.mimetype('html'): return render('/some/template.html') elif m.mimetype('atom'): return render('/some/xml_template.xml') elif m.mimetype('csv'): # write the data to a csv file return csvfile else: abort(404) # Code in a non-Pylons controller. m = MIMETypes(environ) response_type = m.mimetype('html') # ``response_type`` is a MIME type or ``False``. """ import webob if content_type in MIMETypes.aliases: content_type = MIMETypes.aliases[content_type] path = self.env['PATH_INFO'] guess_from_url = mimetypes.guess_type(path)[0] possible_from_accept_header = None has_extension = False if len(path.split('/')) > 1: last_part = path.split('/')[-1] if '.' in last_part: has_extension = True if 'HTTP_ACCEPT' in self.env: possible_from_accept_header = webob.acceptparse.MIMEAccept('ACCEPT', self.env['HTTP_ACCEPT']) if has_extension == False: if possible_from_accept_header is None: return self._set_response_content_type(content_type) elif content_type in possible_from_accept_header: return self._set_response_content_type(content_type) else: return False if content_type == guess_from_url: # Guessed same mimetype return self._set_response_content_type(content_type) elif guess_from_url is None and content_type in possible_from_accept_header: return self._set_response_content_type(content_type) else: return False WebHelpers-1.3/webhelpers/util.py0000664000175000017500000002510211445174513016475 0ustar sluggosluggo"""Utility functions used by various web helpers. This module contains support functions used by other helpers, and functions for URL manipulation. Most of these helpers predate the 0.6 reorganization; they would have been put in other subpackages if they have been created later. """ import cgi import copy import sys import urllib import urlparse from UserDict import DictMixin from xml.sax.saxutils import XMLGenerator try: from urlparse import parse_qs except ImportError: # Python < 2.6 from cgi import parse_qs def update_params(_url, _debug=False, **params): """Update query parameters in a URL. ``_url`` is any URL, with or without a query string. ``\*\*params`` are query parameters to add or replace. Each value may be a string, a list of strings, or None. Passing a list generates multiple values for the same parameter. Passing None deletes the corresponding parameter if present. Return the new URL. *Debug mode:* if a pseudo-parameter ``_debug=True`` is passed, return a tuple: ``[0]`` is the URL without query string or fragment, ``[1]`` is the final query parameters as a dict, and ``[2]`` is the fragment part of the original URL or the empty string. Usage: >>> update_params("foo", new1="NEW1") 'foo?new1=NEW1' >>> update_params("foo?p=1", p="2") 'foo?p=2' >>> update_params("foo?p=1", p=None) 'foo' >>> update_params("http://example.com/foo?new1=OLD1#myfrag", new1="NEW1") 'http://example.com/foo?new1=NEW1#myfrag' >>> update_params("http://example.com/foo?new1=OLD1#myfrag", new1="NEW1", _debug=True) ('http://example.com/foo', {'new1': 'NEW1'}, 'myfrag') >>> update_params("http://www.mau.de?foo=2", brrr=3) 'http://www.mau.de?foo=2&brrr=3' >>> update_params("http://www.mau.de?foo=A&foo=B", foo=["C", "D"]) 'http://www.mau.de?foo=C&foo=D' """ url, fragment = urlparse.urldefrag(_url) if "?" in url: url, qs = url.split("?", 1) query = parse_qs(qs) else: query = {} for key, value in params.iteritems(): if value is not None: query[key] = value elif key in query: del query[key] if _debug: return url, query, fragment qs = urllib.urlencode(query, True) if qs: qs = "?" + qs if fragment: fragment = "#" + fragment return "%s%s%s" % (url, qs, fragment) def cgi_escape(s, quote=False): """Replace special characters '&', '<' and '>' by SGML entities. This is a slightly more efficient version of the cgi.escape by using 'in' membership to test if the replace is needed. This function returns a plain string. Programs using the HTML builder should call ``webhelpers.html.builder.escape()`` instead of this to prevent double-escaping. Changed in WebHelpers 1.2: escape single-quote as well as double-quote. """ if '&' in s: s = s.replace("&", "&") # Must be done first! if '<' in s: s = s.replace("<", "<") if '>' in s: s = s.replace(">", ">") if quote: s = s.replace('"', """) s = s.replace("'", "'") return s def html_escape(s): """HTML-escape a string or object. This converts any non-string objects passed into it to strings (actually, using ``unicode()``). All values returned are non-unicode strings (using ``&#num;`` entities for all non-ASCII characters). None is treated specially, and returns the empty string. This function returns a plain string. Programs using the HTML builder should wrap the result in ``literal()`` to prevent double-escaping. """ if s is None: return '' if not isinstance(s, basestring): if hasattr(s, '__unicode__'): s = unicode(s) else: s = str(s) s = cgi_escape(s, True) if isinstance(s, unicode): s = s.encode('ascii', 'xmlcharrefreplace') return s def iri_to_uri(iri): """ Convert an IRI portion to a URI portion suitable for inclusion in a URL. (An IRI is an Internationalized Resource Identifier.) This is the algorithm from section 3.1 of RFC 3987. However, since we are assuming input is either UTF-8 or unicode already, we can simplify things a little from the full method. Returns an ASCII string containing the encoded result. """ # Called by webhelpers.feedgenerator # # The list of safe characters here is constructed from the printable ASCII # characters that are not explicitly excluded by the list at the end of # section 3.1 of RFC 3987. if iri is None: return iri return urllib.quote(iri, safe='/#%[]=:;$&()+,!?') class Partial(object): """ A partial function object. Equivalent to functools.partial, which was introduced in Python 2.5. """ def __init__(*args, **kw): self = args[0] self.fn, self.args, self.kw = (args[1], args[2:], kw) def __call__(self, *args, **kw): if kw and self.kw: d = self.kw.copy() d.update(kw) else: d = kw or self.kw return self.fn(*(self.args + args), **d) class SimplerXMLGenerator(XMLGenerator): """A subclass of Python's SAX XMLGenerator.""" # Used by webhelpers.feedgenerator def addQuickElement(self, name, contents=None, attrs=None): """Add an element with no children.""" if attrs is None: attrs = {} self.startElement(name, attrs) if contents is not None: self.characters(contents) self.endElement(name) class UnicodeMultiDict(DictMixin): """ A MultiDict wrapper that decodes returned values to unicode on the fly. Decoding is not applied to assigned values. The key/value contents are assumed to be ``str``/``strs`` or ``str``/``FieldStorages`` (as is returned by the :func:`paste.request.parse` functions). Can optionally also decode keys when the ``decode_keys`` argument is True. ``FieldStorage`` instances are cloned, and the clone's ``filename`` variable is decoded. Its ``name`` variable is decoded when ``decode_keys`` is enabled. """ def __init__(self, multi=None, encoding=None, errors='strict', decode_keys=False): self.multi = multi if encoding is None: encoding = sys.getdefaultencoding() self.encoding = encoding self.errors = errors self.decode_keys = decode_keys def _decode_key(self, key): if self.decode_keys: key = key.decode(self.encoding, self.errors) return key def _decode_value(self, value): """ Decode the specified (``str`` or `FieldStorage``) value to unicode. ``FieldStorage`` objects are specially handled. """ if isinstance(value, cgi.FieldStorage): # decode FieldStorage's field name and filename value = copy.copy(value) if self.decode_keys: value.name = value.name.decode(self.encoding, self.errors) value.filename = value.filename.decode(self.encoding, self.errors) else: try: value = value.decode(self.encoding, self.errors) except AttributeError: pass return value def __getitem__(self, key): return self._decode_value(self.multi.__getitem__(key)) def __setitem__(self, key, value): self.multi.__setitem__(key, value) def add(self, key, value): """Add the key and value, not overwriting any previous value.""" self.multi.add(key, value) def getall(self, key): """Return list of all values matching the key (may be an empty list).""" return [self._decode_value(v) for v in self.multi.getall(key)] def getone(self, key): """Return one value matching key. Raise KeyError if multiple matches.""" return self._decode_value(self.multi.getone(key)) def mixed(self): """Return dict where values are single values or a list of values. The value is a single value if key appears just once. It is a list of values when a key/value appears more than once in this dictionary. This is similar to the kind of dictionary often used to represent the variables in a web request. """ unicode_mixed = {} for key, value in self.multi.mixed().iteritems(): if isinstance(value, list): value = [self._decode_value(value) for value in value] else: value = self._decode_value(value) unicode_mixed[self._decode_key(key)] = value return unicode_mixed def dict_of_lists(self): """Return dict where each key is associated with a list of values.""" unicode_dict = {} for key, value in self.multi.dict_of_lists().iteritems(): value = [self._decode_value(value) for value in value] unicode_dict[self._decode_key(key)] = value return unicode_dict def __delitem__(self, key): self.multi.__delitem__(key) def __contains__(self, key): return self.multi.__contains__(key) has_key = __contains__ def clear(self): self.multi.clear() def copy(self): return UnicodeMultiDict(self.multi.copy(), self.encoding, self.errors) def setdefault(self, key, default=None): return self._decode_value(self.multi.setdefault(key, default)) def pop(self, key, *args): return self._decode_value(self.multi.pop(key, *args)) def popitem(self): k, v = self.multi.popitem() return (self._decode_key(k), self._decode_value(v)) def __repr__(self): items = ', '.join(['(%r, %r)' % v for v in self.items()]) return '%s([%s])' % (self.__class__.__name__, items) def __len__(self): return self.multi.__len__() ## ## All the iteration: ## def keys(self): return [self._decode_key(k) for k in self.multi.iterkeys()] def iterkeys(self): for k in self.multi.iterkeys(): yield self._decode_key(k) __iter__ = iterkeys def items(self): return [(self._decode_key(k), self._decode_value(v)) for \ k, v in self.multi.iteritems()] def iteritems(self): for k, v in self.multi.iteritems(): yield (self._decode_key(k), self._decode_value(v)) def values(self): return [self._decode_value(v) for v in self.multi.itervalues()] def itervalues(self): for v in self.multi.itervalues(): yield self._decode_value(v) WebHelpers-1.3/webhelpers/paginate.py0000664000175000017500000010761311542303225017310 0ustar sluggosluggo""" paginate: a module to help split up lists or results from ORM queries ======================================================================= What is pagination? --------------------- This module helps dividing large lists of items into pages. The user is shown one page at a time and can navigate to other pages. Imagine you are offering a company phonebook and let the user search the entries. If the search result contains 23 entries but you may want to display no more than 10 entries at once. The first page contains entries 1-10, the second 11-20 and the third 21-23. See the documentation of the "Page" class for more information. How do I use it? ------------------ One page of items is represented by the *Page* object. A *Page* gets initialized with at least two arguments and usually three: - The collection of items to pick a range from. - The page number we want to display. (Default is 1: the first page.) - A URL generator callback. (This tells what the URLs to other pages are. It's required if using the ``pager()`` method, although it may be omitted under Pylons for backward compatibility. It is required for Pyramid.) Here's an interactive example. First we'll create a URL generator using the basic ``PageURL`` class, which works with all frameworks and has no dependencies. It creates URLs by overriding the 'page' query parameter. :: # Instantiate the URL generator, and call it to see what it does. >>> url_for_page = PageURL("/articles/2013", {"page": "3"}) >>> url_for_page(page=2) '/articles/2013?page=2' Now we can create a collection and instantiate the Page:: # Create a sample collection of 1000 items >>> my_collection = range(1000) # Create a Page object for the 3rd page (20 items per page is the default) >>> my_page = Page(my_collection, page=3, url=url_for_page) # The page object can be printed directly to get its details >>> my_page Page: Collection type: (Current) page: 3 First item: 41 Last item: 60 First page: 1 Last page: 50 Previous page: 2 Next page: 4 Items per page: 20 Number of items: 1000 Number of pages: 50 # Print a list of items on the current page >>> my_page.items [40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59] # The *Page* object can be used as an iterator: >>> for my_item in my_page: print my_item, 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 # The .pager() method returns an HTML fragment with links to surrounding # pages. # [The ">>" prompt is to hide untestable examples from doctest.] >> my_page.pager() 1 2 [3] 4 5 .. 50 (this is actually HTML) # The pager can be customized: >> my_page.pager('$link_previous ~3~ $link_next (Page $page of $page_count)') 1 2 [3] 4 5 6 .. 50 > (Page 3 of 50) There are many parameters that customize the Page's behavor. See the documentation on ``Page`` and ``Page.pager()``. URL generator ------------- The constructor's ``url`` argument is a callback that returns URLs to other pages. It's required when using the ``Page.pager()`` method except under Pylons, where it will fall back to ``pylons.url.current`` (Pylons 1) and then ``routes.url_for`` (Pylons 0.9.7). If none of these are available, you'll get an exception "NotImplementedError: no URL generator available". WebHelpers 1.3 introduces a few URL generators for convenience. **PageURL** is described above. **PageURL_WebOb** takes a ``webobb.Request`` object, and is suitable for Pyramid, Pylons, TurboGears, and other frameworks that have a WebOb-compatible Request object. Both of these classes assume that the page number is in the 'page' query parameter. Here's an example for Pyramid and other WebOb-compatible frameworks:: # Assume ``request`` is the current request. import webhelpers.paginate as paginate current_page = int(request.params["page"]) q = SOME_SQLALCHEMY_QUERY page_url = paginate.PageURL_WebOb(request) records = paginate.Page(q, current_page, url=page_url) If the page number is in the URL path, you'll have to use a framework-specific URL generator. For instance, in Pyramid if the current route is "/articles/{id}/page/{page}" and the current URL is "/articles/ABC/page/3?print=1", you can use Pyramid's "current_route_url" function as follows:: # Assume ``request`` is the current request. import webhelpers.paginate as paginate from pyramid.url import current_route_url def page_url(page): return current_route_url(request, page=page, _query=request.GET) q = SOME_SQLALCHEMY_QUERY current_page = int(request.matchdict["page"]) records = Page(q, current_page, url=page_url) This overrides the 'page' path variable, while leaving the 'id' variable and the query string intact. The callback API is simple. 1. It must accept an integer argument 'page', which will be passed by name. 2. It should return the URL for that page. 3. If you're using AJAX 'partial' functionality described in the ``Page.pager`` docstring, the callback should also accept a 'partial' argument and, if true, set a query parameter 'partial=1'. 4. If you use the 'page_param' or 'partial_param' argument to ``Page.pager``, the 'page' and 'partial' arguments will be renamed to whatever you specify. In this case, the callback would also have to expect these other argument names. The supplied classes adhere to this API in their ``.__call__`` method, all except the fourth condition. So you can use their instances as callbacks as long as you don't use 'page_param' or 'partial_param'. For convenience in writing callbacks that update the 'page' query parameter, a ``make_page_url`` function is available that assembles the pieces into a complete URL. Other callbacks may find ``webhelpers.utl.update_params`` useful, which overrides query parameters on a more general basis. Can I use AJAX / AJAH? ------------------------ Yes. See *partial_param* and *onclick* in ``Page.pager()``. Notes ------- Page numbers and item numbers start at 1. This concept has been used because users expect that the first page has number 1 and the first item on a page also has number 1. So if you want to use the page's items by their index number please note that you have to subtract 1. This module is the successor to the obsolete ``webhelpers.pagination`` module. It is **NOT** API compatible. This module is based on the code from http://workaround.org/cgi-bin/hg-paginate that is known at the "Paginate" module on PyPI. It was written by Christoph Haas , and modified by Christoph Haas and Mike Orr for WebHelpers. (c) 2007-2011. """ import re from string import Template import urllib import warnings from webhelpers.html import literal, HTML INCOMPATIBLE_COLLECTION_TYPE = """\ Sorry, your collection type is not supported by the paginate module. You can provide a list, a tuple, a SQLAlchemy " "select object or a SQLAlchemy ORM-query object.""" # import SQLAlchemy if available try: import sqlalchemy import sqlalchemy.orm # Some users report errors if this is not imported. except: sqlalchemy_available = False sqlalchemy_version = None else: sqlalchemy_available = True sqlalchemy_version = sqlalchemy.__version__ def get_wrapper(obj, sqlalchemy_session=None): """ Auto-detect the kind of object and return a list/tuple to access items from the collection. """ # If the collection is a sequence we can use it directly if isinstance(obj, (list, tuple)): return obj # Is SQLAlchemy 0.4 or better available? (0.3 is not supported - sorry) # Note: SQLAlchemy objects aren't sliceable, so this has to be before # the next if-stanza if sqlalchemy_available and sqlalchemy_version[:3] != '0.3': # Is the collection a query? if isinstance(obj, sqlalchemy.orm.query.Query): return _SQLAlchemyQuery(obj) # Is the collection an SQLAlchemy select object? if isinstance(obj, sqlalchemy.sql.expression.CompoundSelect) \ or isinstance(obj, sqlalchemy.sql.expression.Select): return _SQLAlchemySelect(obj, sqlalchemy_session) # If object is iterable we can use it. (This is not true if it's # non-sliceable but there doesn't appear to be a way to test for that. We'd # have to call .__getitem__ with a slice and guess what the exception # means, and calling it may cause side effects.) required_methods = ["__iter__", "__len__", "__getitem__"] for meth in required_methods: if not hasattr(obj, meth): break else: return obj raise TypeError(INCOMPATIBLE_COLLECTION_TYPE) class _SQLAlchemySelect(object): """ Iterable that allows to get slices from an SQLAlchemy Select object """ def __init__(self, obj, sqlalchemy_session=None): session_types = ( sqlalchemy.orm.scoping.ScopedSession, sqlalchemy.orm.Session) if not isinstance(sqlalchemy_session, session_types): raise TypeError("If you want to page an SQLAlchemy 'select' object then you " "have to provide a 'sqlalchemy_session' argument. See also: " "http://www.sqlalchemy.org/docs/04/session.html") self.sqlalchemy_session = sqlalchemy_session self.obj = obj def __getitem__(self, range): if not isinstance(range, slice): raise Exception, "__getitem__ without slicing not supported" offset = range.start limit = range.stop - range.start select = self.obj.offset(offset).limit(limit) return self.sqlalchemy_session.execute(select).fetchall() def __len__(self): return self.sqlalchemy_session.execute(self.obj).rowcount class _SQLAlchemyQuery(object): """ Iterable that allows to get slices from an SQLAlchemy Query object """ def __init__(self, obj): self.obj = obj def __getitem__(self, range): if not isinstance(range, slice): raise Exception, "__getitem__ without slicing not supported" return self.obj[range] def __len__(self): return self.obj.count() # Since the items on a page are mainly a list we subclass the "list" type class Page(list): """A list/iterator of items representing one page in a larger collection. An instance of the "Page" class is created from a collection of things. The instance works as an iterator running from the first item to the last item on the given page. The collection can be: - a sequence - an SQLAlchemy query - e.g.: Session.query(MyModel) - an SQLAlchemy select - e.g.: sqlalchemy.select([my_table]) A "Page" instance maintains pagination logic associated with each page, where it begins, what the first/last item on the page is, etc. The pager() method creates a link list allowing the user to go to other pages. **WARNING:** Unless you pass in an item_count, a count will be performed on the collection every time a Page instance is created. If using an ORM, it's advised to pass in the number of items in the collection if that number is known. Instance attributes: original_collection Points to the collection object being paged through item_count Number of items in the collection page Number of the current page items_per_page Maximal number of items displayed on a page first_page Number of the first page - starts with 1 last_page Number of the last page page_count Number of pages items Sequence/iterator of items on the current page first_item Index of first item on the current page - starts with 1 last_item Index of last item on the current page """ def __init__(self, collection, page=1, items_per_page=20, item_count=None, sqlalchemy_session=None, presliced_list=False, url=None, **kwargs): """Create a "Page" instance. Parameters: collection Sequence, SQLAlchemy select object or SQLAlchemy ORM-query representing the collection of items to page through. page The requested page number - starts with 1. Default: 1. items_per_page The maximal number of items to be displayed per page. Default: 20. item_count (optional) The total number of items in the collection - if known. If this parameter is not given then the paginator will count the number of elements in the collection every time a "Page" is created. Giving this parameter will speed up things. presliced_list (optional) Indicates whether the collection, when a list, has already been sliced for the current viewing page, and thus should *not* be sliced again. sqlalchemy_session (optional) If you want to use an SQLAlchemy (0.4) select object as a collection then you need to provide an SQLAlchemy session object. Select objects do not have a database connection attached so it would not be able to execute the SELECT query. url (optional) A URL generator function. See module docstring for details. This is used only by ``.pager()``. Further keyword arguments are used as link arguments in the pager(). """ self._url_generator = url # 'page_nr' is deprecated. if 'page_nr' in kwargs: warnings.warn("'page_nr' is deprecated. Please use 'page' instead.") page = kwargs['page_nr'] del kwargs['page_nr'] # 'current_page' is also deprecated. if 'current_page' in kwargs: warnings.warn("'current_page' is deprecated. Please use 'page' instead.") page = kwargs['current_page'] del kwargs['current_page'] # Safe the kwargs class-wide so they can be used in the pager() method self.kwargs = kwargs # Save a reference to the collection self.original_collection = collection # Decorate the ORM/sequence object with __getitem__ and __len__ # functions to be able to get slices. if collection is not None: # Determine the type of collection and use a wrapper for ORMs self.collection = get_wrapper(collection, sqlalchemy_session) else: self.collection = [] # The self.page is the number of the current page. # The first page has the number 1! try: self.page = int(page) # make it int() if we get it as a string except (ValueError, TypeError): self.page = 1 self.items_per_page = items_per_page # Unless the user tells us how many items the collections has # we calculate that ourselves. if item_count is not None: self.item_count = item_count else: self.item_count = len(self.collection) # Compute the number of the first and last available page if self.item_count > 0: self.first_page = 1 self.page_count = ((self.item_count - 1) / self.items_per_page) + 1 self.last_page = self.first_page + self.page_count - 1 # Make sure that the requested page number is the range of valid pages if self.page > self.last_page: self.page = self.last_page elif self.page < self.first_page: self.page = self.first_page # Note: the number of items on this page can be less than # items_per_page if the last page is not full self.first_item = (self.page - 1) * items_per_page + 1 self.last_item = min(self.first_item + items_per_page - 1, self.item_count) # We subclassed "list" so we need to call its init() method # and fill the new list with the items to be displayed on the page. # We use list() so that the items on the current page are retrieved # only once. Otherwise it would run the actual SQL query everytime # .items would be accessed. if presliced_list: self.items = self.collection else: try: first = self.first_item - 1 last = self.last_item self.items = list(self.collection[first:last]) except TypeError, e: if str(e) == "unhashable type": # Assume this means collection is unsliceable. raise TypeError(INCOMPATIBLE_COLLECTION_TYPE) raise # Links to previous and next page if self.page > self.first_page: self.previous_page = self.page-1 else: self.previous_page = None if self.page < self.last_page: self.next_page = self.page+1 else: self.next_page = None # No items available else: self.first_page = None self.page_count = 0 self.last_page = None self.first_item = None self.last_item = None self.previous_page = None self.next_page = None self.items = [] # This is a subclass of the 'list' type. Initialise the list now. list.__init__(self, self.items) def __repr__(self): return ("Page:\n" "Collection type: %(type)s\n" "(Current) page: %(page)s\n" "First item: %(first_item)s\n" "Last item: %(last_item)s\n" "First page: %(first_page)s\n" "Last page: %(last_page)s\n" "Previous page: %(previous_page)s\n" "Next page: %(next_page)s\n" "Items per page: %(items_per_page)s\n" "Number of items: %(item_count)s\n" "Number of pages: %(page_count)s\n" % { 'type':type(self.collection), 'page':self.page, 'first_item':self.first_item, 'last_item':self.last_item, 'first_page':self.first_page, 'last_page':self.last_page, 'previous_page':self.previous_page, 'next_page':self.next_page, 'items_per_page':self.items_per_page, 'item_count':self.item_count, 'page_count':self.page_count, }) def pager(self, format='~2~', page_param='page', partial_param='partial', show_if_single_page=False, separator=' ', onclick=None, symbol_first='<<', symbol_last='>>', symbol_previous='<', symbol_next='>', link_attr={'class':'pager_link'}, curpage_attr={'class':'pager_curpage'}, dotdot_attr={'class':'pager_dotdot'}, **kwargs): """ Return string with links to other pages (e.g. "1 2 [3] 4 5 6 7"). format: Format string that defines how the pager is rendered. The string can contain the following $-tokens that are substituted by the string.Template module: - $first_page: number of first reachable page - $last_page: number of last reachable page - $page: number of currently selected page - $page_count: number of reachable pages - $items_per_page: maximal number of items per page - $first_item: index of first item on the current page - $last_item: index of last item on the current page - $item_count: total number of items - $link_first: link to first page (unless this is first page) - $link_last: link to last page (unless this is last page) - $link_previous: link to previous page (unless this is first page) - $link_next: link to next page (unless this is last page) To render a range of pages the token '~3~' can be used. The number sets the radius of pages around the current page. Example for a range with radius 3: '1 .. 5 6 7 [8] 9 10 11 .. 500' Default: '~2~' symbol_first String to be displayed as the text for the %(link_first)s link above. Default: '<<' symbol_last String to be displayed as the text for the %(link_last)s link above. Default: '>>' symbol_previous String to be displayed as the text for the %(link_previous)s link above. Default: '<' symbol_next String to be displayed as the text for the %(link_next)s link above. Default: '>' separator: String that is used to separate page links/numbers in the above range of pages. Default: ' ' page_param: The name of the parameter that will carry the number of the page the user just clicked on. The parameter will be passed to a url_for() call so if you stay with the default ':controller/:action/:id' routing and set page_param='id' then the :id part of the URL will be changed. If you set page_param='page' then url_for() will make it an extra parameters like ':controller/:action/:id?page=1'. You need the page_param in your action to determine the page number the user wants to see. If you do not specify anything else the default will be a parameter called 'page'. Note: If you set this argument and are using a URL generator callback, the callback must accept this name as an argument instead of 'page'. callback, becaust the callback requires its argument to be 'page'. Instead the callback itself can return any URL necessary. partial_param: When using AJAX/AJAH to do partial updates of the page area the application has to know whether a partial update (only the area to be replaced) or a full update (reloading the whole page) is required. So this parameter is the name of the URL parameter that gets set to 1 if the 'onclick' parameter is used. So if the user requests a new page through a Javascript action (onclick) then this parameter gets set and the application is supposed to return a partial content. And without Javascript this parameter is not set. The application thus has to check for the existence of this parameter to determine whether only a partial or a full page needs to be returned. See also the examples in this modules docstring. Default: 'partial' Note: If you set this argument and are using a URL generator callback, the callback must accept this name as an argument instead of 'partial'. show_if_single_page: if True the navigator will be shown even if there is only one page Default: False link_attr (optional) A dictionary of attributes that get added to A-HREF links pointing to other pages. Can be used to define a CSS style or class to customize the look of links. Example: { 'style':'border: 1px solid green' } Default: { 'class':'pager_link' } curpage_attr (optional) A dictionary of attributes that get added to the current page number in the pager (which is obviously not a link). If this dictionary is not empty then the elements will be wrapped in a SPAN tag with the given attributes. Example: { 'style':'border: 3px solid blue' } Default: { 'class':'pager_curpage' } dotdot_attr (optional) A dictionary of attributes that get added to the '..' string in the pager (which is obviously not a link). If this dictionary is not empty then the elements will be wrapped in a SPAN tag with the given attributes. Example: { 'style':'color: #808080' } Default: { 'class':'pager_dotdot' } onclick (optional) This paramter is a string containing optional Javascript code that will be used as the 'onclick' action of each pager link. It can be used to enhance your pager with AJAX actions loading another page into a DOM object. In this string the variable '$partial_url' will be replaced by the URL linking to the desired page with an added 'partial=1' parameter (or whatever you set 'partial_param' to). In addition the '$page' variable gets replaced by the respective page number. Note that the URL to the destination page contains a 'partial_param' parameter so that you can distinguish between AJAX requests (just refreshing the paginated area of your page) and full requests (loading the whole new page). [Backward compatibility: you can use '%s' instead of '$partial_url'] jQuery example: "$('#my-page-area').load('$partial_url'); return false;" Yahoo UI example: "YAHOO.util.Connect.asyncRequest('GET','$partial_url',{ success:function(o){YAHOO.util.Dom.get('#my-page-area').innerHTML=o.responseText;} },null); return false;" scriptaculous example: "new Ajax.Updater('#my-page-area', '$partial_url', {asynchronous:true, evalScripts:true}); return false;" ExtJS example: "Ext.get('#my-page-area').load({url:'$partial_url'}); return false;" Custom example: "my_load_page($page)" Additional keyword arguments are used as arguments in the links. Otherwise the link will be created with url_for() which points to the page you are currently displaying. """ self.curpage_attr = curpage_attr self.separator = separator self.pager_kwargs = kwargs self.page_param = page_param self.partial_param = partial_param self.onclick = onclick self.link_attr = link_attr self.dotdot_attr = dotdot_attr # Don't show navigator if there is no more than one page if self.page_count == 0 or (self.page_count == 1 and not show_if_single_page): return '' # Replace ~...~ in token format by range of pages result = re.sub(r'~(\d+)~', self._range, format) # Interpolate '%' variables result = Template(result).safe_substitute({ 'first_page': self.first_page, 'last_page': self.last_page, 'page': self.page, 'page_count': self.page_count, 'items_per_page': self.items_per_page, 'first_item': self.first_item, 'last_item': self.last_item, 'item_count': self.item_count, 'link_first': self.page>self.first_page and \ self._pagerlink(self.first_page, symbol_first) or '', 'link_last': self.page leftmost_page = 5 # -> rightmost_page = 9 leftmost_page = max(self.first_page, (self.page-radius)) rightmost_page = min(self.last_page, (self.page+radius)) nav_items = [] # Create a link to the first page (unless we are on the first page # or there would be no need to insert '..' spacers) if self.page != self.first_page and self.first_page < leftmost_page: nav_items.append( self._pagerlink(self.first_page, self.first_page) ) # Insert dots if there are pages between the first page # and the currently displayed page range if leftmost_page - self.first_page > 1: # Wrap in a SPAN tag if nolink_attr is set text = '..' if self.dotdot_attr: text = HTML.span(c=text, **self.dotdot_attr) nav_items.append(text) for thispage in xrange(leftmost_page, rightmost_page+1): # Hilight the current page number and do not use a link if thispage == self.page: text = '%s' % (thispage,) # Wrap in a SPAN tag if nolink_attr is set if self.curpage_attr: text = HTML.span(c=text, **self.curpage_attr) nav_items.append(text) # Otherwise create just a link to that page else: text = '%s' % (thispage,) nav_items.append( self._pagerlink(thispage, text) ) # Insert dots if there are pages between the displayed # page numbers and the end of the page range if self.last_page - rightmost_page > 1: text = '..' # Wrap in a SPAN tag if nolink_attr is set if self.dotdot_attr: text = HTML.span(c=text, **self.dotdot_attr) nav_items.append(text) # Create a link to the very last page (unless we are on the last # page or there would be no need to insert '..' spacers) if self.page != self.last_page and rightmost_page < self.last_page: nav_items.append( self._pagerlink(self.last_page, self.last_page) ) return self.separator.join(nav_items) def _pagerlink(self, page, text): """ Create a URL that links to another page using url_for(). Parameters: page Number of the page that the link points to text Text to be printed in the A-HREF tag """ # Let the url_for() from webhelpers create a new link and set # the variable called 'page_param'. Example: # You are in '/foo/bar' (controller='foo', action='bar') # and you want to add a parameter 'page'. Then you # call the navigator method with page_param='page' and # the url_for() call will create a link '/foo/bar?page=...' # with the respective page number added. link_params = {} # Use the instance kwargs from Page.__init__ as URL parameters link_params.update(self.kwargs) # Add keyword arguments from pager() to the link as parameters link_params.update(self.pager_kwargs) link_params[self.page_param] = page # Get the URL generator if self._url_generator is not None: url_generator = self._url_generator else: try: import pylons url_generator = pylons.url.current except (ImportError, AttributeError): try: import routes url_generator = routes.url_for config = routes.request_config() except (ImportError, AttributeError): raise NotImplementedError("no URL generator available") else: # if the Mapper is configured with explicit=True we have to fetch # the controller and action manually if config.mapper.explicit: if hasattr(config, 'mapper_dict'): for k, v in config.mapper_dict.items(): if k != self.page_param: link_params[k] = v # Create the URL to load a certain page link_url = url_generator(**link_params) if self.onclick: # create link with onclick action for AJAX # Create the URL to load the page area part of a certain page (AJAX # updates) link_params[self.partial_param] = 1 partial_url = url_generator(**link_params) try: # if '%s' is used in the 'onclick' parameter (backwards compatibility) onclick_action = self.onclick % (partial_url,) except TypeError: onclick_action = Template(self.onclick).safe_substitute({ "partial_url": partial_url, "page": page }) return HTML.a(text, href=link_url, onclick=onclick_action, **self.link_attr) else: # return static link return HTML.a(text, href=link_url, **self.link_attr) #### URL GENERATOR CLASSES def make_page_url(path, params, page, partial=False, sort=True): """A helper function for URL generators. I assemble a URL from its parts. I assume that a link to a certain page is done by overriding the 'page' query parameter. ``path`` is the current URL path, with or without a "scheme://host" prefix. ``params`` is the current query parameters as a dict or dict-like object. ``page`` is the target page number. If ``partial`` is true, set query param 'partial=1'. This is to for AJAX calls requesting a partial page. If ``sort`` is true (default), the parameters will be sorted. Otherwise they'll be in whatever order the dict iterates them. """ params = params.copy() params["page"] = page if partial: params["partial"] = "1" if sort: params = params.items() params.sort() qs = urllib.urlencode(params, True) return "%s?%s" % (path, qs) class PageURL(object): """A simple page URL generator for any framework.""" def __init__(self, path, params): """ ``path`` is the current URL path, with or without a "scheme://host" prefix. ``params`` is the current URL's query parameters as a dict or dict-like object. """ self.path = path self.params = params def __call__(self, page, partial=False): """Generate a URL for the specified page.""" return make_page_url(self.path, self.params, page, partial) class PageURL_WebOb(object): """A page URL generator for WebOb-compatible Request objects. I derive new URLs based on the current URL but overriding the 'page' query parameter. I'm suitable for Pyramid, Pylons, and TurboGears, as well as any other framework whose Request object has 'application_url', 'path', and 'GET' attributes that behave the same way as ``webob.Request``'s. """ def __init__(self, request, qualified=False): """ ``request`` is a WebOb-compatible ``Request`` object. If ``qualified`` is false (default), generated URLs will have just the path and query string. If true, the "scheme://host" prefix will be included. The default is false to match traditional usage, and to avoid generating unuseable URLs behind reverse proxies (e.g., Apache's mod_proxy). """ self.request = request self.qualified = qualified def __call__(self, page, partial=False): """Generate a URL for the specified page.""" if self.qualified: path = self.request.application_url else: path = self.request.path return make_page_url(path, self.request.GET, page, partial) WebHelpers-1.3/webhelpers/media.py0000664000175000017500000001054211373653355016607 0ustar sluggosluggo"""Multimedia helpers for images, etc.""" import logging import os import struct import sys __all__ = ["choose_height", "get_dimensions_pil", "get_dimensions"] def choose_height(new_width, width, height): """Return the height corresponding to ``new_width`` that's proportional to the original size (``width`` x ``height``). """ proportion = float(height) / float(width) return int(new_width * proportion) def get_dimensions_pil(path, default=(None, None)): """Get an image's size using the Python Imaging Library (PIL). ``path`` is the path of the image file. ``default`` is returned if the size could not be ascertained. This usually means the file does not exist or is not in a format recognized by PIL. The normal return value is a tuple: ``(width, height)``. Depends on the `Python Imaging Library `_. If your application is not otherwise using PIL, see the ``get_dimensions()`` function, which does not have external dependencies. """ import Image try: im = Image.open(path) except Exception: return default return im.size def get_dimensions(path, default=(None, None)): """Get an image's size using only the Python standard library. ``path`` is the path of the image file. ``default`` is returned if the size could not be ascertained. This usually means the file does not exist or is not in a recognized format. PIL. Only JPG, PNG, GIF, and BMP are supported at this time. The normal return value is a tuple: ``(width, height)``. The algorithms are based on a `PyCode recipe `_ by Perenzo/Welch/Ray. This helper recognizes fewer image formats and is potentially less accurate than ``get_dimensions_pil()``. Running this module as a script tests this helper. It will print the size of each image file specified on the command line. """ apath = os.path.abspath(path) try: f = open(path, "rb") except IOError: return default try: header = f.read(1024) # JPG if header.startswith("\xFF\xD8"): width = height = None f.seek(2) while True: length = 4 buf = f.read(length) try: marker, code, length = struct.unpack("!ccH", buf) except Exception: break if marker != "\xff": break if 0xc0 <= ord(code) <= 0xc3: length = 5 buf = f.read(length) height, width = struct.unpack("!xHH", buf) else: f.read(length-2) return width, height # PNG elif header.startswith("\x89PNG\x0d\x0a\x1a\x0a") or \ header.startswith("\x8aMNG\x0d\x0a\x1a\x0a"): f.seek(12) control = f.read(4) if control in ["IHDR", "MHDR"]: buf = f.read(8) width, height = struct.unpack("!II", buf) return width, height # GIF elif header.startswith("GIF87a") or header.startswith("GIF89a"): f.seek(6) buf = f.read(7) width, height, flags, bci, par = struct.unpack(" 1: return str(delta) + " " + _pluralize_granularity(granularity) def _is_leap_year(year): if year % 4 == 0 and year % 400 != 0: return True return False def distance_of_time_in_words(from_time, to_time=0, granularity="second", round=False): """ Return the absolute time-distance string for two datetime objects, ints or any combination you can dream of. If times are integers, they are interpreted as seconds from now. ``granularity`` dictates where the string calculation is stopped. If set to seconds (default) you will receive the full string. If another accuracy is supplied you will receive an approximation. Available granularities are: 'century', 'decade', 'year', 'month', 'day', 'hour', 'minute', 'second' Setting ``round`` to true will increase the result by 1 if the fractional value is greater than 50% of the granularity unit. Examples: >>> distance_of_time_in_words(86399, round=True, granularity='day') '1 day' >>> distance_of_time_in_words(86399, granularity='day') 'less than 1 day' >>> distance_of_time_in_words(86399) '23 hours, 59 minutes and 59 seconds' >>> distance_of_time_in_words(datetime(2008,3,21, 16,34), ... datetime(2008,2,6,9,45)) '1 month, 15 days, 6 hours and 49 minutes' >>> distance_of_time_in_words(datetime(2008,3,21, 16,34), ... datetime(2008,2,6,9,45), granularity='decade') 'less than 1 decade' >>> distance_of_time_in_words(datetime(2008,3,21, 16,34), ... datetime(2008,2,6,9,45), granularity='second') '1 month, 15 days, 6 hours and 49 minutes' """ granularities = ['century', 'decade', 'year', 'month', 'day', 'hour', 'minute', 'second'] # 15 days in the month is a gross approximation, but this # value is only used if rounding to the nearest month granularity_size = {'century': 10, 'decade': 10, 'year': 10, 'month': 12, 'day': 15, 'hour': 24, 'minute': 60, 'second': 60 } if granularity not in granularities: raise ValueError("Please provide a valid granularity: %s" % (granularities)) # Get everything into datetimes if isinstance(from_time, int): from_time = datetime.fromtimestamp(time.time()+from_time) if isinstance(to_time, int): to_time = datetime.fromtimestamp(time.time()+to_time) # Ensure that the to_time is the larger if from_time > to_time: s = from_time from_time = to_time to_time = s # Stop if the tiems are equal elif from_time == to_time: return "0 " + _pluralize_granularity(granularity) # Collect up all the differences deltas = {'century': 0, 'decade': 0, 'year': 0, 'month': 0, 'day': 0, 'hour': 0, 'minute': 0, 'second' : 0} # Collect the easy deltas for field in ['month', 'hour', 'day', 'minute', 'second']: deltas[field] = getattr(to_time,field) - getattr(from_time,field) # deal with year, century and decade delta_year = to_time.year - from_time.year if delta_year >= 100: deltas['century'] = delta_year // 100 if delta_year % 100 >= 10: deltas['decade'] = delta_year // 10 - deltas['century'] * 10 if delta_year % 10: deltas['year'] = delta_year % 10 # Now we need to deal with the negative deltas, as we move from # the smallest granularity to the largest when we encounter a negative # we will 'borrow' from the next highest value. Because to_time is # the larger of the two, carry_over = [('second', 'minute', granularity_size['second']), ('minute', 'hour', granularity_size['minute']), ('hour', 'day', granularity_size['hour'])] _process_carryover(deltas, carry_over) # Day is its own special animal. We need to deal with negative days # differently depending on what months we are spanning across. We need to # look up the from_time.month value in order to bring the number of days # to the end of the month. month_carry = [None, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] if deltas['day'] < 0: deltas['month'] -= 1 # Deal with leap years if (from_time.month) == 2 and _is_leap_year(from_time.year): deltas['day'] += 29 else: deltas['day'] += month_carry[from_time.month] carry_over = [('month', 'year', granularity_size['month']), ('year', 'decade', granularity_size['year']), ('decade', 'century', granularity_size['decade'])] _process_carryover(deltas, carry_over) # Display the differences we care about, at this point we should only have # positive deltas return_strings = [] for g in granularities: delta = deltas[g] # This is the finest granularity we will display if g == granularity: # We can only use rounding if the granularity is higher than # seconds if round and g != 'second': i = granularities.index(g) # Get the next finest granularity and it's delta g_p = granularities[i + 1] delta_p = deltas[g_p] # Determine if we should round up if delta_p > granularity_size[g_p] / 2: delta += 1 if delta != 0: return_strings.append(_delta_string(delta, g)) if not return_strings: return "less than 1 " + granularity break else: if delta != 0: return_strings.append(_delta_string(delta, g)) # We're not rounding, check to see if we have encountered # any deltas to display, if not our time difference # is less than our finest granularity if not return_strings: return "less than 1 " + granularity break # Read the value and continue else: if delta != 0: return_strings.append(_delta_string(delta, g)) if len(return_strings) == 1: return return_strings[0] return ", ".join(return_strings[:-1]) + " and " + return_strings[-1] def time_ago_in_words(from_time, granularity="second", round=False): """ Return approximate-time-distance string for ``from_time`` till now. Same as ``distance_of_time_in_words`` but the endpoint is now. """ return distance_of_time_in_words(from_time, datetime.now(), granularity, round) WebHelpers-1.3/webhelpers/textile.py0000644000175000017500000034112211373653355017205 0ustar sluggosluggo#!/usr/bin/env python # _*_ coding: iso-8859-1 _*_ """This is Textile A Humane Web Text Generator TODO: * Make it work with Python 2.1. * Make it work with Python 1.5.2? Or that's too optimistic? --- To get an overview of all PyTextile's features, simply type 'tell me about textile.' in a single line. """ __authors__ = ["Roberto A. F. De Almeida (roberto@dealmeida.net)", "Mark Pilgrim (f8dy@diveintomark.org)"] __version__ = "2.0.10" __date__ = "2004/10/06" __copyright__ = """ Copyright (c) 2004, Roberto A. F. De Almeida, http://dealmeida.net/ Copyright (c) 2003, Mark Pilgrim, http://diveintomark.org/ All rights reserved. Original PHP version: Version 1.0 21 Feb, 2003 Copyright (c) 2003, Dean Allen, www.textism.com All rights reserved. Parts of the documentation and some of the regular expressions are (c) Brad Choate, http://bradchoate.com/. Thanks, Brad! """ __license__ = """ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Textile nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. """ __history__ = """ 1.0 - 2003/03/19 - MAP - initial release 1.01 - 2003/03/19 - MAP - don't strip whitespace within
     tags;
      map high-bit ASCII to HTML numeric entities
    1.02 - 2003/03/19 - MAP - changed hyperlink qtag expression to only
      match valid URL characters (per RFC 2396); fixed preg_replace to
      not match across line breaks (solves lots of problems with
      mistakenly matching overlapping inline markup); fixed whitespace
      stripping to only strip whitespace from beginning and end of lines,
      not immediately before and after HTML tags.
    1.03 - 2003/03/20 - MAP - changed hyperlink qtag again to more
      closely match original Textile (fixes problems with links
      immediately followed by punctuation -- somewhere Dean is
      grinning right now); handle curly apostrophe with "ve"
      contraction; clean up empty titles at end.
    1.04 - 2003/03/23 - MAP - lstrip input to deal with extra spaces at
      beginning of first line; tweaked list loop to handle consecutive lists
    1.1 - 2003/06/06 - MAP - created initial test suite for links and images,
      and fixed a bunch of related bugs to pass them
    1.11 - 2003/07/20 - CL - don't demoronise unicode strings; handle
      "they're" properly
    1.12 - 2003/07/23 - GW - print debug messages to stderr; handle bq(cite).
    1.13 - 2003/07/23 - MAP - wrap bq. text in 

    ...

    2 - 2004/03/26 - RAFA - rewritten from (almost) scratch to include all features from Textile 2 and a little bit more. 2.0.1 - 2004/04/02 - RAFA - Fixed validating function that uses uTidyLib. 2.0.2 - 2004/04/02 - RAFA - Fixed problem with caps letters in URLs. 2.0.3 - 2004/04/19 - RAFA - Multiple classes are allowed, thanks to Dave Anderson. The "lang" attribute is now removed from , to be valid XHTML. Fixed UCAS problem. 2.0.4 - 2004/05/20 - RAFA, CLB - Added inline formatting to table cells. Curt Bergmann fixed a bug with the colspan formatting. Added Amazon Associated id. 2.0.5 - 2004/06/01 - CL - Applied patch from Chris Lawrence to (1) fix that Amazon associates ID was being added to all search URIs, (2) customize the Amazon site used with the AMAZON variable, and (3) added an "isbn" URI type that links directly to an Amazon product by ISBN or Amazon ASIN. 2.0.6 - 2004/06/02 - RAFA - Fixed CAPS problem, again. I hope this is the last time. 2.0.7 - 2004/06/04 - RAFA, MW - Fixed bullet macro, thanks to Adam Messinger. Added patch from Michal Wallace changing {}.pop() for compatibility with Python 2.2.x. 2.0.8 - 2004/06/25 - RAFA - Strip tags when adding the content from a footnote to the reference link. Escaped '<' and '>' in the self- generated documentation. 2.0.9 - 2004/10/04 - RAFA - In images, if ALT is not defined, add an empty attribute. Added "LaTeX" style open/close quotes. Fixed a bug where the acronym definition was being formatted with inline rules. Handle "broken" lines correctly, removing the
    from inside split HTML tags. 2.0.10 - 2004/10/06 - RAFA, LO - Escape all non-escaped ampersands. Applied "trivial patch" from Ludvig Omholt to remove newline right after the
     tag.
    """
    
    # Set your encoding here.
    ENCODING = 'latin-1'
    
    # Output? Non-ASCII characters will be automatically
    # converted to XML entities if you choose ASCII.
    OUTPUT = 'ascii'
    
    # PyTextile can optionally validate the generated
    # XHTML code. We can use either mxTidy or uTidyLib.
    # You can change the default behaviour here.
    VALIDATE = 0
    
    # If you want h1. to be translated to something other
    # than 

    , change this offset. You can also pass it # as an argument to textile(). HEAD_OFFSET = 0 # If you want to use itex2mml, specify the full path # to the binary here. You can download it from here: # http://golem.ph.utexas.edu/~distler/blog/files/itexToMML.tar.gz itex2mml = None #itex2mml = '/usr/local/bin/itex2MML' #itex2mml = '/usr/people/almeida/bin/itex2MML' # PyTextile can optionally sanitize the generated XHTML, # which is good for weblog comments or if you don't trust # yourself. SANITIZE = 0 # Turn debug on? DEBUGLEVEL = 0 # Amazon associate for links: "keywords":amazon # If you don't have one, please consider leaving mine here as # a small compensation for writing PyTextile. It's commented # off as default. #amazon_associate_id = 'bomtempo-21' amazon_associate_id = None #AMAZON = 'www.amazon.co.uk' AMAZON = 'www.amazon.com' import re import sys import os import sgmllib try: import unicodedata except ImportError: unicodedata = None def _in_tag(text, tag): """Extracts text from inside a tag. This function extracts the text from inside a given tag. It's useful to get the text between or
     when using the validators or the colorizer.
        """
        if text.count('<%s' % tag):
            text = text.split('<%s' % tag, 1)[1]
            if text.count('>'):
                text = text.split('>', 1)[1]
        if text.count('

    from input. code = _in_tag(code_out.getvalue(), 'pre') # Fix newlines. code = code.replace('\n', '\n') return code except ImportError: htmlizer = None # PyTextile can optionally validate the generated # XHTML code using either mxTidy or uTidyLib. try: # This is mxTidy. from mx.Tidy import Tidy def _tidy1(text): """mxTidy's XHTML validator. This function is a wrapper to mxTidy's validator. """ nerrors, nwarnings, text, errortext = Tidy.tidy(text, output_xhtml=1, numeric_entities=1, wrap=0) return _in_tag(text, 'body') _tidy = _tidy1 except ImportError: try: # This is uTidyLib. import tidy def _tidy2(text): """uTidyLib's XHTML validator. This function is a wrapper to uTidyLib's validator. """ text = tidy.parseString(text, output_xhtml=1, add_xml_decl=0, indent=0, tidy_mark=0) return _in_tag(str(text), 'body') _tidy = _tidy2 except ImportError: _tidy = None # This is good for debugging. def _debug(s, level=1): """Outputs debug information to sys.stderr. This function outputs debug information if DEBUGLEVEL is higher than a given treshold. """ if DEBUGLEVEL >= level: print >> sys.stderr, s ############################# # Useful regular expressions. parameters = { # Horizontal alignment. 'align': r'''(?:(?:<>|[<>=]) # Either '<>', '<', '>' or '=' (?![^\s]*(?:<>|[<>=]))) # Look-ahead to ensure it happens once ''', # Horizontal padding. 'padding': r'''(?:[\(\)]+) # Any number of '(' and/or ')' ''', # Class and/or id. 'classid': r'''( # (?:\(\#[\w]+\)) # (#id) | # (?:\((?:[\w]+(?:\s[\w]+)*) # (?:\#[\w]+)?\)) # (class1 class2 ... classn#id) or (class1 class2 ... classn) ) # (?![^\s]*(?:\([\w#]+\))) # must happen once ''', # Language. 'lang': r'''(?:\[[\w-]+\]) # [lang] (?![^\s]*(?:\[.*?\])) # must happen once ''', # Style. 'style': r'''(?:{[^\}]+}) # {style} (?![^\s]*(?:{.*?})) # must happen once ''', } res = { # Punctuation. 'punct': r'''[\!"#\$%&'()\*\+,\-\./:;<=>\?@\[\\\]\^_`{\|}\~]''', # URL regular expression. 'url': r'''(?=[a-zA-Z0-9./#]) # Must start correctly (?: # Match the leading part (proto://hostname, or just hostname) (?:ftp|https?|telnet|nntp) # protocol :// # :// (?: # Optional 'username:password@' \w+ # username (?::\w+)? # optional :password @ # @ )? # [-\w]+(?:\.\w[-\w]*)+ # hostname (sub.example.com) | # (?:mailto:)? # Optional mailto: [-\+\w]+ # username \@ # at [-\w]+(?:\.\w[-\w]*)+ # hostname | # (?:[a-z0-9](?:[-a-z0-9]*[a-z0-9])?\.)+ # domain without protocol (?:com\b # TLD | edu\b # | biz\b # | gov\b # | in(?:t|fo)\b # .int or .info | mil\b # | net\b # | org\b # | museum\b # | aero\b # | coop\b # | name\b # | pro\b # | [a-z][a-z]\b # two-letter country codes ) # )? # (?::\d+)? # Optional port number (?: # Rest of the URL, optional /? # Start with '/' [^.!,?;:"'<>()\[\]{}\s\x7F-\xFF]* # Can't start with these (?: # [.!,?;:]+ # One or more of these [^.!,?;:"'<>()\[\]{}\s\x7F-\xFF]+ # Can't finish with these #'" # # or ' or " )* # )? # ''', # Block attributes. 'battr': r'''(?P # (?: %(align)s # alignment | %(classid)s # class and/or id | %(padding)s # padding tags | %(lang)s # [lang] | %(style)s # {style} )+ # )? # ''' % parameters, # (Un)ordered list attributes. 'olattr': r'''(?P # (?: %(align)s # alignment | ((?:\(\#[\w]+\)) # (#id) | # (?:\((?:[\w]+(?:\s[\w]+)*) # (?:\#[\w]+)?\)) # (class1 class2 ... classn#id) or (class1 class2 ... classn) ) # | %(padding)s # padding tags | %(lang)s # [lang] | %(style)s # {style} )+ # )? # ''' % parameters, # List item attributes. 'liattr': r'''(?P # (?: %(align)s # alignment | %(classid)s # class and/or id | %(padding)s # padding tags | %(lang)s # [lang] | %(style)s # {style} )+ # )? # ''' % parameters, # Qtag attributes. 'qattr': r'''(?P # (?: %(classid)s # class and/or id | %(lang)s # [lang] | %(style)s # {style} )+ # )? # ''' % parameters, # Link attributes. 'lattr': r'''(?P # Links attributes (?: %(align)s # alignment | %(classid)s # class and/or id | %(lang)s # [lang] | %(style)s # {style} )+ # )? # ''' % parameters, # Image attributes. 'iattr': r'''(?P # (?: # (?: [<>]+ # horizontal alignment tags (?![^\s]*(?:[<>]))) # (must happen once) | # (?: [\-\^~]+ # vertical alignment tags (?![^\s]*(?:[\-\^~]))) # (must happen once) | %(classid)s # class and/or id | %(padding)s # padding tags | %(style)s # {style} )+ # )? # ''' % parameters, # Resize attributes. 'resize': r'''(?: # (?:([\d]+%?)x([\d]+%?)) # 20x10 | # (?: # or (?:([\d]+)%?w\s([\d]+)%?h) # 10h 20w | # or (?:([\d]+)%?h\s([\d]+)%?w) # 20w 10h ) # )? # ''', # Table attributes. 'tattr': r'''(?P # (?: # (?: [\^~] # vertical alignment (?![^\s]*(?:[\^~]))) # (must happen once) | %(align)s # alignment | %(lang)s # [lang] | %(style)s # {style} | %(classid)s # class and/or id | %(padding)s # padding | _ # is this a header row/cell? | \\\d+ # colspan | /\d+ # rowspan )+ # )? # ''' % parameters, } def preg_replace(pattern, replacement, text): """Alternative re.sub that handles empty groups. This acts like re.sub, except it replaces empty groups with '' instead of raising an exception. """ def replacement_func(matchobj): counter = 1 rc = replacement _debug(matchobj.groups()) for matchitem in matchobj.groups(): if not matchitem: matchitem = '' rc = rc.replace(r'\%s' % counter, matchitem) counter += 1 return rc p = re.compile(pattern) _debug(pattern) return p.sub(replacement_func, text) def html_replace(pattern, replacement, text): """Replacement outside HTML tags. Does a preg_replace only outside HTML tags. """ # If there is no html, do a simple search and replace. if not re.search(r'''<.*>''', text): return preg_replace(pattern, replacement, text) else: lines = [] # Else split the text into an array at <>. for line in re.split('(<.*?>)', text): if not re.match('<.*?>', line): line = preg_replace(pattern, replacement, line) lines.append(line) return ''.join(lines) # PyTextile can optionally sanitize the generated XHTML, # which is good for weblog comments. This code is from # Mark Pilgrim's feedparser. class _BaseHTMLProcessor(sgmllib.SGMLParser): elements_no_end_tag = ['area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', 'input', 'isindex', 'link', 'meta', 'param'] def __init__(self): sgmllib.SGMLParser.__init__(self) def reset(self): self.pieces = [] sgmllib.SGMLParser.reset(self) def normalize_attrs(self, attrs): # utility method to be called by descendants attrs = [(k.lower(), sgmllib.charref.sub(lambda m: unichr(int(m.groups()[0])), v).strip()) for k, v in attrs] attrs = [(k, k in ('rel', 'type') and v.lower() or v) for k, v in attrs] return attrs def unknown_starttag(self, tag, attrs): # called for each start tag # attrs is a list of (attr, value) tuples # e.g. for
    , tag="pre", attrs=[("class", "screen")]
            strattrs = "".join([' %s="%s"' % (key, value) for key, value in attrs])
            if tag in self.elements_no_end_tag:
                self.pieces.append("<%(tag)s%(strattrs)s />" % locals())
            else:
                self.pieces.append("<%(tag)s%(strattrs)s>" % locals())
            
        def unknown_endtag(self, tag):
            # called for each end tag, e.g. for 
    , tag will be "pre" # Reconstruct the original end tag. if tag not in self.elements_no_end_tag: self.pieces.append("" % locals()) def handle_charref(self, ref): # called for each character reference, e.g. for " ", ref will be "160" # Reconstruct the original character reference. self.pieces.append("&#%(ref)s;" % locals()) def handle_entityref(self, ref): # called for each entity reference, e.g. for "©", ref will be "copy" # Reconstruct the original entity reference. self.pieces.append("&%(ref)s;" % locals()) def handle_data(self, text): # called for each block of plain text, i.e. outside of any tag and # not containing any character or entity references # Store the original text verbatim. self.pieces.append(text) def handle_comment(self, text): # called for each HTML comment, e.g. # Reconstruct the original comment. self.pieces.append("" % locals()) def handle_pi(self, text): # called for each processing instruction, e.g. # Reconstruct original processing instruction. self.pieces.append("" % locals()) def handle_decl(self, text): # called for the DOCTYPE, if present, e.g. # # Reconstruct original DOCTYPE self.pieces.append("" % locals()) def output(self): """Return processed HTML as a single string""" return "".join(self.pieces) class _HTMLSanitizer(_BaseHTMLProcessor): acceptable_elements = ['a', 'abbr', 'acronym', 'address', 'area', 'b', 'big', 'blockquote', 'br', 'button', 'caption', 'center', 'cite', 'code', 'col', 'colgroup', 'dd', 'del', 'dfn', 'dir', 'div', 'dl', 'dt', 'em', 'fieldset', 'font', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'hr', 'i', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'map', 'menu', 'ol', 'optgroup', 'option', 'p', 'pre', 'q', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var'] acceptable_attributes = ['abbr', 'accept', 'accept-charset', 'accesskey', 'action', 'align', 'alt', 'axis', 'border', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'cite', 'class', 'clear', 'cols', 'colspan', 'color', 'compact', 'coords', 'datetime', 'dir', 'disabled', 'enctype', 'for', 'frame', 'headers', 'height', 'href', 'hreflang', 'hspace', 'id', 'ismap', 'label', 'lang', 'longdesc', 'maxlength', 'media', 'method', 'multiple', 'name', 'nohref', 'noshade', 'nowrap', 'prompt', 'readonly', 'rel', 'rev', 'rows', 'rowspan', 'rules', 'scope', 'selected', 'shape', 'size', 'span', 'src', 'start', 'summary', 'tabindex', 'target', 'title', 'type', 'usemap', 'valign', 'value', 'vspace', 'width'] unacceptable_elements_with_end_tag = ['script', 'applet'] # This if for MathML. mathml_elements = ['math', 'mi', 'mn', 'mo', 'mrow', 'msup'] mathml_attributes = ['mode', 'xmlns'] acceptable_elements = acceptable_elements + mathml_elements acceptable_attributes = acceptable_attributes + mathml_attributes def reset(self): _BaseHTMLProcessor.reset(self) self.unacceptablestack = 0 def unknown_starttag(self, tag, attrs): if not tag in self.acceptable_elements: if tag in self.unacceptable_elements_with_end_tag: self.unacceptablestack += 1 return attrs = self.normalize_attrs(attrs) attrs = [(key, value) for key, value in attrs if key in self.acceptable_attributes] _BaseHTMLProcessor.unknown_starttag(self, tag, attrs) def unknown_endtag(self, tag): if not tag in self.acceptable_elements: if tag in self.unacceptable_elements_with_end_tag: self.unacceptablestack -= 1 return _BaseHTMLProcessor.unknown_endtag(self, tag) def handle_pi(self, text): pass def handle_decl(self, text): pass def handle_data(self, text): if not self.unacceptablestack: _BaseHTMLProcessor.handle_data(self, text) class Textiler: """Textile formatter. This is the base class for the PyTextile text processor. """ def __init__(self, text=''): """Instantiate the class, passing the text to be formatted. Here we pre-process the text and collect all the link lookups for later. """ self.text = text # Basic regular expressions. self.res = res # Smart searches. self.searches = {} self.searches['imdb'] = 'http://www.imdb.com/Find?for=%s' self.searches['google'] = 'http://www.google.com/search?q=%s' self.searches['python'] = 'http://www.python.org/doc/current/lib/module-%s.html' if amazon_associate_id: self.searches['isbn'] = ''.join(['http://', AMAZON, '/exec/obidos/ASIN/%s/', amazon_associate_id]) self.searches['amazon'] = ''.join(['http://', AMAZON, '/exec/obidos/external-search?mode=blended&keyword=%s&tag=', amazon_associate_id]) else: self.searches['isbn'] = ''.join(['http://', AMAZON, '/exec/obidos/ASIN/%s']) self.searches['amazon'] = ''.join(['http://', AMAZON, '/exec/obidos/external-search?mode=blended&keyword=%s']) # These are the blocks we know. self.signatures = [ # Paragraph. (r'''^p # Paragraph signature %(battr)s # Paragraph attributes (?P\.) # . (?P\.)? # Extended paragraph denoted by a second dot \s # whitespace (?P.*) # text ''' % self.res, self.paragraph), # Pre-formatted text. (r'''^pre # Pre signature %(battr)s # Pre attributes (?P\.) # . (?P\.)? # Extended pre denoted by a second dot \s # whitespace (?P.*) # text ''' % self.res, self.pre), # Block code. (r'''^bc # Blockcode signature %(battr)s # Blockcode attributes (?P\.) # . (?P\.)? # Extended blockcode denoted by a second dot \s # whitespace (?P.*) # text ''' % self.res, self.bc), # Blockquote. (r'''^bq # Blockquote signature %(battr)s # Blockquote attributes (?P\.) # . (?P\.)? # Extended blockquote denoted by a second dot (:(?P # Optional cite attribute ( # %(url)s # URL | "[\w]+(?:\s[\w]+)*" # "Name inside quotes" )) # )? # \s # whitespace (?P.*) # text ''' % self.res, self.blockquote), # Header. (r'''^h # Header signature (?P
    \d) # Header number %(battr)s # Header attributes (?P\.) # . (?P\.)? # Extended header denoted by a second dot \s # whitespace (?P.*) # text ''' % self.res, self.header), # Footnote. (r'''^fn # Footnote signature (?P[\d]+) # Footnote number (?P\.) # . (?P\.)? # Extended footnote denoted by a second dot \s # whitespace (?P.*) # text ''', self.footnote), # Definition list. (r'''^dl # Definition list signature %(battr)s # Definition list attributes (?P\.) # . (?P\.)? # Extended definition list denoted by a second dot \s # whitespace (?P.*) # text ''' % self.res, self.dl), # Ordered list (attributes to first
  1. ). (r'''^%(olattr)s # Ordered list attributes \# # Ordered list signature %(liattr)s # List item attributes (?P\.)? # . \s # whitespace (?P.*) # text ''' % self.res, self.ol), # Unordered list (attributes to first
  2. ). (r'''^%(olattr)s # Unrdered list attributes \* # Unordered list signature %(liattr)s # Unordered list attributes (?P\.)? # . \s # whitespace (?P.*) # text ''' % self.res, self.ul), # Escaped text. (r'''^==?(?P.*?)(==)?$ # Escaped text ''', self.escape), (r'''^(?P<.*)$ # XHTML tag ''', self.escape), # itex code. (r'''^(?P # itex code \\\[ # starts with \[ .*? # complicated mathematical equations go here \\\]) # ends with \] ''', self.itex), # Tables. (r'''^table # Table signature %(tattr)s # Table attributes (?P\.) # . (?P\.)? # Extended blockcode denoted by a second dot \s # whitespace (?P.*) # text ''' % self.res, self.table), # Simple tables. (r'''^(?P \| .*) ''', self.table), # About. (r'''^(?Ptell\sme\sabout\stextile\.)$''', self.about), ] def preprocess(self): """Pre-processing of the text. Remove whitespace, fix carriage returns. """ # Remove whitespace. self.text = self.text.strip() # Zap carriage returns. self.text = self.text.replace("\r\n", "\n") self.text = self.text.replace("\r", "\n") # Minor sanitizing. self.text = self.sanitize(self.text) def grab_links(self): """Grab link lookups. Check the text for link lookups, store them in a dictionary, and clean them up. """ # Grab links like this: '[id]example.com' links = {} p = re.compile(r'''(?:^|\n)\[([\w]+?)\](%(url)s)(?:$|\n)''' % self.res, re.VERBOSE) for key, link in p.findall(self.text): links[key] = link # And clear them from the text. self.text = p.sub('', self.text) return links def process(self, head_offset=HEAD_OFFSET, validate=VALIDATE, sanitize=SANITIZE, output=OUTPUT, encoding=ENCODING): """Process the text. Here we actually process the text, splitting the text in blocks and applying the corresponding function to each one of them. """ # Basic global changes. self.preprocess() # Grab lookup links and clean them from the text. self._links = self.grab_links() # Offset for the headers. self.head_offset = head_offset # Process each block. self.blocks = self.split_text() text = [] for [function, captures] in self.blocks: text.append(function(**captures)) text = '\n\n'.join(text) # Add titles to footnotes. text = self.footnotes(text) # Convert to desired output. if isinstance(text, str): text = unicode(text, encoding) text = text.encode(output, 'xmlcharrefreplace') # Sanitize? if sanitize: p = _HTMLSanitizer() p.feed(text) text = p.output() # Validate output. if _tidy and validate: text = _tidy(text) return text def sanitize(self, text): """Fix single tags. Fix tags like ,
    and
    . --- h1. Sanitizing Textile can help you generate valid XHTML(eXtensible HyperText Markup Language). It will fix any single tags that are not properly closed, like @@, @
    @ and @
    @. If you have "mx.Tidy":http://www.egenix.com/files/python/mxTidy.html and/or "µTidyLib":http://utidylib.sourceforge.net/ installed, it also can optionally validade the generated code with these wrappers to ensure 100% valid XHTML(eXtensible HyperText Markup Language). """ # Fix single tags like and
    . text = preg_replace(r'''<(img|br|hr)(.*?)(?:\s*/?\s*)?>''', r'''<\1\2 />''', text) # Remove ampersands. text = preg_replace(r'''&(?!#?[xX]?(?:[0-9a-fA-F]+|\w{1,8});)''', r'''&''', text) return text def split_text(self): """Process the blocks from the text. Split the blocks according to the signatures, join extended blocks and associate each one of them with a function to process them. --- h1. Blocks Textile process your text by dividing it in blocks. Each block is identified by a signature and separated from other blocks by an empty line. All signatures should end with a period followed by a space. A header @

    @ can be done this way: pre. h1. This is a header 1. Blocks may continue for multiple paragraphs of text. If you want a block signature to stay "active", use two periods after the signature instead of one. For example: pre.. bq.. This is paragraph one of a block quote. This is paragraph two of a block quote. =p. Now we're back to a regular paragraph. p. Becomes: pre..

    This is paragraph one of a block quote.

    This is paragraph two of a block quote.

    Now we’re back to a regular paragraph.

    p. The blocks can be customised by adding parameters between the signature and the period. These include: dl. {style rule}:A CSS(Cascading Style Sheets) style rule. [ll]:A language identifier (for a "lang" attribute). (class) or (#id) or (class#id):For CSS(Cascading Style Sheets) class and id attributes. >, <, =, <>:Modifier characters for alignment. Right-justification, left-justification, centered, and full-justification. The paragraph will also receive the class names "right", "left", "center" and "justify", respectively. ( (one or more):Adds padding on the left. 1em per "(" character is applied. When combined with the align-left or align-right modifier, it makes the block float. ) (one or more):Adds padding on the right. 1em per ")" character is applied. When combined with the align-left or align-right modifier, it makes the block float. Here's an overloaded example: pre. p(())>(class#id)[en]{color:red}. A simple paragraph. Becomes: pre.

    A simple paragraph.

    """ # Clear signature. clear_sig = r'''^clear(?P[<>])?\.$''' clear = None extending = 0 # We capture the \n's because they are important inside "pre..". blocks = re.split(r'''(\n{2,})''', self.text) output = [] for block in blocks: # Check for the clear signature. m = re.match(clear_sig, block) if m: clear = m.group('alignment') if clear: clear = {'<': 'clear:left;', '>': 'clear:right;'}[clear] else: clear = 'clear:both;' else: # Check each of the code signatures. for regexp, function in self.signatures: p = re.compile(regexp, (re.VERBOSE | re.DOTALL)) m = p.match(block) if m: # Put everything in a dictionary. captures = m.groupdict() # If we are extending a block, we require a dot to # break it, so we can start lines with '#' inside # an extended
     without matching an ordered list.
                            if extending and not captures.get('dot', None):
                                output[-1][1]['text'] += block
                                break 
                            elif captures.has_key('dot'):
                                del captures['dot']
                                
                            # If a signature matches, we are not extending a block.
                            extending = 0
    
                            # Check if we should extend this block.
                            if captures.has_key('extend'):
                                extending = captures['extend']
                                del captures['extend']
                                
                            # Apply head_offset.
                            if captures.has_key('header'):
                                captures['header'] = int(captures['header']) + self.head_offset
    
                            # Apply clear.
                            if clear:
                                captures['clear'] = clear
                                clear = None
    
                            # Save the block to be processed later.
                            output.append([function, captures])
    
                            break
    
                    else:
                        if extending:
                            # Append the text to the last block.
                            output[-1][1]['text'] += block
                        elif block.strip():
                            output.append([self.paragraph, {'text': block}])
        
            return output
    
    
        def parse_params(self, parameters, clear=None, align_type='block'):
            """Parse the parameters from a block signature.
    
            This function parses the parameters from a block signature,
            splitting the information about class, id, language and
            style. The positioning (indentation and alignment) is parsed
            and stored in the style.
    
            A paragraph like:
    
                p>(class#id){color:red}[en]. Paragraph.
    
            or:
                
                p{color:red}[en](class#id)>. Paragraph.
    
            will have its parameters parsed to:
    
                output = {'lang' : 'en',
                          'class': 'class',
                          'id'   : 'id',
                          'style': 'color:red;text-align:right;'}
    
            Note that order is not important.
            """
            if not parameters:
                if clear:
                    return {'style': clear}
                else:
                    return {}
    
            output = {}
            
            # Match class from (class) or (class#id).
            m = re.search(r'''\((?P[\w]+(\s[\w]+)*)(\#[\w]+)?\)''', parameters)
            if m: output['class'] = m.group('class')
    
            # Match id from (#id) or (class#id).
            m = re.search(r'''\([\w]*(\s[\w]+)*\#(?P[\w]+)\)''', parameters)
            if m: output['id'] = m.group('id')
    
            # Match [language].
            m = re.search(r'''\[(?P[\w-]+)\]''', parameters)
            if m: output['lang'] = m.group('lang')
    
            # Match {style}.
            m = re.search(r'''{(?P