Selaa lähdekoodia

import AsciiDocReader from core as a plugin

Deniz Turgut 10 vuotta sitten
vanhempi
commit
fc3a4d8993

+ 28 - 0
asciidoc_reader/README.rst

@@ -0,0 +1,28 @@
+AsciiDoc Reader
+###############
+
+This plugin allows you to use `AsciiDoc <http://www.methods.co.nz/asciidoc/>`_ 
+to write your posts. File extension should be ``.asc``, ``.adoc``, 
+or ``.asciidoc``.
+
+Dependency
+----------
+
+If you want to use AsciiDoc you need to install it from `source
+<http://www.methods.co.nz/asciidoc/INSTALL.html>`_ or use your operating
+system's package manager.
+
+**Note**: AsciiDoc does not work with Python 3, so you should be using Python 2.
+
+Settings
+--------
+
+========================================  =======================================================
+Setting name (followed by default value)  What does it do?
+========================================  =======================================================
+``ASCIIDOC_OPTIONS = []``                 A list of options to pass to AsciiDoc. See the `manpage
+                                          <http://www.methods.co.nz/asciidoc/manpage.html>`_.
+``ASCIIDOC_BACKEND = 'html5'``            Backend format for output. See the `documentation 
+                                          <http://www.methods.co.nz/asciidoc/userguide.html#X5>`_
+                                          for possible values.
+========================================  =======================================================

+ 1 - 0
asciidoc_reader/__init__.py

@@ -0,0 +1 @@
+from .asciidoc_reader import *

+ 64 - 0
asciidoc_reader/asciidoc_reader.py

@@ -0,0 +1,64 @@
+# -*- coding: utf-8 -*-
+"""
+AsciiDoc Reader
+===============
+
+This plugin allows you to use AsciiDoc to write your posts. 
+File extension should be ``.asc``, ``.adoc``, or ``asciidoc``.
+"""
+
+from pelican.readers import BaseReader
+from pelican.utils import pelican_open
+from pelican import signals
+
+try:
+    # asciidocapi won't import on Py3
+    from .asciidocapi import AsciiDocAPI, AsciiDocError
+    # AsciiDocAPI class checks for asciidoc.py
+    AsciiDocAPI()
+except:
+    asciidoc_enabled = False
+else:
+    asciidoc_enabled = True
+
+
+
+class AsciiDocReader(BaseReader):
+    """Reader for AsciiDoc files"""
+
+    enabled = asciidoc_enabled
+    file_extensions = ['asc', 'adoc', 'asciidoc']
+    default_options = ["--no-header-footer", "-a newline=\\n"]
+    default_backend = 'html5'
+
+    def read(self, source_path):
+        """Parse content and metadata of asciidoc files"""
+        from cStringIO import StringIO
+        with pelican_open(source_path) as source:
+            text = StringIO(source)
+        content = StringIO()
+        ad = AsciiDocAPI()
+
+        options = self.settings.get('ASCIIDOC_OPTIONS', [])
+        options = self.default_options + options
+        for o in options:
+            ad.options(*o.split())
+
+        backend = self.settings.get('ASCIIDOC_BACKEND', self.default_backend)
+        ad.execute(text, content, backend=backend)
+        content = content.getvalue()
+
+        metadata = {}
+        for name, value in ad.asciidoc.document.attributes.items():
+            name = name.lower()
+            metadata[name] = self.process_metadata(name, value)
+        if 'doctitle' in metadata:
+            metadata['title'] = metadata['doctitle']
+        return content, metadata
+
+def add_reader(readers):
+    for ext in AsciiDocReader.file_extensions:
+        readers.reader_classes[ext] = AsciiDocReader
+
+def register():
+    signals.readers_init.connect(add_reader)

+ 257 - 0
asciidoc_reader/asciidocapi.py

