소스 검색

Updated AsciiDoc reader to support both legacy and asciidoctor CLI utilities.

jeffrimko 7 년 전
부모
커밋
ad6d40715e

+ 10 - 10
asciidoc_reader/README.rst

@@ -1,18 +1,19 @@
 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``, 
+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.
+There are two command line utilities commonly used to render AsciiDoc:
+``asciidoc`` and ``asciidoctor``. One of the two will need to be installed and
+on the PATH.
 
-**Note**: AsciiDoc does not work with Python 3, so you should be using Python 2.
+**Note**: The ``asciidoctor`` utility is recommended since the original
+``asciidoc`` is no longer maintained.
 
 Settings
 --------
@@ -20,11 +21,10 @@ Settings
 ========================================  =======================================================
 Setting name (followed by default value)  What does it do?
 ========================================  =======================================================
+``ASCIIDOC_CMD = asciidoc``               Selects which utility to use for rendering. Will
+                                          autodetect utility if not provided.
 ``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.
 ========================================  =======================================================
 
 Example file header
@@ -45,5 +45,5 @@ Following the `example <https://github.com/getpelican/pelican/blob/master/docs/c
   :summary: Short version for index and feeds
 
   == title level 2
-    
+
   and so on...

+ 58 - 38
asciidoc_reader/asciidoc_reader.py

@@ -3,61 +3,81 @@
 AsciiDoc Reader
 ===============
 
-This plugin allows you to use AsciiDoc to write your posts. 
+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
-import six
+import os
+import re
+import subprocess
 
-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
+def call(cmd):
+    """Calls a CLI command and returns the stdout as string."""
+    return subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True).communicate()[0].decode('utf-8')
 
+def default():
+    """Attempt to find the default AsciiDoc utility."""
+    for cmd in ALLOWED_CMDS:
+        if len(call(cmd + " --help")):
+            return cmd
 
+ALLOWED_CMDS = ["asciidoc", "asciidoctor"]
+
+ENABLED = None != default()
 
 class AsciiDocReader(BaseReader):
-    """Reader for AsciiDoc files"""
+    """Reader for AsciiDoc files."""
 
-    enabled = asciidoc_enabled
+    enabled = ENABLED
     file_extensions = ['asc', 'adoc', 'asciidoc']
-    default_options = ["--no-header-footer", "-a newline=\\n"]
-    default_backend = 'html5'
+    default_options = ['--no-header-footer']
 
     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.encode('utf8'))
-        content = StringIO()
-        ad = AsciiDocAPI()
-
-        options = self.settings.get('ASCIIDOC_OPTIONS', [])
-        options = self.default_options + options
-        for o in options:
-            ad.options(*o.split())
+        """Parse content and metadata of AsciiDoc files."""
+        cmd = self._get_cmd()
+        content = ""
+        if cmd:
+            optlist = self.settings.get('ASCIIDOC_OPTIONS', []) + self.default_options
+            options = " ".join(optlist)
+            content = call("%s %s -o - %s" % (cmd, options, source_path))
+        metadata = self._read_metadata(source_path)
+        return content, metadata
 
-        backend = self.settings.get('ASCIIDOC_BACKEND', self.default_backend)
-        ad.execute(text, content, backend=backend)
-        content = content.getvalue().decode('utf8')
+    def _get_cmd(self):
+        """Returns the AsciiDoc utility command to use for rendering or None if
+        one cannot be found."""
+        if self.settings.get('ASCIIDOC_CMD') in ALLOWED_CMDS:
+            return self.settings.get('ASCIIDOC_CMD')
+        return default()
 
+    def _read_metadata(self, source_path):
+        """Parses the AsciiDoc file at the given `source_path` and returns found
+        metadata."""
         metadata = {}
-        for name, value in ad.asciidoc.document.attributes.items():
-            if value is None:
-                continue
-            name = name.lower()
-            metadata[name] = self.process_metadata(name, six.text_type(value))
-        if 'doctitle' in metadata:
-            metadata['title'] = metadata['doctitle']
-        return content, metadata
+        with open(source_path) as fi:
+            prev = ""
+            for line in fi.readlines():
+                # Parse for doc title.
+                if 'title' not in metadata.keys():
+                    title = ""
+                    if line.startswith("= "):
+                        title = line[2:].strip()
+                    elif line.count("=") == len(prev.strip()):
+                        title = prev.strip()
+                    if title:
+                        metadata['title'] = self.process_metadata('title', title)
+
+                # Parse for other metadata.
+                regexp = re.compile(r"^:[A-z]+:\s*[A-z0-9]")
+                if regexp.search(line):
+                    toks = line.split(":", 2)
+                    key = toks[1].strip().lower()
+                    val = toks[2].strip()
+                    metadata[key] = self.process_metadata(key, val)
+                prev = line
+        return metadata
 
 def add_reader(readers):
     for ext in AsciiDocReader.file_extensions:

+ 0 - 257
asciidoc_reader/asciidocapi.py

@@ -1,257 +0,0 @@
-#!/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)

+ 17 - 14
asciidoc_reader/test_asciidoc_reader.py

@@ -7,12 +7,12 @@ import os
 from pelican.readers import Readers
 from pelican.tests.support import unittest, get_settings
 
-from .asciidoc_reader import asciidoc_enabled
+from .asciidoc_reader import ENABLED
 
 CUR_DIR = os.path.dirname(__file__)
 CONTENT_PATH = os.path.join(CUR_DIR, 'test_data')
 
-@unittest.skipUnless(asciidoc_enabled, "asciidoc isn't installed")
+@unittest.skipUnless(ENABLED, "asciidoc isn't installed")
 class AsciiDocReaderTest(unittest.TestCase):
     def read_file(self, path, **kwargs):
         # Isolate from future API changes to readers.read_file
@@ -23,15 +23,17 @@ class AsciiDocReaderTest(unittest.TestCase):
         # 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'
+        expected = ('<div class="sect1">'
                     '<h2 id="_used_for_pelican_test">'
-                    'Used for pelican test</h2>\n'
-                    '<div class="sectionbody">\n'
+                    'Used for pelican test</h2>'
+                    '<div class="sectionbody">'
                     '<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)
+                    '</div></div></div>')
+        actual = "".join(page.content.splitlines())
+        expected = "".join(expected.splitlines())
+        self.assertEqual(actual, expected)
         expected = {
             'category': 'Blog',
             'author': 'Author O. Article',
@@ -39,7 +41,6 @@ class AsciiDocReaderTest(unittest.TestCase):
             'date': datetime.datetime(2011, 9, 15, 9, 5),
             'tags': ['Linux', 'Python', 'Pelican'],
         }
-
         for key, value in expected.items():
             self.assertEqual(value, page.metadata[key], (
                 'Metadata attribute \'%s\' does not match expected value.\n'
@@ -50,17 +51,19 @@ class AsciiDocReaderTest(unittest.TestCase):
         # 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'
+        expected = ('<div class="sect1">'
                     '<h2 id="_used_for_pelican_test">'
-                    'Used for pelican test</h2>\n'
-                    '<div class="sectionbody">\n'
+                    'Used for pelican test</h2>'
+                    '<div class="sectionbody">'
                     '<div class="paragraph">'
-                    '<p>version 1.0.42</p></div>\n'
+                    '<p>version 1.0.42</p></div>'
                     '<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)
+                    '</div></div></div>')
+        actual = "".join(page.content.splitlines())
+        expected = "".join(expected.splitlines())
+        self.assertEqual(actual, expected)
 
 
 if __name__ == '__main__':

+ 1 - 2
asciidoc_reader/test_data/article_with_asc_extension.asc

@@ -6,7 +6,6 @@ Test AsciiDoc File Header
 :Category: Blog
 :Tags: Linux, Python, Pelican
 
-Used for pelican test
----------------------
+== Used for pelican test
 
 The quick brown fox jumped over the lazy dog's back.

+ 2 - 4
asciidoc_reader/test_data/article_with_asc_options.asc

@@ -1,8 +1,6 @@
-Test AsciiDoc File Header
-=========================
+= Test AsciiDoc File Header
 
-Used for pelican test
----------------------
+== Used for pelican test
 
 version {revision}