source: plantumlmacro/trunk/plantuml/macro.py

Last change on this file was 17025, checked in by Ryan J Ollos, 6 years ago

TracPlantUml 2.3dev: Fix usemap attribute not prefixed with #

Patch by dries.decock@...

Fixes #13366.

File size: 7.6 KB
Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2010 Polar Technologies - www.polartech.es
4# Copyright (C) 2010 Alvaro J Iradier <alvaro.iradier@polartech.es>
5# Copyright (C) 2012-2017 Ryan J Ollos <ryan.j.ollos@gmail.com>
6# All rights reserved.
7#
8# This software is licensed as described in the file COPYING, which
9# you should have received as part of this distribution.
10
11import hashlib
12import os
13import subprocess
14import re
15
16from trac.config import Option
17from trac.core import implements
18from trac.util.html import Markup, html as tag
19from trac.util.translation import _
20from trac.versioncontrol.api import NoSuchNode, RepositoryManager
21from trac.web import IRequestHandler
22from trac.wiki.formatter import system_message
23from trac.wiki.macros import WikiMacroBase
24
25img_dir = 'cache/plantuml'
26
27
28class PlantUmlMacro(WikiMacroBase):
29    """
30    A wiki processor that renders PlantUML diagrams in wiki text.
31
32    Example:
33    {{{
34    {{{
35    #!PlantUML
36    @startuml
37    Alice -> Bob: Authentication Reque
38    st
39    Bob --> Alice: Authentication Response
40    Alice -> Bob: Another authentication Request
41    Alice <-- Bob: another authentication Response
42    @enduml
43    }}}
44    }}}
45
46    Results in:
47    {{{
48    #!PlantUML
49    @startuml
50    Alice -> Bob: Authentication Request
51    Bob --> Alice: Authentication Response
52    Alice -> Bob: Another authentication Request
53    Alice <-- Bob: another authentication Response
54    @enduml
55    }}}
56    """
57
58    implements(IRequestHandler)
59
60    plantuml_jar = Option('plantuml', 'plantuml_jar', '',
61        """Path to PlantUML jar file. The jar file can be downloaded from the
62          [http://plantuml.sourceforge.net/download.html PlantUML] site.""")
63
64    java_bin = Option('plantuml', 'java_bin', 'java',
65        """Path to the Java binary file. The default is `java`, which and
66           assumes that the Java binary is on the search path.""")
67
68    def __init__(self):
69        self.abs_img_dir = os.path.join(os.path.abspath(self.env.path),
70                                        img_dir)
71        if not os.path.isdir(self.abs_img_dir):
72            os.makedirs(self.abs_img_dir)
73
74    # WikiMacroBase methods
75
76    def expand_macro(self, formatter, name, content, args=None):
77        if not self.plantuml_jar:
78            return system_message(_("Installation error: plantuml_jar "
79                                    "option not defined in trac.ini"))
80        if not os.path.exists(self.plantuml_jar):
81            return system_message(_("Installation error: plantuml.jar not "
82                                    "found at '%(path)s'",
83                                    path=self.plantuml_jar))
84        filename = os.path.basename(self.plantuml_jar)
85        if not os.path.splitext(filename)[1] == '.jar':
86            return system_message(_("'%(path)s' is not the path of a JAR "
87                                    "file.", path=self.plantuml_jar))
88
89        # Trac 0.12 supports expand_macro(self, formatter, name, content,
90        # args) which allows us to readily differentiate between a WikiProcess
91        # and WikiMacro call. To support Trac 0.11, some additional work is
92        # required.
93        try:
94            args = formatter.code_processor.args
95        except AttributeError:
96            args = None
97        args = args or {}
98
99        path = None
100        if 'path' not in args:  # Could be WikiProcessor or WikiMacro call
101            if content.strip().startswith("@startuml"):
102                path = None
103            else:
104                path = content
105                if not path:
106                    return system_message(_("Path not specified"))
107        elif args:  # WikiProcessor with args
108            path = args.get('path')
109            if not path:
110                return system_message(_("Path not specified"))
111
112        if path:
113            try:
114                markup = self._read_source_from_repos(formatter, path)
115            except NoSuchNode, e:
116                return system_message(e)
117        else:
118            if not content:
119                return system_message(_("No UML text defined"))
120            markup = content.encode('utf-8').strip()
121
122        img_id = hashlib.sha1(markup).hexdigest()
123        if not self._is_img_existing(img_id):
124            self._write_markup_to_file(img_id, markup)
125            cmd = '%s -jar -Djava.awt.headless=true "%s" ' \
126                  '-charset UTF-8 "%s"' % (self.java_bin, self.plantuml_jar,
127                                           self._get_markup_path(img_id))
128            p = subprocess.Popen(cmd, shell=True)
129            stderr = p.wait()
130            if p.returncode != 0:
131                return system_message(_("Error running plantuml: '%(error)s'",
132                                        error=stderr))
133        img_link = formatter.href('plantuml', id=img_id)
134        cmap = self._is_cmapx_existing(img_id) and \
135               self._read_cmapx_from_file(img_id) or ''
136        return Markup(cmap) + tag.img(src=img_link, usemap='#%s_map' % img_id)
137
138    def get_macros(self):
139        yield 'PlantUml'  # WikiMacros syntax
140        yield 'plantuml'  # WikiProcessor syntax
141        yield 'PlantUML'  # deprecated, retained for backward compatibility
142
143    # IRequestHandler methods
144
145    def match_request(self, req):
146        return re.match(r'/plantuml?$', req.path_info)
147
148    def process_request(self, req):
149        img_id = req.args.get('id')
150        img_data = self._read_img_from_file(img_id)
151        req.send(img_data, 'image/png', status=200)
152
153    # Internal methods
154
155    def _get_markup_path(self, img_id):
156        img_path = os.path.join(self.abs_img_dir, img_id)
157        img_path += '.txt'
158        return img_path
159
160    def _get_img_path(self, img_id):
161        img_path = os.path.join(self.abs_img_dir, img_id)
162        img_path += '.png'
163        return img_path
164
165    def _get_cmapx_path(self, img_id):
166        img_path = os.path.join(self.abs_img_dir, img_id)
167        img_path += '.cmapx'
168        return img_path
169
170    def _is_img_existing(self, img_id):
171        img_path = self._get_img_path(img_id)
172        return os.path.isfile(img_path)
173
174    def _is_cmapx_existing(self, img_id):
175        img_path = self._get_cmapx_path(img_id)
176        return os.path.isfile(img_path)
177
178    def _write_markup_to_file(self, img_id, markup):
179        img_path = self._get_markup_path(img_id)
180        open(img_path, 'wb').write(markup)
181
182    def _read_cmapx_from_file(self, img_id):
183        img_path = self._get_cmapx_path(img_id)
184        img_data = open(img_path, 'r').read()
185        return img_data
186
187    def _read_img_from_file(self, img_id):
188        img_path = self._get_img_path(img_id)
189        img_data = open(img_path, 'rb').read()
190        return img_data
191
192    def _read_source_from_repos(self, formatter, src_path):
193        repos_mgr = RepositoryManager(self.env)
194        try:  # 0.12+
195            repos_name, repos, repos_rel_path = \
196                repos_mgr.get_repository_by_path(src_path)
197        except AttributeError:  # 0.11
198            repos = repos_mgr.get_repository(formatter.req.authname)
199        path, rev = _split_path(repos_rel_path)
200        if repos and repos.has_node(path, rev):
201            node = repos.get_node(path, rev)
202            content = node.get_content().read()
203        else:
204            if rev is None and repos:
205                rev = repos.get_youngest_rev()
206            raise NoSuchNode(path, rev)
207
208        return content
209
210
211def _split_path(fqpath):
212    if '@' in fqpath:
213        path, rev = fqpath.split('@', 1)
214    else:
215        path, rev = fqpath, None
216    return path, rev
Note: See TracBrowser for help on using the repository browser.