source: rtems-tools/doc/asciidoc/asciidocapi.py @ f91e023

4.104.115
Last change on this file since f91e023 was f91e023, checked in by Chris Johns <chrisj@…>, on 02/17/14 at 07:04:46

Add the documentation.

  • Property mode set to 100644
File size: 8.2 KB
Line 
1#!/usr/bin/env python
2"""
3asciidocapi - AsciiDoc API wrapper class.
4
5The AsciiDocAPI class provides an API for executing asciidoc. Minimal example
6compiles `mydoc.txt` to `mydoc.html`:
7
8  import asciidocapi
9  asciidoc = asciidocapi.AsciiDocAPI()
10  asciidoc.execute('mydoc.txt')
11
12- Full documentation in asciidocapi.txt.
13- See the doctests below for more examples.
14
15Doctests:
16
171. Check execution:
18
19   >>> import StringIO
20   >>> infile = StringIO.StringIO('Hello *{author}*')
21   >>> outfile = StringIO.StringIO()
22   >>> asciidoc = AsciiDocAPI()
23   >>> asciidoc.options('--no-header-footer')
24   >>> asciidoc.attributes['author'] = 'Joe Bloggs'
25   >>> asciidoc.execute(infile, outfile, backend='html4')
26   >>> print outfile.getvalue()
27   <p>Hello <strong>Joe Bloggs</strong></p>
28
29   >>> asciidoc.attributes['author'] = 'Bill Smith'
30   >>> infile = StringIO.StringIO('Hello _{author}_')
31   >>> outfile = StringIO.StringIO()
32   >>> asciidoc.execute(infile, outfile, backend='docbook')
33   >>> print outfile.getvalue()
34   <simpara>Hello <emphasis>Bill Smith</emphasis></simpara>
35
362. Check error handling:
37
38   >>> import StringIO
39   >>> asciidoc = AsciiDocAPI()
40   >>> infile = StringIO.StringIO('---------')
41   >>> outfile = StringIO.StringIO()
42   >>> asciidoc.execute(infile, outfile)
43   Traceback (most recent call last):
44     File "<stdin>", line 1, in <module>
45     File "asciidocapi.py", line 189, in execute
46       raise AsciiDocError(self.messages[-1])
47   AsciiDocError: ERROR: <stdin>: line 1: [blockdef-listing] missing closing delimiter
48
49
50Copyright (C) 2009 Stuart Rackham. Free use of this software is granted
51under the terms of the GNU General Public License (GPL).
52
53"""
54
55import sys,os,re,imp
56
57API_VERSION = '0.1.2'
58MIN_ASCIIDOC_VERSION = '8.4.1'  # Minimum acceptable AsciiDoc version.
59
60
61def find_in_path(fname, path=None):
62    """
63    Find file fname in paths. Return None if not found.
64    """
65    if path is None:
66        path = os.environ.get('PATH', '')
67    for dir in path.split(os.pathsep):
68        fpath = os.path.join(dir, fname)
69        if os.path.isfile(fpath):
70            return fpath
71    else:
72        return None
73
74
75class AsciiDocError(Exception):
76    pass
77
78
79class Options(object):
80    """
81    Stores asciidoc(1) command options.
82    """
83    def __init__(self, values=[]):
84        self.values = values[:]
85    def __call__(self, name, value=None):
86        """Shortcut for append method."""
87        self.append(name, value)
88    def append(self, name, value=None):
89        if type(value) in (int,float):
90            value = str(value)
91        self.values.append((name,value))
92
93
94class Version(object):
95    """
96    Parse and compare AsciiDoc version numbers. Instance attributes:
97
98    string: String version number '<major>.<minor>[.<micro>][suffix]'.
99    major:  Integer major version number.
100    minor:  Integer minor version number.
101    micro:  Integer micro version number.
102    suffix: Suffix (begins with non-numeric character) is ignored when
103            comparing.
104
105    Doctest examples:
106
107    >>> Version('8.2.5') < Version('8.3 beta 1')
108    True
109    >>> Version('8.3.0') == Version('8.3. beta 1')
110    True
111    >>> Version('8.2.0') < Version('8.20')
112    True
113    >>> Version('8.20').major
114    8
115    >>> Version('8.20').minor
116    20
117    >>> Version('8.20').micro
118    0
119    >>> Version('8.20').suffix
120    ''
121    >>> Version('8.20 beta 1').suffix
122    'beta 1'
123
124    """
125    def __init__(self, version):
126        self.string = version
127        reo = re.match(r'^(\d+)\.(\d+)(\.(\d+))?\s*(.*?)\s*$', self.string)
128        if not reo:
129            raise ValueError('invalid version number: %s' % self.string)
130        groups = reo.groups()
131        self.major = int(groups[0])
132        self.minor = int(groups[1])
133        self.micro = int(groups[3] or '0')
134        self.suffix = groups[4] or ''
135    def __cmp__(self, other):
136        result = cmp(self.major, other.major)
137        if result == 0:
138            result = cmp(self.minor, other.minor)
139            if result == 0:
140                result = cmp(self.micro, other.micro)
141        return result
142
143
144class AsciiDocAPI(object):
145    """
146    AsciiDoc API class.
147    """
148    def __init__(self, asciidoc_py=None):
149        """
150        Locate and import asciidoc.py.
151        Initialize instance attributes.
152        """
153        self.options = Options()
154        self.attributes = {}
155        self.messages = []
156        # Search for the asciidoc command file.
157        # Try ASCIIDOC_PY environment variable first.
158        cmd = os.environ.get('ASCIIDOC_PY')
159        if cmd:
160            if not os.path.isfile(cmd):
161                raise AsciiDocError('missing ASCIIDOC_PY file: %s' % cmd)
162        elif asciidoc_py:
163            # Next try path specified by caller.
164            cmd = asciidoc_py
165            if not os.path.isfile(cmd):
166                raise AsciiDocError('missing file: %s' % cmd)
167        else:
168            # Try shell search paths.
169            for fname in ['asciidoc.py','asciidoc.pyc','asciidoc']:
170                cmd = find_in_path(fname)
171                if cmd: break
172            else:
173                # Finally try current working directory.
174                for cmd in ['asciidoc.py','asciidoc.pyc','asciidoc']:
175                    if os.path.isfile(cmd): break
176                else:
177                    raise AsciiDocError('failed to locate asciidoc')
178        self.cmd = os.path.realpath(cmd)
179        self.__import_asciidoc()
180
181    def __import_asciidoc(self, reload=False):
182        '''
183        Import asciidoc module (script or compiled .pyc).
184        See
185        http://groups.google.com/group/asciidoc/browse_frm/thread/66e7b59d12cd2f91
186        for an explanation of why a seemingly straight-forward job turned out
187        quite complicated.
188        '''
189        if os.path.splitext(self.cmd)[1] in ['.py','.pyc']:
190            sys.path.insert(0, os.path.dirname(self.cmd))
191            try:
192                try:
193                    if reload:
194                        import __builtin__  # Because reload() is shadowed.
195                        __builtin__.reload(self.asciidoc)
196                    else:
197                        import asciidoc
198                        self.asciidoc = asciidoc
199                except ImportError:
200                    raise AsciiDocError('failed to import ' + self.cmd)
201            finally:
202                del sys.path[0]
203        else:
204            # The import statement can only handle .py or .pyc files, have to
205            # use imp.load_source() for scripts with other names.
206            try:
207                imp.load_source('asciidoc', self.cmd)
208                import asciidoc
209                self.asciidoc = asciidoc
210            except ImportError:
211                raise AsciiDocError('failed to import ' + self.cmd)
212        if Version(self.asciidoc.VERSION) < Version(MIN_ASCIIDOC_VERSION):
213            raise AsciiDocError(
214                'asciidocapi %s requires asciidoc %s or better'
215                % (API_VERSION, MIN_ASCIIDOC_VERSION))
216
217    def execute(self, infile, outfile=None, backend=None):
218        """
219        Compile infile to outfile using backend format.
220        infile can outfile can be file path strings or file like objects.
221        """
222        self.messages = []
223        opts = Options(self.options.values)
224        if outfile is not None:
225            opts('--out-file', outfile)
226        if backend is not None:
227            opts('--backend', backend)
228        for k,v in self.attributes.items():
229            if v == '' or k[-1] in '!@':
230                s = k
231            elif v is None: # A None value undefines the attribute.
232                s = k + '!'
233            else:
234                s = '%s=%s' % (k,v)
235            opts('--attribute', s)
236        args = [infile]
237        # The AsciiDoc command was designed to process source text then
238        # exit, there are globals and statics in asciidoc.py that have
239        # to be reinitialized before each run -- hence the reload.
240        self.__import_asciidoc(reload=True)
241        try:
242            try:
243                self.asciidoc.execute(self.cmd, opts.values, args)
244            finally:
245                self.messages = self.asciidoc.messages[:]
246        except SystemExit, e:
247            if e.code:
248                raise AsciiDocError(self.messages[-1])
249
250
251if __name__ == "__main__":
252    """
253    Run module doctests.
254    """
255    import doctest
256    options = doctest.NORMALIZE_WHITESPACE + doctest.ELLIPSIS
257    doctest.testmod(optionflags=options)
Note: See TracBrowser for help on using the repository browser.