boom, asciidoc
Chris Wanstrath chris@ozmm.org
Fri, 30 Oct 2009 23:13:21 -0700
4 files changed,
379 insertions(+),
0 deletions(-)
A
lib/github/commands/asciidoc2html
@@ -0,0 +1,119 @@
+#!/usr/bin/env python + +"""A small wrapper file for parsing AsciiDoc files at Github.""" + +__author__ = "Devin Weaver" +__copyright__ = "Copyright (C) 2009 Devin Weaver" +__license__ = "Public Domain" +__version__ = "0.1" + +""" +github_asciidoc.py +------------------ + +This is a wrapper file for parsing AsciiDoc files at github. It wraps the +current AsciiDoc API. + +AsciiDoc specifications suggest using the file extension of `.txt` however this +causes conflict because there is no way to determine if a text file is an +AsciiDoc or not without pre-processing the file. This gives us two simple +options: + +1. **Parse all text files**. We could have all files ending in `.txt` or + ``README.txt`` be parsed through AsciiDoc. It will print pretty text fine + even if it isn't formatted as such. However this could be *not what the user + expects*. +2. **Pick a unique extension**. We could pick a unique extension (i.e. + `.asciidoc`) to prevent clashing. Although not directly suggested by the + author of AsciiDoc there is no standard or practice to the contrary. + +Option two is recommended by myself. + +Requirements +~~~~~~~~~~~~ + +The AsciiDoc API comes in two parts. The first is the system installation of +AsciiDoc which has a simple install_. The second part is the API script. You +can either copy this to the current directory or the application's lib folder. +There is more information on the `API page`_ + +The `re` package is imported here for the purpose to accomplish E-Mail address +cloaking. AsciiDoc does not offer it's own cloaking algorithm like docutils +does. So I made a simple one here to do the same. **If the expense of regex's +is too high it can be easily commented out.** + +.. tip:: + AsciiDoc by default runs in *safe mode* which means it will not include + external files that are **not** in the same directory as the `infile`. + However since we use a StringIO through the API it should be based on the + current working directory. + +.. _install: http://www.methods.co.nz/asciidoc/userguide.html +.. _API page: http://www.methods.co.nz/asciidoc/asciidocapi.html +""" + +try: + import locale + locale.setlocale(locale.LC_ALL, '') +except: + pass + +import sys +import cStringIO # faster then StringIO +from asciidocapi import AsciiDocAPI +from asciidocapi import AsciiDocError +import re # only needed to simulate cloak_email_addresses + +def main(): + """ + Parses the given AsciiDoc file or the redirected string input and returns + the HTML body. + + Usage: asciidoc2html < README.rst + asciidoc2html README.rst + """ + try: + text = open(sys.argv[1], 'r').read() + except IOError: # given filename could not be found + return '' + except IndexError: # no filename given + text = sys.stdin.read() + + infile = cStringIO.StringIO(text) + outfile = cStringIO.StringIO() + asciidoc = AsciiDocAPI() + asciidoc.options('-s') + + try: + asciidoc.execute(infile, outfile, 'xhtml11') + except AsciiDocError, strerror: + str = "%s" % (strerror) + str = str.replace("&", "&") # Must be done first + str = str.replace("<", "%lt;") + str = str.replace(">", "%gt;") + outfile.write ("<blockquote><strong>AsciiDoc ERROR: %s</strong></blockquote>" % (str)) + + """ + Cloak email addresses + + AsciiDoc API does not have a `cloak_email_addresses` option. We can do the + same with a set of regex but that can be expensive. Keep section commented + to disable. So ``abc@mail.example.com`` becomes: + + ----------- + <a class="reference" href="mailto:abc%40mail.example.org"> + abc<span>@</span>mail<span>.</span>example<span>.</span>org</a> + ----------- + """ + def mangleEmail(matches): + email1 = "%s%40%s" % (matches.group(1), matches.group(2)) + email1 = email1.replace(".", ".") + email2 = "%s<span>@</span>%s" % (matches.group(1), matches.group(2)) + email2 = email2.replace(".", "<span>.</span>") + return "<a class=\"reference\" href=\"mailto:%s\">%s</a>" % (email1, email2) + + return re.sub(r'<a href="mailto:([^@]+)@([^@]+)">([^@]+)@([^@]+)</a>', mangleEmail, outfile.getvalue()) + #return outfile.getvalue() + +if __name__ == '__main__': + print main()
A
lib/github/commands/asciidocapi.py
@@ -0,0 +1,240 @@
+#!/usr/bin/env python +""" +asciidocapi - AsciiDoc API wrapper class. + +The AsciiDocAPI class provides an API for executing asciidoc. Minimal example +compiles `mydoc.txt` to `mydoc.html`: + + import asciidocapi + asciidoc = asciidocapi.AsciiDocAPI() + asciidoc.execute('mydoc.txt') + +- Full documentation in asciidocapi.txt. +- See the doctests below for more examples. + +Doctests: + +1. Check execution: + + >>> import StringIO + >>> infile = StringIO.StringIO('Hello *{author}*') + >>> outfile = StringIO.StringIO() + >>> asciidoc = AsciiDocAPI() + >>> asciidoc.options('--no-header-footer') + >>> asciidoc.attributes['author'] = 'Joe Bloggs' + >>> asciidoc.execute(infile, outfile, backend='html4') + >>> print outfile.getvalue() + <p>Hello <strong>Joe Bloggs</strong></p> + + >>> asciidoc.attributes['author'] = 'Bill Smith' + >>> infile = StringIO.StringIO('Hello _{author}_') + >>> outfile = StringIO.StringIO() + >>> asciidoc.execute(infile, outfile, backend='docbook') + >>> print outfile.getvalue() + <simpara>Hello <emphasis>Bill Smith</emphasis></simpara> + +2. Check error handling: + + >>> import StringIO + >>> asciidoc = AsciiDocAPI() + >>> infile = StringIO.StringIO('---------') + >>> outfile = StringIO.StringIO() + >>> asciidoc.execute(infile, outfile) + Traceback (most recent call last): + File "<stdin>", line 1, in <module> + File "asciidocapi.py", line 189, in execute + raise AsciiDocError(self.messages[-1]) + AsciiDocError: ERROR: <stdin>: line 1: [blockdef-listing] missing closing delimiter + + +Copyright (C) 2009 Stuart Rackham. Free use of this software is granted +under the terms of the GNU General Public License (GPL). + +""" + +import sys,os,re + +API_VERSION = '0.1.1' +MIN_ASCIIDOC_VERSION = '8.4.1' # Minimum acceptable AsciiDoc version. + + +def find_in_path(fname, path=None): + """ + Find file fname in paths. Return None if not found. + """ + if path is None: + path = os.environ.get('PATH', '') + for dir in path.split(os.pathsep): + fpath = os.path.join(dir, fname) + if os.path.isfile(fpath): + return fpath + else: + return None + + +class AsciiDocError(Exception): + pass + + +class Options(object): + """ + Stores asciidoc(1) command options. + """ + def __init__(self, values=[]): + self.values = values[:] + def __call__(self, name, value=None): + """Shortcut for append method.""" + self.append(name, value) + def append(self, name, value=None): + if type(value) in (int,float): + value = str(value) + self.values.append((name,value)) + + +class Version(object): + """ + Parse and compare AsciiDoc version numbers. Instance attributes: + + string: String version number '<major>.<minor>[.<micro>][suffix]'. + major: Integer major version number. + minor: Integer minor version number. + micro: Integer micro version number. + suffix: Suffix (begins with non-numeric character) is ignored when + comparing. + + Doctest examples: + + >>> Version('8.2.5') < Version('8.3 beta 1') + True + >>> Version('8.3.0') == Version('8.3. beta 1') + True + >>> Version('8.2.0') < Version('8.20') + True + >>> Version('8.20').major + 8 + >>> Version('8.20').minor + 20 + >>> Version('8.20').micro + 0 + >>> Version('8.20').suffix + '' + >>> Version('8.20 beta 1').suffix + 'beta 1' + + """ + def __init__(self, version): + self.string = version + reo = re.match(r'^(\d+)\.(\d+)(\.(\d+))?\s*(.*?)\s*$', self.string) + if not reo: + raise ValueError('invalid version number: %s' % self.string) + groups = reo.groups() + self.major = int(groups[0]) + self.minor = int(groups[1]) + self.micro = int(groups[3] or '0') + self.suffix = groups[4] or '' + def __cmp__(self, other): + result = cmp(self.major, other.major) + if result == 0: + result = cmp(self.minor, other.minor) + if result == 0: + result = cmp(self.micro, other.micro) + return result + + +class AsciiDocAPI(object): + """ + AsciiDoc API class. + """ + def __init__(self, asciidoc_py=None): + """ + Locate and import asciidoc.py. + Initialize instance attributes. + """ + self.options = Options() + self.attributes = {} + self.messages = [] + # Search for the asciidoc command file. + # Try ASCIIDOC_PY environment variable first. + cmd = os.environ.get('ASCIIDOC_PY') + if cmd: + if not os.path.isfile(cmd): + raise AsciiDocError('missing ASCIIDOC_PY file: %s' % cmd) + elif asciidoc_py: + # Next try path specified by caller. + cmd = asciidoc_py + if not os.path.isfile(cmd): + raise AsciiDocError('missing file: %s' % cmd) + else: + # Try shell search paths. + for fname in ['asciidoc.py','asciidoc.pyc','asciidoc']: + cmd = find_in_path(fname) + if cmd: break + else: + # Finally try current working directory. + for cmd in ['asciidoc.py','asciidoc.pyc','asciidoc']: + if os.path.isfile(cmd): break + else: + raise AsciiDocError('failed to locate asciidoc.py[c]') + cmd = os.path.realpath(cmd) + if os.path.splitext(cmd)[1] not in ['.py','.pyc']: + raise AsciiDocError('invalid Python module name: %s' % cmd) + sys.path.insert(0, os.path.dirname(cmd)) + try: + try: + import asciidoc + except ImportError: + raise AsciiDocError('failed to import asciidoc') + finally: + del sys.path[0] + if Version(asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION): + raise AsciiDocError( + 'asciidocapi %s requires asciidoc %s or better' + % (API_VERSION, MIN_ASCIIDOC_VERSION)) + self.asciidoc = asciidoc + self.cmd = cmd + + def execute(self, infile, outfile=None, backend=None): + """ + Compile infile to outfile using backend format. + infile can outfile can be file path strings or file like objects. + """ + self.messages = [] + opts = Options(self.options.values) + if outfile is not None: + opts('--out-file', outfile) + if backend is not None: + opts('--backend', backend) + for k,v in self.attributes.items(): + if v == '' or k[-1] in '!@': + s = k + elif v is None: # A None value undefines the attribute. + s = k + '!' + else: + s = '%s=%s' % (k,v) + opts('--attribute', s) + args = [infile] + sys.path.insert(0, os.path.dirname(self.cmd)) + try: + # The AsciiDoc command was designed to process source text then + # exit, there are globals and statics in asciidoc.py that have + # to be reinitialized before each run -- hence the reload. + reload(self.asciidoc) + finally: + del sys.path[0] + try: + try: + self.asciidoc.execute(self.cmd, opts.values, args) + finally: + self.messages = self.asciidoc.messages[:] + except SystemExit, e: + if e.code: + raise AsciiDocError(self.messages[-1]) + + +if __name__ == "__main__": + """ + Run module doctests. + """ + import doctest + options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS + doctest.testmod(optionflags=options)
M
lib/github/markups.rb
→
lib/github/markups.rb
@@ -11,3 +11,5 @@ GitHub::Markup::RDoc.new(content).to_html
end command(:rest2html, /rest|rst/) + +command(:asciidoc2html, /asciidoc/)
M
test/markup_test.rb
→
test/markup_test.rb
@@ -52,6 +52,24 @@ <li>Two\n\n</li>
</ul> output + test 'README.asciidoc', <<-input, <<-output +* One +* Two +input +<div class="ulist"><ul>\r +<li>\r +<p>\r +One\r +</p>\r +</li>\r +<li>\r +<p>\r +Two\r +</p>\r +</li>\r +</ul></div>\r\n +output + test 'README.rst', <<-input, <<-output 1. Blah blah ``code`` blah