From 6903db9d7c2535375e6802294adeb56f5c71f785 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 08:58:13 -0500 Subject: [PATCH 01/10] fixed CRYPT --- VERSION | 2 +- gluon/tools.py | 3 ++- gluon/utils.py | 12 ++++++------ gluon/validators.py | 21 ++++++++++++++++++++- 4 files changed, 29 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index d44d196a..33097687 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-19 16:56:40) dev +Version 2.00.0 (2012-07-20 08:58:08) dev diff --git a/gluon/tools.py b/gluon/tools.py index 4eaafd8e..0dd4c58c 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -1198,7 +1198,8 @@ class Auth(object): if URL() == action: next = '' else: - next = '?_next=' + urllib.quote(URL(args=request.args, vars=request.get_vars)) + next = '?_next=' + urllib.quote(URL(args=request.args, + vars=request.get_vars)) href = lambda function: '%s/%s%s' % (action, function, next if referrer_actions is DEFAULT or function in referrer_actions else '') diff --git a/gluon/utils.py b/gluon/utils.py index 64a714ab..3c68ab85 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -72,12 +72,12 @@ def get_digest(value): raise ValueError("Invalid digest algorithm") DIGEST_ALG_BY_SIZE = { - 128/16: 'md5', - 160/16: 'sha1', - 224/16: 'sha224', - 256/16: 'sha256', - 384/16: 'sha384', - 512/16: 'sha512', + 128/4: 'md5', + 160/4: 'sha1', + 224/4: 'sha224', + 256/4: 'sha256', + 384/4: 'sha384', + 512/4: 'sha512', } def hmac_hash(value, salt, digest_alg='md5'): diff --git a/gluon/validators.py b/gluon/validators.py index 85d23539..a5fab8cb 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2579,7 +2579,13 @@ class LazyCrypt(object): """ compares the current lazy crypted password with a stored password """ - key = self.crypt.key.split(':')[1] if ':' in self.crypt.key else '' + if self.crypt.key: + if ':' in self.crypt.key: + key = self.crypt.key.split(':')[1] + else: + key = self.crypt.key + else: + key = '' if stored_password.count('$')==2: (digest_alg, salt, hash) = stored_password.split('$') masterkey = key+salt @@ -2629,6 +2635,19 @@ class CRYPT(object): Important: hashed password is returned as a LazyCrypt object and computed only if needed. The LasyCrypt object also knows how to compare itself with an existing salted password + Some tests: + + >>> a = str(CRYPT(digest_alg='sha1',salt=False)('test')[0]) + >>> a + 'sha1$$a94a8fe5ccb19ba61c4c0873d391e987982fbbd3' + >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a + True + >>> CRYPT(digest_alg='sha1',salt=False)('test')[0] == a[6:] + True + >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a + True + >>> CRYPT(digest_alg='md5',salt=False)('test')[0] == a[6:] + True """ def __init__(self, From a8131c2b432f469f2b712bfeefe1e138ef3d08f1 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 10:17:55 -0500 Subject: [PATCH 02/10] added more CRYPT examples and doctests, fixed other doctests --- VERSION | 2 +- gluon/utils.py | 2 +- gluon/validators.py | 40 +++++++++++++++++++++++++++++++++++----- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 33097687..4e1d7daf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 08:58:08) dev +Version 2.00.0 (2012-07-20 10:17:51) dev diff --git a/gluon/utils.py b/gluon/utils.py index 3c68ab85..b3747979 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -16,7 +16,7 @@ import random import time import os import logging -from gluon.contrib.pbkdf2 import pbkdf2_hex +from contrib.pbkdf2 import pbkdf2_hex logger = logging.getLogger("web2py") diff --git a/gluon/validators.py b/gluon/validators.py index a5fab8cb..1582c060 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -130,7 +130,7 @@ class IS_MATCH(Validator): ('hello', None) >>> IS_MATCH('hell')('hello') - ('hello', 'invalid expression') + ('hello', None) >>> IS_MATCH('hell.*', strict=False)('hello') ('hello', None) @@ -139,10 +139,10 @@ class IS_MATCH(Validator): ('shello', 'invalid expression') >>> IS_MATCH('hello', search=True)('shello') - ('hello', None) + ('shello', None) >>> IS_MATCH('hello', search=True, strict=False)('shellox') - ('hello', None) + ('shellox', None) >>> IS_MATCH('.*hello.*', search=True, strict=False)('shellox') ('shellox', None) @@ -2635,7 +2635,36 @@ class CRYPT(object): Important: hashed password is returned as a LazyCrypt object and computed only if needed. The LasyCrypt object also knows how to compare itself with an existing salted password - Some tests: + Supports standard algorithms + + >>> for alg in ('md5','sha1','sha256','sha384','sha512'): + ... print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) + md5$...$... + sha1$...$... + sha256$...$... + sha384$...$... + sha512$...$... + + The syntax is always alg$salt$hash + + Supports for pbkdf2 + + >>> alg = 'pbkdf2(1000,20,sha512)' + >>> print str(CRYPT(digest_alg=alg,salt=True)('test')[0]) + pbkdf2(1000,20,sha512)$...$... + + An optional hmac_key can be specified and it is used as salt prefix + + >>> a = str(CRYPT(digest_alg='md5',key='mykey',salt=True)('test')[0]) + >>> print a + md5$...$... + + Even if the algorithm changes the hash can still be validated + + >>> CRYPT(digest_alg='sha1',key='mykey',salt=True)('test')[0] == a + True + + If no salt is specified CRYPT can guess the algorithms from length: >>> a = str(CRYPT(digest_alg='sha1',salt=False)('test')[0]) >>> a @@ -3112,7 +3141,8 @@ class IS_IPV4(Validator): if __name__ == '__main__': import doctest - doctest.testmod() + doctest.testmod(optionflags=doctest.NORMALIZE_WHITESPACE|doctest.ELLIPSIS) + From ee376afa2da9c9b96974b5c908f129975b8f01dd Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 10:31:07 -0500 Subject: [PATCH 03/10] changed CRYPT, Auth, welcome digest_alg default --- VERSION | 2 +- applications/welcome/models/db.py | 2 +- gluon/tools.py | 5 ++--- gluon/validators.py | 4 ++-- 4 files changed, 6 insertions(+), 7 deletions(-) diff --git a/VERSION b/VERSION index 4e1d7daf..3faf2c88 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 10:17:51) dev +Version 2.00.0 (2012-07-20 10:31:02) dev diff --git a/applications/welcome/models/db.py b/applications/welcome/models/db.py index 980084c9..26d28122 100644 --- a/applications/welcome/models/db.py +++ b/applications/welcome/models/db.py @@ -40,7 +40,7 @@ response.generic_patterns = ['*'] if request.is_local else [] ######################################################################### from gluon.tools import Auth, Crud, Service, PluginManager, prettydate -auth = Auth(db, hmac_key=Auth.get_or_create_key(), salt=True) +auth = Auth(db) crud, service, plugins = Crud(db), Service(), PluginManager() ## create all tables needed by auth if not custom tables diff --git a/gluon/tools.py b/gluon/tools.py index 0dd4c58c..24467443 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -880,7 +880,7 @@ class Auth(object): def here(self): return URL(args=current.request.args,vars=current.request.vars) - def __init__(self, environment=None, db=None, mailer=True, salt = False, + def __init__(self, environment=None, db=None, mailer=True, hmac_key=None, controller='default', function='user', cas_provider=None): """ auth=Auth(db) @@ -922,7 +922,6 @@ class Auth(object): settings.hideerror = False settings.password_min_length = 4 - settings.salt = salt settings.cas_domains = [request.env.http_host] settings.cas_provider = cas_provider settings.cas_actions = {'login':'login', @@ -1410,7 +1409,7 @@ class Auth(object): table.last_name.requires = \ IS_NOT_EMPTY(error_message=self.messages.is_empty) table[passfield].requires = [ - CRYPT(key=settings.hmac_key,salt=settings.salt, + CRYPT(key=settings.hmac_key, min_length=settings.password_min_length)] table.email.requires = \ [IS_EMAIL(error_message=self.messages.invalid_email), diff --git a/gluon/validators.py b/gluon/validators.py index 1582c060..3f6f49de 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2681,9 +2681,9 @@ class CRYPT(object): def __init__(self, key=None, - digest_alg='md5', + digest_alg='pbkdf2(1000,20,sh512)', min_length=0, - error_message='too short', salt=None): + error_message='too short', salt=True): """ important, digest_alg='md5' is not the default hashing algorithm for web2py. This is only an example of usage of this function. From 62d4eb0556fd0d6e200211e75d580ea1bba41a64 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 11:27:38 -0500 Subject: [PATCH 04/10] added comment in welcome default.py --- VERSION | 2 +- applications/welcome/controllers/default.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/VERSION b/VERSION index 3faf2c88..970b83e2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 10:31:02) dev +Version 2.00.0 (2012-07-20 11:27:33) dev diff --git a/applications/welcome/controllers/default.py b/applications/welcome/controllers/default.py index f449da20..4c4b563d 100644 --- a/applications/welcome/controllers/default.py +++ b/applications/welcome/controllers/default.py @@ -13,6 +13,9 @@ def index(): """ example action using the internationalization operator T and flash rendered by views/default/index.html or views/generic.html + + if you need a simple wiki simple replace the two lines below with: + return auth.wiki() """ response.flash = T("Welcome to web2py!") return dict(message=T('Hello World')) From 6497a2a6aa36f3d1a2d581905745c73addf344ea Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 12:47:06 -0500 Subject: [PATCH 05/10] invalid digest %s algorithm, thanks Jonathan --- VERSION | 2 +- gluon/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 970b83e2..e91a9dd5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 11:27:33) dev +Version 2.00.0 (2012-07-20 12:47:01) dev diff --git a/gluon/utils.py b/gluon/utils.py index b3747979..d429b100 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -69,7 +69,7 @@ def get_digest(value): elif value == "sha512": return hashlib.sha512 else: - raise ValueError("Invalid digest algorithm") + raise ValueError("Invalid digest algorithm: %s" % value) DIGEST_ALG_BY_SIZE = { 128/4: 'md5', From bd00292828b18183eb364494460da0ddacbbaa6c Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 13:51:26 -0500 Subject: [PATCH 06/10] oops sh512 --- VERSION | 2 +- gluon/validators.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index e91a9dd5..555c039d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 12:47:01) dev +Version 2.00.0 (2012-07-20 13:51:22) dev diff --git a/gluon/validators.py b/gluon/validators.py index 3f6f49de..a18099fb 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -2681,7 +2681,7 @@ class CRYPT(object): def __init__(self, key=None, - digest_alg='pbkdf2(1000,20,sh512)', + digest_alg='pbkdf2(1000,20,sha512)', min_length=0, error_message='too short', salt=True): """ From 1e0d4c6dfaf44fe897e1f61aaa4a19e86a820250 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 16:32:53 -0500 Subject: [PATCH 07/10] fixed a problem with db().select().sort() did not follow book specs --- VERSION | 2 +- gluon/dal.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 555c039d..4994cbd7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 13:51:22) dev +Version 2.00.0 (2012-07-20 16:32:48) dev diff --git a/gluon/dal.py b/gluon/dal.py index c7322082..42d357e1 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -1800,6 +1800,7 @@ class BaseAdapter(ConnectionPool): query = query & newquery return query + ################################################################################### # List of all the available adapters; they all extend BaseAdapter. ################################################################################### @@ -8593,7 +8594,10 @@ class Rows(object): """ returns a list of sorted elements (not sorted in place) """ - return Rows(self.db,sorted(self,key=f,reverse=reverse),self.colnames) + rows = Rows(self.db,[],self.colnames,compact=False) + rows.records = sorted(self,key=f,reverse=reverse) + return rows + def group_by_value(self, field): """ From 59ddb063455aadb00f4d15db1e56abbd8bf94eef Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 16:45:07 -0500 Subject: [PATCH 08/10] Copyright date now displays current year, always, thanks Brian Cottingham --- VERSION | 2 +- gluon/widget.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 4994cbd7..581a8a64 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 16:32:48) dev +Version 2.00.0 (2012-07-20 16:45:03) dev diff --git a/gluon/widget.py b/gluon/widget.py index f0f7278b..96ef1d6e 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -9,6 +9,7 @@ License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html) The widget is called from web2py. """ +import datetime import sys import cStringIO import time @@ -41,7 +42,7 @@ except NameError: BaseException = Exception ProgramName = 'web2py Web Framework' -ProgramAuthor = 'Created by Massimo Di Pierro, Copyright 2007-2011' +ProgramAuthor = 'Created by Massimo Di Pierro, Copyright 2007-' + str(datetime.datetime.now().year) ProgramVersion = read_file('VERSION').strip() ProgramInfo = '''%s From fa65647aa3f2064472c9c111093f40c58b3a8281 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 16:47:59 -0500 Subject: [PATCH 09/10] fixed typo in X509Auth, thanks Michele --- VERSION | 2 +- gluon/contrib/login_methods/x509_auth.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 581a8a64..02cb0948 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 16:45:03) dev +Version 2.00.0 (2012-07-20 16:47:55) dev diff --git a/gluon/contrib/login_methods/x509_auth.py b/gluon/contrib/login_methods/x509_auth.py index ca8623b6..09c3e922 100644 --- a/gluon/contrib/login_methods/x509_auth.py +++ b/gluon/contrib/login_methods/x509_auth.py @@ -25,7 +25,7 @@ class X509Auth(object): from gluon.contrib.login_methods.x509_auth import X509Account auth.settings.actions_disabled=['register','change_password', 'request_reset_password','profile'] - auth.settings.login_form = X509Account() + auth.settings.login_form = X509Auth() """ From beac56a57a0f6917df63ab3db886d2fdda6248e4 Mon Sep 17 00:00:00 2001 From: mdipierro Date: Fri, 20 Jul 2012 17:37:53 -0500 Subject: [PATCH 10/10] other improvements, thanks Vladyslav --- VERSION | 2 +- applications/admin/controllers/default.py | 6 +- applications/admin/static/css/styles.css | 1 + gluon/contrib/markmin/markmin.html | 37 +++++++----- gluon/contrib/markmin/markmin2html.py | 70 ++++++++++++++++------- gluon/utf8.py | 47 +++++++-------- 6 files changed, 100 insertions(+), 63 deletions(-) diff --git a/VERSION b/VERSION index 02cb0948..45616fdb 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-07-20 16:47:55) dev +Version 2.00.0 (2012-07-20 17:37:48) dev diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index 102cac82..711f0330 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -392,7 +392,7 @@ def peek(): app = get_app(request.vars.app) filename = '/'.join(request.args) if request.vars.app: - path = abspath(filename, gluon=False) + path = abspath(filename) else: path = apath(filename, r=request) try: @@ -696,7 +696,7 @@ def edit_language(): s = strings[key] (prefix, sep, key) = key.partition('\x01') if sep: - prefix = SPAN(prefix+': ', _style='color: blue;') + prefix = SPAN(prefix+': ', _class='tm_ftag') k = key else: (k, prefix) = (prefix, '') @@ -1034,7 +1034,7 @@ def create_file(): anchor='#'+request.vars.id if request.vars.id else '' if request.vars.app: app = get_app(request.vars.app) - path = abspath(request.vars.location, gluon=False) + path = abspath(request.vars.location) else: app = get_app(name=request.vars.location.split('/')[0]) path = apath(request.vars.location, r=request) diff --git a/applications/admin/static/css/styles.css b/applications/admin/static/css/styles.css index f75bdfe1..8375f145 100644 --- a/applications/admin/static/css/styles.css +++ b/applications/admin/static/css/styles.css @@ -1248,3 +1248,4 @@ color: #222; .error, .error a {color:red} .pluralsform thead td {font-weight:bold; font-size:1.2em; padding-bottom:5px} .pluralsform td {padding-left:5px} +.tm_ftag {color:blue} diff --git a/gluon/contrib/markmin/markmin.html b/gluon/contrib/markmin/markmin.html index d68ae23a..450d599b 100644 --- a/gluon/contrib/markmin/markmin.html +++ b/gluon/contrib/markmin/markmin.html @@ -1,17 +1,22 @@ - - -

Markmin markup language

About

This is a new markup language that we call markmin designed to produce high quality scientific papers and books and also put them online. We provide serializers for html, latex and pdf. It is implemented in the markmin2html function in the markmin2html.py.

Example of usage:

m = "Hello **world** [[link http://web2py.com]]"
+  td.num { text-align: right; }
+  pre { background-color: #E0E0E0; }
+
+Markmin markup language
+
+
+

Markmin markup language

About

This is a new markup language that we call markmin designed to produce high quality scientific papers and books and also put them online. We provide serializers for html, latex and pdf. It is implemented in the markmin2html function in the markmin2html.py.

Example of usage:

m = "Hello **world** [[link http://web2py.com]]"
 from markmin2html import markmin2html
 print markmin2html(m)
 from markmin2latex import markmin2latex
@@ -24,7 +29,7 @@ line 2
   line 3
This item finishes with this paragraph.

Item in sublevel 3 can be continued with paragraphs.

  this is another
 code block
     in the
-  sublevel 3 item
  1. The last item in sublevel 3

This is a continuous paragraph for item 2 in sublevel 2. You can use such structure to create difficult structured documents.

  • item 3 in sublevel 2
    • item 1 in sublevel 2 (new unordered list)
    • item 2 in sublevel 2
    • item 3 in sublevel 2
    1. item 1 in sublevel 2 (new ordered list)
    2. item 2 in sublevel 2
    3. item 3 in sublevle 2
  • item 2 in level 1
  • item 3 in level 1
    • new unordered list (item 1 in level 1)
    • level 2 in level 1
    • level 3 in level 1
    • level 4 in level 1

    This is the last section of the test

    Single paragraph with '----' in it will be turned into separator:


    And this is the last paragraph in the test. Be happy!

    ====================

    Why?

    We wanted a markup language with the following requirements:

    • less than 300 lines of functional code
    • easy to read
    • secure
    • support table, ul, ol, code
    • support html5 video and audio elements (html serialization only)
    • can align images and resize them
    • can specify class for tables and code elements
    • can add anchors
    • does not use _ for markup (since it creates odd behavior)
    • automatically links urls
    • fast
    • easy to extend
    • supports latex and pdf including references
    • allows to describe the markup in the markup (this document is generated from markmin syntax)

    (results depend on text but in average for text ~100K markmin is 30% faster than markdown, for text ~10K it is 10x faster)

    The web2py book published by lulu, for example, was entirely generated with markmin2pdf from the online web2py wiki

    Download

    markmin2html.py and markmin2latex.py are single files and have no web2py dependence. Their license is BSD.

    Examples

    Bold, italic, code and links

    SOURCEOUTPUT
    # titletitle
    ## sectionsection
    ### subsectionsubsection
    **bold**bold
    ''italic''italic
    ~~strikeout~~strikeout
    ``verbatim``verbatim
    ``color with **bold**``:redcolor with bold
    ``many colors``:color[blue:#ffff00]many colors
    http://google.comhttp://google.com
    [[**click** me #myanchor]]click me
    [[click me [extra info] #myanchor popup]]click me

    More on links

    The format is always [[title link]] or [[title [extra] link]]. Notice you can nest bold, italic, strikeout and code inside the link title.

    Anchors

    You can place an anchor anywhere in the text using the syntax [[name]] where name is the name of the anchor. You can then link the anchor with link, i.e. [[link #myanchor]] or link with an extra info, i.e. [[link with an extra info [extra info] #myanchor]].

    Images

    alt-string for the image This paragraph has an image aligned to the right with a width of 200px. Its is placed using the code

    [[alt-string for the image [the image title] http://www.web2py.com/examples/static/web2py_logo.png right 200px]].

    Unordered Lists

    - Dog
    +  sublevel 3 item
    1. The last item in sublevel 3

    This is a continuous paragraph for item 2 in sublevel 2. You can use such structure to create difficult structured documents.

  • item 3 in sublevel 2
    • item 1 in sublevel 2 (new unordered list)
    • item 2 in sublevel 2
    • item 3 in sublevel 2
    1. item 1 in sublevel 2 (new ordered list)
    2. item 2 in sublevel 2
    3. item 3 in sublevle 2
  • item 2 in level 1
  • item 3 in level 1
    • new unordered list (item 1 in level 1)
    • level 2 in level 1
    • level 3 in level 1
    • level 4 in level 1

    This is the last section of the test

    Single paragraph with '----' in it will be turned into separator:


    And this is the last paragraph in the test. Be happy!

    ====================

    Why?

    We wanted a markup language with the following requirements:

    • less than 300 lines of functional code
    • easy to read
    • secure
    • support table, ul, ol, code
    • support html5 video and audio elements (html serialization only)
    • can align images and resize them
    • can specify class for tables and code elements
    • can add anchors
    • does not use _ for markup (since it creates odd behavior)
    • automatically links urls
    • fast
    • easy to extend
    • supports latex and pdf including references
    • allows to describe the markup in the markup (this document is generated from markmin syntax)

    (results depend on text but in average for text ~100K markmin is 30% faster than markdown, for text ~10K it is 10x faster)

    The web2py book published by lulu, for example, was entirely generated with markmin2pdf from the online web2py wiki

    Download

    markmin2html.py and markmin2latex.py are single files and have no web2py dependence. Their license is BSD.

    Examples

    Bold, italic, code and links

    SOURCEOUTPUT
    # titletitle
    ## sectionsection
    ### subsectionsubsection
    **bold**bold
    ''italic''italic
    ~~strikeout~~strikeout
    ``verbatim``verbatim
    ``color with **bold**``:redcolor with bold
    ``many colors``:color[blue:#ffff00]many colors
    http://google.comhttp://google.com
    [[**click** me #myanchor]]click me
    [[click me [extra info] #myanchor popup]]click me

    More on links

    The format is always [[title link]] or [[title [extra] link]]. Notice you can nest bold, italic, strikeout and code inside the link title.

    Anchors

    You can place an anchor anywhere in the text using the syntax [[name]] where name is the name of the anchor. You can then link the anchor with link, i.e. [[link #myanchor]] or link with an extra info, i.e. [[link with an extra info [extra info] #myanchor]].

    Images

    alt-string for the image This paragraph has an image aligned to the right with a width of 200px. Its is placed using the code

    [[alt-string for the image [the image title] http://www.web2py.com/examples/static/web2py_logo.png right 200px]].

    Unordered Lists

    - Dog
     - Cat
     - Mouse

    is rendered as

    • Dog
    • Cat
    • Mouse

    Two new lines between items break the list in two lists.

    Ordered Lists

    + Dog
     + Cat
    @@ -79,4 +84,6 @@ markmin2html(text,{'latex':lambda code: LATEX % code.replace('"','\"')})<
     
     ## References
     
    -- [[mdipierro]] web2py Manual, 3rd Edition, lulu.com

    Caveats

    <ul/>, <ol/>, <code/>, <table/>, <blockquote/>, <h1/>, ..., <h6/> do not have <p>...</p> around them.

    +- [[mdipierro]] web2py Manual, 3rd Edition, lulu.com

    Caveats

    <ul/>, <ol/>, <code/>, <table/>, <blockquote/>, <h1/>, ..., <h6/> do not have <p>...</p> around them.

    + + diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index 2b305608..2c6da1e7 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -667,19 +667,19 @@ def render(text,extra={},allowed={},sep='p',URL=None,environment=None,latex='goo '

    [[probe]]

    ' >>> render(r"\\\\[[probe]]") - '

    \\\\

    ' + '

    \\\\

    ' >>> render(r"\\\\\\[[probe]]") '

    \\\\[[probe]]

    ' >>> render(r"\\\\\\\\[[probe]]") - '

    \\\\\\\\

    ' + '

    \\\\\\\\

    ' >>> render(r"\\\\\\\\\[[probe]]") '

    \\\\\\\\[[probe]]

    ' >>> render(r"\\\\\\\\\\\[[probe]]") - '

    \\\\\\\\\\\\

    ' + '

    \\\\\\\\\\\\

    ' >>> render("``[[ [\\[[probe\]\\]] URL\\[x\\]]]``:red[dummy_params]") 'URL[x]' @@ -725,6 +725,10 @@ def render(text,extra={},allowed={},sep='p',URL=None,environment=None,latex='goo >>> render("**@{probe}**", environment=dict(probe="this is a test")) '

    this is a test

    ' + + >>> render('[[id1 [span **messag** in ''markmin''] ]] ... [[**link** to id [link\\\'s title] #mark1]]') + '

    span messag in markmin ... link to id

    ' + """ text = str(text or '') text = regex_backslash.sub(lambda m: m.group(1).translate(ttab_in), text) @@ -855,7 +859,7 @@ def render(text,extra={},allowed={},sep='p',URL=None,environment=None,latex='goo """ paragraphs in lists """ lent=len(t) if lent>lev: - return parse_list(t, '.', s, 'ul', lev, mtag) + return parse_list(t, '.', s, 'ul', lev, mtag, lineno) elif lentlent: ltags.pop() @@ -986,7 +990,9 @@ def render(text,extra={},allowed={},sep='p',URL=None,environment=None,latex='goo URL, environment, latex, - auto) + auto, + class_prefix, + id_prefix) ) mtag='q' else: @@ -1100,7 +1106,8 @@ def render(text,extra={},allowed={},sep='p',URL=None,environment=None,latex='goo elif p in ('left','right'): style = ' style="float:%s"' % p if p in ('video','audio'): - t = render(t, {}, {}, 'br', URL, environment, latex, auto) + t = render(t, {}, {}, 'br', URL, environment, latex, + auto, class_prefix, id_prefix) return '<%(p)s controls="controls"%(title)s%(width)s>%(t)s' \ % dict(p=p, title=title, width=width, k=k, t=t) alt = ' alt="%s"'%escape(t).replace(META, DISABLED_META) if t else '' @@ -1115,13 +1122,19 @@ def render(text,extra={},allowed={},sep='p',URL=None,environment=None,latex='goo t = t or '' a = escape(a) if a else '' if k: + if k.startswith('#'): + k = '#'+id_prefix+k[1:] k = escape(k) title = ' title="%s"' % a.replace(META, DISABLED_META) if a else '' target = ' target="_blank"' if p == 'popup' else '' - t = render(t, {}, {}, 'br', URL, environment, latex, auto) if t else k + t = render(t, {}, {}, 'br', URL, environment, latex, auto, + class_prefix, id_prefix) if t else k return '%(t)s' \ % dict(k=k, title=title, target=target, t=t) - return '%s' % (escape(t),a) + return '%s' % (escape(id_prefix+t), + render(a, {},{},'br', URL, + environment, latex, auto, + class_prefix, id_prefix)) parts = text.split(LINK) text = parts[0] @@ -1183,21 +1196,36 @@ def markmin2html(text, extra={}, allowed={}, sep='p', auto=True): if __name__ == '__main__': import sys import doctest + from textwrap import dedent + + html=dedent(""" + + + + + %(style)s + %(title)s + + + %(body)s + + """)[1:] + if sys.argv[1:2] == ['-h']: - print """ - """)[1:] - td.num { text-align: right; } - pre { background-color: #E0E0E0; } - - """+markmin2html(__doc__)+'' + print html % dict(title="Markmin markup language", style=style, body=markmin2html(__doc__)) elif sys.argv[1:2] == ['-t']: from timeit import Timer loops=1000 @@ -1208,7 +1236,7 @@ if __name__ == '__main__': elif len(sys.argv) > 1: fargv = open(sys.argv[1],'r') try: - print ''+markmin2html(fargv.read())+'' + print html % dict(title=sys.argv[1], style='', body=markmin2html(fargv.read())) finally: fargv.close() else: diff --git a/gluon/utf8.py b/gluon/utf8.py index e8baf75d..d21efb8f 100644 --- a/gluon/utf8.py +++ b/gluon/utf8.py @@ -15,7 +15,7 @@ import __builtin__ __all__ = ['Utf8'] repr_escape_tab={} -for i in xrange(1,32): repr_escape_tab[i]=ur'\x%02i'%i +for i in range(1,32): repr_escape_tab[i]=ur'\x%02x'%i repr_escape_tab[7]=u'\\a' repr_escape_tab[8]=u'\\b' repr_escape_tab[9]=u'\\t' @@ -71,6 +71,24 @@ def size(string): """ return Utf8(string).__size__() +def truncate(string, length, dots='...'): + """ returns string of length < *length* or truncate + string with adding *dots* suffix to the string's end + + args: + length (int): max length of string + dots (str or unicode): string suffix, when string is cutted + + returns: + (utf8-str): original or cutted string + """ + text = unicode(string, 'utf-8') + dots = unicode(dots, 'utf-8') if isinstance(dots, str) else dots + if len(text) > length: + text = text[:length-len(dots)] + dots + return str.__new__(Utf8, text.encode('utf-8')) + + class Utf8(str): """ Class for utf8 string storing and manipulations @@ -131,23 +149,6 @@ class Utf8(str): else: return "'"+unicode(self, 'utf-8').translate(repr_escape_tab2).encode('utf-8')+"'" - def truncate(self, length, dots='...'): - """ returns string of length < *length* or truncate - string with adding *dots* suffix to the string's end - - args: - length (int): max length of string - dots (str or unicode): string suffix, when string is cutted - - returns: - (utf8-str): original or cutted string - """ - text = unicode(self, 'utf-8') - dots = unicode(dots, 'utf-8') if isinstance(dots, str) else dots - if len(text) > length: - text = text[:length-len(dots)] + dots - return str.__new__(Utf8, text.encode('utf-8')) - def __size__(self): """ length of utf-8 string in bytes """ return str.__len__(self) @@ -419,15 +420,15 @@ if __name__ == '__main__': 'прОБА є prOBE' >>> type(s.swapcase()) - >>> s.truncate(10) + >>> truncate(s, 10) 'ПРоба Є...' - >>> s.truncate(20) + >>> truncate(s, 20) 'ПРоба Є PRobe' - >>> s.truncate(10, '•••') # utf-8 string as *dots* + >>> truncate(s, 10, '•••') # utf-8 string as *dots* 'ПРоба Є•••' - >>> s.truncate(10, u'®') # you can use unicode string as *dots* + >>> truncate(s, 10, u'®') # you can use unicode string as *dots* 'ПРоба Є P®' - >>> type(s.truncate(10)) + >>> type(truncate(s, 10)) >>> Utf8(s.encode('koi8-u'), 'koi8-u') 'ПРоба Є PRobe'