@@ -0,0 +1,257 @@
+#!/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,imp
+
+API_VERSION = '0.1.2'
+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')
+        self.cmd = os.path.realpath(cmd)
+        self.__import_asciidoc()
+
+    def __import_asciidoc(self, reload=False):
+        '''
+        Import asciidoc module (script or compiled .pyc).
+        See
+        http://groups.google.com/group/asciidoc/browse_frm/thread/66e7b59d12cd2f91
+        for an explanation of why a seemingly straight-forward job turned out
+        quite complicated.
+        '''
+        if os.path.splitext(self.cmd)[1] in ['.py','.pyc']:
+            sys.path.insert(0, os.path.dirname(self.cmd))
+            try:
+                try:
+                    if reload:
+                        import __builtin__  # Because reload() is shadowed.
+                        __builtin__.reload(self.asciidoc)
+                    else:
+                        import asciidoc
+                        self.asciidoc = asciidoc
+                except ImportError:
+                    raise AsciiDocError('failed to import ' + self.cmd)
+            finally:
+                del sys.path[0]
+        else:
+            # The import statement can only handle .py or .pyc files, have to
+            # use imp.load_source() for scripts with other names.
+            try:
+                imp.load_source('asciidoc', self.cmd)
+                import asciidoc
+                self.asciidoc = asciidoc
+            except ImportError:
+                raise AsciiDocError('failed to import ' + self.cmd)
+        if Version(self.asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION):
+            raise AsciiDocError(
+                'asciidocapi %s requires asciidoc %s or better'
+                % (API_VERSION, MIN_ASCIIDOC_VERSION))
+
+    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]
+        # 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.
+        self.__import_asciidoc(reload=True)
+        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)

+ 60 - 0
asciidoc_reader/test_asciidoc_reader.py

@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+import datetime
+import os
+
+from pelican.readers import Readers
+from pelican.tests.support import unittest, get_settings
+
+from .asciidoc_reader import asciidoc_enabled
+
+CUR_DIR = os.path.dirname(__file__)
+CONTENT_PATH = os.path.join(CUR_DIR, 'test_data')
+
+@unittest.skipUnless(asciidoc_enabled, "asciidoc isn't installed")
+class AsciiDocReaderTest(unittest.TestCase):
+    def read_file(self, path, **kwargs):
+        # Isolate from future API changes to readers.read_file
+        r = Readers(settings=get_settings(**kwargs))
+        return r.read_file(base_path=CONTENT_PATH, path=path)
+
+    def test_article_with_asc_extension(self):
+        # Ensure the asc extension is being processed by the correct reader
+        page = self.read_file(
+            path='article_with_asc_extension.asc')
+        expected = ('<div class="sect1">\n'
+                    '<h2 id="_used_for_pelican_test">'
+                    'Used for pelican test</h2>\n'
+                    '<div class="sectionbody">\n'
+                    '<div class="paragraph">'
+                    '<p>The quick brown fox jumped over '
+                    'the lazy dog&#8217;s back.</p>'
+                    '</div>\n</div>\n</div>\n')
+        self.assertEqual(page.content, expected)
+        expected = {
+            'category': 'Blog',
+            'author': 'Author O. Article',
+            'title': 'Test AsciiDoc File Header',
+            'date': datetime.datetime(2011, 9, 15, 9, 5),
+            'tags': ['Linux', 'Python', 'Pelican'],
+        }
+
+        for key, value in expected.items():
+            self.assertEqual(value, page.metadata[key], key)
+
+    def test_article_with_asc_options(self):
+        # test to ensure the ASCIIDOC_OPTIONS is being used
+        page = self.read_file(path='article_with_asc_options.asc',
+            ASCIIDOC_OPTIONS=["-a revision=1.0.42"])
+        expected = ('<div class="sect1">\n'
+                    '<h2 id="_used_for_pelican_test">'
+                    'Used for pelican test</h2>\n'
+                    '<div class="sectionbody">\n'
+                    '<div class="paragraph">'
+                    '<p>version 1.0.42</p></div>\n'
+                    '<div class="paragraph">'
+                    '<p>The quick brown fox jumped over '
+                    'the lazy dog&#8217;s back.</p>'
+                    '</div>\n</div>\n</div>\n')
+        self.assertEqual(page.content, expected)

+ 12 - 0
asciidoc_reader/test_data/article_with_asc_extension.asc

@@ -0,0 +1,12 @@
+Test AsciiDoc File Header
+=========================
+:Author: Author O. Article
+:Email: <author@nowhere.com>
+:Date: 2011-09-15 09:05
+:Category: Blog
+:Tags: Linux, Python, Pelican
+
+Used for pelican test
+---------------------
+
+The quick brown fox jumped over the lazy dog's back.

+ 9 - 0
asciidoc_reader/test_data/article_with_asc_options.asc

@@ -0,0 +1,9 @@
+Test AsciiDoc File Header
+=========================
+
+Used for pelican test
+---------------------
+
+version {revision}
+
+The quick brown fox jumped over the lazy dog's back.