├── src └── actdiag │ ├── tests │ ├── VLGothic │ │ ├── VL-Gothic-Regular.ttf │ │ ├── LICENSE_J.mplus │ │ ├── LICENSE_E.mplus │ │ ├── LICENSE │ │ └── LICENSE.en │ ├── diagrams │ │ └── simple.diag │ ├── test_generate_diagram.py │ └── test_rst_directives.py │ ├── utils │ ├── __init__.py │ └── rst │ │ ├── __init__.py │ │ ├── nodes.py │ │ └── directives.py │ ├── plugins │ ├── __init__.py │ └── autolane.py │ ├── __init__.py │ ├── command.py │ ├── elements.py │ ├── drawer.py │ ├── metrics.py │ ├── parser.py │ └── builder.py ├── .gitignore ├── MANIFEST.in ├── setup.cfg ├── tox.ini ├── .github └── workflows │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── README.rst ├── actdiag.1 ├── setup.py ├── CHANGES.rst └── LICENSE /src/actdiag/tests/VLGothic/VL-Gothic-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blockdiag/actdiag/HEAD/src/actdiag/tests/VLGothic/VL-Gothic-Regular.ttf -------------------------------------------------------------------------------- /src/actdiag/tests/diagrams/simple.diag: -------------------------------------------------------------------------------- 1 | { 2 | lane { 3 | A; C; 4 | } 5 | lane { 6 | B; 7 | } 8 | 9 | A -> B -> C; 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg 3 | *.egg-info 4 | *.so 5 | *.swp 6 | 7 | .cache/ 8 | .mypy_cache/ 9 | .pytest_cache/ 10 | TAGS 11 | .tags 12 | .tox/ 13 | .coverage 14 | .DS_Store 15 | 16 | env/ 17 | _build/ 18 | dist/ 19 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include CHANGES.rst 2 | include MANIFEST.in 3 | include README.rst 4 | include LICENSE 5 | include actdiag.1 6 | include tox.ini 7 | include src/actdiag/tests/VLGothic/* 8 | recursive-include src *.py *.diag *.gif 9 | 10 | exclude .drone.io.sh 11 | exclude examples/update.sh 12 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [egg_info] 2 | ;tag_build = dev 3 | 4 | [build] 5 | build-base = _build 6 | 7 | [sdist] 8 | formats = gztar 9 | 10 | [check] 11 | strict = 1 12 | restructuredtext = 1 13 | 14 | [flake8] 15 | ignore = W504 16 | max-line-length = 120 17 | copyright-check = True 18 | 19 | [isort] 20 | -------------------------------------------------------------------------------- /src/actdiag/tests/VLGothic/LICENSE_J.mplus: -------------------------------------------------------------------------------- 1 | M+ FONTS Copyright (C) 2002-2012 M+ FONTS PROJECT 2 | 3 | - 4 | 5 | LICENSE_J 6 | 7 | 8 | 9 | 10 | これらのフォントはフリー(自由な)ソフトウエアです。 11 | あらゆる改変の有無に関わらず、また商業的な利用であっても、自由にご利用、 12 | 複製、再配布することができますが、全て無保証とさせていただきます。 13 | 14 | 15 | http://mplus-fonts.sourceforge.jp/ 16 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist=py37,py38,py39,flake8,blockdiag_dev 3 | 4 | [testenv] 5 | usedevelop = True 6 | extras = 7 | testing 8 | passenv= 9 | ALL_TESTS 10 | commands= 11 | nosetests 12 | 13 | [testenv:flake8] 14 | description = 15 | Run style checks. 16 | extras = 17 | testing 18 | commands = 19 | flake8 src 20 | 21 | [testenv:blockdiag_dev] 22 | deps= 23 | git+https://github.com/blockdiag/blockdiag 24 | -------------------------------------------------------------------------------- /src/actdiag/tests/VLGothic/LICENSE_E.mplus: -------------------------------------------------------------------------------- 1 | M+ FONTS Copyright (C) 2002-2012 M+ FONTS PROJECT 2 | 3 | - 4 | 5 | LICENSE_E 6 | 7 | 8 | 9 | 10 | These fonts are free software. 11 | Unlimited permission is granted to use, copy, and distribute them, with 12 | or without modification, either commercially or noncommercially. 13 | THESE FONTS ARE PROVIDED "AS IS" WITHOUT WARRANTY. 14 | 15 | 16 | http://mplus-fonts.sourceforge.jp/mplus-outline-fonts/ 17 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout code 9 | uses: actions/checkout@v2 10 | with: 11 | fetch-depth: 1 12 | 13 | - name: Setup python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.7 17 | 18 | - name: Install tox and any other packages for test 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install tox 22 | 23 | - name: Run tox 24 | run: tox -e flake8 25 | -------------------------------------------------------------------------------- /src/actdiag/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | -------------------------------------------------------------------------------- /src/actdiag/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | -------------------------------------------------------------------------------- /src/actdiag/utils/rst/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | -------------------------------------------------------------------------------- /src/actdiag/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | __version__ = '3.0.0' 17 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release a new package 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: '3.x' 17 | - name: Install dependencies 18 | run: pip install docutils setuptools wheel 19 | - name: Build package 20 | run: python setup.py sdist bdist_wheel 21 | - name: Upload package to PyPI 22 | uses: pypa/gh-action-pypi-publish@master 23 | if: startsWith(github.ref, 'refs/tags/') 24 | with: 25 | user: __token__ 26 | password: ${{ secrets.PYPI_API_TOKEN }} 27 | -------------------------------------------------------------------------------- /src/actdiag/utils/rst/nodes.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from blockdiag.utils.rst import nodes 17 | 18 | import actdiag.builder 19 | import actdiag.drawer 20 | import actdiag.parser 21 | 22 | 23 | class actdiag(nodes.blockdiag): 24 | name = 'actdiag' 25 | processor = actdiag 26 | -------------------------------------------------------------------------------- /src/actdiag/command.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import sys 17 | 18 | from blockdiag.utils.bootstrap import Application 19 | 20 | import actdiag 21 | import actdiag.builder 22 | import actdiag.drawer 23 | import actdiag.parser 24 | 25 | 26 | class ActdiagApp(Application): 27 | module = actdiag 28 | 29 | 30 | def main(args=sys.argv[1:]): 31 | return ActdiagApp().run(args) 32 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | env: 6 | PYTHONFAULTHANDLER: x 7 | ALL_TESTS: 1 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - python-version: '3.7' 17 | toxenv: py37 18 | - python-version: '3.8' 19 | toxenv: py38 20 | - python-version: '3.9' 21 | toxenv: py39 22 | - python-version: '3.9' 23 | toxenv: blockdiag_dev 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | - name: Install dependencies 31 | run: | 32 | sudo apt-get install fonts-ipafont-gothic ghostscript libjpeg8-dev libfreetype6-dev 33 | pip install -U docutils tox 34 | - name: Run tox 35 | env: 36 | TOXENV: ${{ matrix.toxenv }} 37 | run: tox -- -v 38 | -------------------------------------------------------------------------------- /src/actdiag/tests/test_generate_diagram.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | 18 | from blockdiag.tests.test_generate_diagram import (get_diagram_files, 19 | testcase_generator) 20 | 21 | import actdiag.command 22 | 23 | 24 | def test_generate(): 25 | mainfunc = actdiag.command.main 26 | basepath = os.path.dirname(__file__) 27 | files = get_diagram_files(basepath) 28 | options = [] 29 | 30 | for testcase in testcase_generator(basepath, mainfunc, files, options): 31 | yield testcase 32 | -------------------------------------------------------------------------------- /src/actdiag/plugins/autolane.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import re 17 | 18 | from blockdiag import plugins 19 | 20 | 21 | class AutoLane(plugins.NodeHandler): 22 | def on_created(self, node): 23 | if node.id is None: 24 | return 25 | 26 | for lane in self.diagram.lanes: 27 | pattern = "^%s_" % re.escape(lane.id) 28 | 29 | if re.search(pattern, node.id) and node.lane is None: 30 | node.label = re.sub(pattern, '', node.id) 31 | node.lane = lane 32 | lane.nodes.append(node) 33 | 34 | 35 | def setup(self, diagram, **kwargs): 36 | plugins.install_node_handler(AutoLane(diagram, **kwargs)) 37 | -------------------------------------------------------------------------------- /src/actdiag/elements.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import blockdiag.elements 17 | 18 | 19 | class DiagramNode(blockdiag.elements.DiagramNode): 20 | lane = None 21 | 22 | 23 | class DiagramEdge(blockdiag.elements.DiagramEdge): 24 | pass 25 | 26 | 27 | class NodeGroup(blockdiag.elements.NodeGroup): 28 | def __init__(self, _id): 29 | super(NodeGroup, self).__init__(_id) 30 | 31 | self.color = '#ffff99' 32 | 33 | 34 | class Diagram(blockdiag.elements.Diagram): 35 | _DiagramNode = DiagramNode 36 | _NodeGroup = NodeGroup 37 | 38 | def __init__(self): 39 | super(Diagram, self).__init__() 40 | 41 | self.orientation = 'portrait' 42 | self.lanes = [] 43 | -------------------------------------------------------------------------------- /src/actdiag/utils/rst/directives.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from blockdiag.utils.rst import directives 17 | from docutils.parsers import rst 18 | 19 | from actdiag.utils.rst.nodes import actdiag as actdiag_node 20 | 21 | directive_options_default = dict(format='PNG', 22 | antialias=False, 23 | fontpath=None, 24 | outputdir=None, 25 | nodoctype=False, 26 | noviewbox=False, 27 | inline_svg=False) 28 | directive_options = {} 29 | 30 | 31 | class ActdiagDirective(directives.BlockdiagDirective): 32 | name = "actdiag" 33 | node_class = actdiag_node 34 | 35 | @property 36 | def global_options(self): 37 | return directive_options 38 | 39 | 40 | def setup(**kwargs): 41 | for key, value in directive_options_default.items(): 42 | directive_options[key] = kwargs.get(key, value) 43 | 44 | rst.directives.register_directive("actdiag", ActdiagDirective) 45 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | `actdiag` generate activity-diagram image file from spec-text file. 2 | 3 | .. image:: https://drone.io/bitbucket.org/blockdiag/actdiag/status.png 4 | :target: https://drone.io/bitbucket.org/blockdiag/actdiag 5 | :alt: drone.io CI build status 6 | 7 | .. image:: https://pypip.in/v/actdiag/badge.png 8 | :target: https://pypi.python.org/pypi/actdiag/ 9 | :alt: Latest PyPI version 10 | 11 | .. image:: https://pypip.in/d/actdiag/badge.png 12 | :target: https://pypi.python.org/pypi/actdiag/ 13 | :alt: Number of PyPI downloads 14 | 15 | 16 | Features 17 | ======== 18 | 19 | * Generate activity-diagram from dot like text (basic feature). 20 | * Multilingualization for node-label (utf-8 only). 21 | 22 | You can get some examples and generated images on 23 | `blockdiag.com `_ . 24 | 25 | Setup 26 | ===== 27 | 28 | Use easy_install or pip:: 29 | 30 | $ sudo easy_install actdiag 31 | 32 | Or 33 | 34 | $ sudo pip actdiag 35 | 36 | 37 | spec-text setting sample 38 | ======================== 39 | 40 | Few examples are available. 41 | You can get more examples at 42 | `blockdiag.com`_ . 43 | 44 | simple.diag 45 | ------------ 46 | 47 | simple.diag is simply define nodes and transitions by dot-like text format:: 48 | 49 | diagram { 50 | A -> B -> C; 51 | lane you { 52 | A; B; 53 | } 54 | lane me { 55 | C; 56 | } 57 | } 58 | 59 | 60 | Usage 61 | ===== 62 | 63 | Execute actdiag command:: 64 | 65 | $ actdiag simple.diag 66 | $ ls simple.png 67 | simple.png 68 | 69 | 70 | Requirements 71 | ============ 72 | * Python 3.7 or later 73 | * blockdiag 1.5.0 or later 74 | * funcparserlib 0.3.6 or later 75 | * reportlab (optional) 76 | * wand and imagemagick (optional) 77 | * setuptools 78 | 79 | 80 | License 81 | ======= 82 | Apache License 2.0 83 | -------------------------------------------------------------------------------- /src/actdiag/drawer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import blockdiag.drawer 17 | 18 | from actdiag.metrics import DiagramMetrics 19 | 20 | 21 | class DiagramDraw(blockdiag.drawer.DiagramDraw): 22 | def create_metrics(self, *args, **kwargs): 23 | return DiagramMetrics(*args, **kwargs) 24 | 25 | def _draw_elements(self, **kwargs): 26 | m = self.metrics 27 | 28 | # render label of lanes 29 | for i, lane in enumerate(self.diagram.lanes): 30 | if lane.label: 31 | label = lane.label 32 | elif isinstance(lane.id, str): 33 | label = lane.id 34 | else: 35 | label = 'Lane %d' % (i + 1) 36 | 37 | if lane.href and self.format == 'SVG': 38 | drawer = self.drawer.anchor(lane.href) 39 | else: 40 | drawer = self.drawer 41 | 42 | headerbox = m.lane_headerbox(lane) 43 | drawer.rectangle(headerbox, fill=lane.color, outline=lane.color) 44 | 45 | textbox = m.lane_textbox(lane) 46 | drawer.textarea(textbox, label, fill=self.fill, 47 | font=self.metrics.font_for(lane)) 48 | 49 | # render frame of activity lanes 50 | frame = m.frame(self.diagram.lanes) 51 | self.drawer.rectangle(frame.outline, outline='gray') 52 | for xy in frame.separators: 53 | self.drawer.line(xy, fill='gray') 54 | 55 | super(DiagramDraw, self)._draw_elements(**kwargs) 56 | -------------------------------------------------------------------------------- /actdiag.1: -------------------------------------------------------------------------------- 1 | .\" Hey, EMACS: -*- nroff -*- 2 | .\" First parameter, NAME, should be all caps 3 | .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection 4 | .\" other parameters are allowed: see man(7), man(1) 5 | .TH ACTDIAG 1 "June 5, 2011" 6 | .\" Please adjust this date whenever revising the manpage. 7 | .\" 8 | .\" Some roff macros, for reference: 9 | .\" .nh disable hyphenation 10 | .\" .hy enable hyphenation 11 | .\" .ad l left justify 12 | .\" .ad b justify to both left and right margins 13 | .\" .nf disable filling 14 | .\" .fi enable filling 15 | .\" .br insert line break 16 | .\" .sp insert n+1 empty lines 17 | .\" for manpage-specific macros, see man(7) 18 | .SH NAME 19 | actdiag \- generate activity-diagram image file from spec-text file. 20 | .SH SYNOPSIS 21 | .B actdiag 22 | .RI [ options ] " files" 23 | .SH DESCRIPTION 24 | This manual page documents briefly the 25 | .B actdiag 26 | commands. 27 | .PP 28 | .\" TeX users may be more comfortable with the \fB\fP and 29 | .\" \fI\fP escape sequences to invode bold face and italics, 30 | .\" respectively. 31 | \fBactdiag\fP is a program that generate activity-diagram image file from spec-text file. 32 | .SH OPTIONS 33 | These programs follow the usual GNU command line syntax, with long 34 | options starting with two dashes (`-'). 35 | A summary of options is included below. 36 | For a complete description, see the Info files. 37 | .TP 38 | .B \-h, \-\-help 39 | show this help message and exit 40 | .TP 41 | .B \-a, \-\-antialias 42 | Pass diagram image to anti-alias filter 43 | .TP 44 | .B \-c FILE, \-\-config=FILE 45 | read configurations from FILE 46 | .TP 47 | .B \-o FILE 48 | write diagram to FILE 49 | .TP 50 | .B \-f FONT, \-\-font=FONT 51 | use FONT to draw diagram 52 | .TP 53 | .B \-s, \-\---separate 54 | Separate diagram images for each group (SVG only) 55 | .TP 56 | .B \-T TYPE 57 | Output diagram as TYPE format 58 | .SH SEE ALSO 59 | The programs are documented fully by 60 | .br 61 | .BR http://tk0miya.bitbucket.org/actdiag/build/html/index.html 62 | .br 63 | .SH AUTHOR 64 | actdiag was written by Takeshi Komiya 65 | .PP 66 | This manual page was written by Kouhei Maeda , 67 | for the Debian project (and may be used by others). 68 | -------------------------------------------------------------------------------- /src/actdiag/tests/VLGothic/LICENSE: -------------------------------------------------------------------------------- 1 | VL ゴシックフォントファミリライセンス 2 | ------------------------------------- 3 | 4 | M+ FONTS 由来の部分については、M+ FONTS PROJECT のライセンスが適用されます。 5 | 添付の LICENSE_J.mplus を参照してください。 6 | 7 | さざなみゴシックフォント由来の部分およびそれらの部品を元に改変した一部の文字 8 | については、さざなみフォントと同様に修正BSDライセンスとします。オリジナルの 9 | さざなみフォントのライセンスについては添付の README.sazanami を参照してくだ 10 | さい。 11 | 12 | その他 VL ゴシックフォントファミリで独自に追加した文字、および M+フォント 13 | の部品を元に独自に作成した文字(2007/05/06以降に修正した漢字および記号)はM+ 14 | フォントと同じライセンスを摘要します。 15 | 16 | なお、文書への埋め込みなど、フォントとしての再使用を目的としない用途におい 17 | ては、以下で言う Redistribution には当たらず、制限なく行えるものとします。 18 | 19 | Copyright (c) 1990-2003 Wada Laboratory, the University of Tokyo. 20 | Copyright (c) 2003-2004 Electronic Font Open Laboratory (/efont/). 21 | Copyright (C) 2002-2014 M+ FONTS PROJECT 22 | Copyright (C) 2006-2014 Daisuke SUZUKI . 23 | Copyright (C) 2006-2014 Project Vine . 24 | All rights reserved. 25 | 26 | Redistribution and use in source and binary forms, with or without 27 | modification, are permitted provided that the following conditions 28 | are met: 29 | 1. Redistributions of source code must retain the above copyright notice, 30 | this list of conditions and the following disclaimer. 31 | 2. Redistributions in binary form must reproduce the above copyright notice, 32 | this list of conditions and the following disclaimer in the documentation 33 | and/or other materials provided with the distribution. 34 | 3. Neither the name of the Wada Laboratory, the University of Tokyo nor 35 | the names of its contributors may be used to endorse or promote products 36 | derived from this software without specific prior written permission. 37 | 38 | THIS SOFTWARE IS PROVIDED BY WADA LABORATORY, THE UNIVERSITY OF TOKYO AND 39 | CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 40 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 41 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE LABORATORY OR 42 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 43 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 44 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 45 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 46 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 47 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 48 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 49 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import os 3 | 4 | from setuptools import find_packages, setup 5 | 6 | classifiers = [ 7 | "Development Status :: 5 - Production/Stable", 8 | "Intended Audience :: System Administrators", 9 | "License :: OSI Approved :: Apache Software License", 10 | "Programming Language :: Python", 11 | "Programming Language :: Python :: 3.7", 12 | "Programming Language :: Python :: 3.8", 13 | "Programming Language :: Python :: 3.9", 14 | "Topic :: Software Development", 15 | "Topic :: Software Development :: Documentation", 16 | "Topic :: Text Processing :: Markup", 17 | ] 18 | 19 | 20 | def get_version(): 21 | """Get version number of the package from version.py without importing core module.""" 22 | package_dir = os.path.abspath(os.path.dirname(__file__)) 23 | version_file = os.path.join(package_dir, 'src/actdiag/__init__.py') 24 | 25 | namespace = {} 26 | with open(version_file, 'r') as f: 27 | exec(f.read(), namespace) 28 | 29 | return namespace['__version__'] 30 | 31 | 32 | setup( 33 | name='actdiag', 34 | version=get_version(), 35 | description='actdiag generates activity-diagram image from text', 36 | long_description=open("README.rst").read(), 37 | long_description_content_type='text/x-rst', 38 | classifiers=classifiers, 39 | keywords=['diagram', 'generator'], 40 | author='Takeshi Komiya', 41 | author_email='i.tkomiya@gmail.com', 42 | url='http://blockdiag.com/', 43 | download_url='http://pypi.python.org/pypi/actdiag', 44 | project_urls={ 45 | "Code": "https://github.com/blockdiag/actdiag", 46 | "Issue tracker": "https://github.com/blockdiag/actdiag/issues", 47 | }, 48 | license='Apache License 2.0', 49 | packages=find_packages('src'), 50 | package_dir={'': 'src'}, 51 | package_data={'': ['buildout.cfg']}, 52 | include_package_data=True, 53 | python_requires=">=3.7", 54 | install_requires=['blockdiag >= 3.0.0'], 55 | extras_require=dict( 56 | rst=[ 57 | 'docutils', 58 | ], 59 | testing=[ 60 | 'nose', 61 | 'pep8 >=1.3', 62 | 'reportlab', 63 | 'docutils', 64 | 'flake8', 65 | 'flake8-coding', 66 | 'flake8-copyright', 67 | 'flake8-isort', 68 | ], 69 | ), 70 | test_suite='nose.collector', 71 | entry_points=""" 72 | [console_scripts] 73 | actdiag = actdiag.command:main 74 | 75 | [blockdiag_plugins] 76 | autolane = actdiag.plugins.autolane 77 | """, 78 | ) 79 | -------------------------------------------------------------------------------- /src/actdiag/tests/VLGothic/LICENSE.en: -------------------------------------------------------------------------------- 1 | License for VLGothic Font Family 2 | -------------------------------- 3 | 4 | This font includes glyphs derived from M+ FONTS which is created by 5 | M+ FONTS PROJECT. License for M+ FONTS part is described in M+ FONTS 6 | PROJECT's license. See attached 'LICENSE_E.mplus'. 7 | 8 | This font also includes glyphs derived from Sazanami Gothic font which 9 | is created by Electronic Font Open Laboratory (/efont/). License for 10 | Sazanami Gothic part is described in it's license. See attached 11 | 'README.sazanami' for original Sazanami Gothic font license. 12 | 13 | This font also includes original glyphs which is created by Daisuke 14 | SUZUKI and Project Vine based on M+ FONTS. Licese for VL Gothic 15 | original glyphs is same as M+ FONTS PROJECT's license. 16 | 17 | There is no limitation and the below description is not applied 18 | as for in order not to reuse as font (ex: font is embeded to documents). 19 | 20 | Copyright (c) 1990-2003 Wada Laboratory, the University of Tokyo. 21 | Copyright (c) 2003-2004 Electronic Font Open Laboratory (/efont/). 22 | Copyright (C) 2003-2012 M+ FONTS PROJECT 23 | Copyright (C) 2006-2012 Daisuke SUZUKI . 24 | Copyright (C) 2006-2012 Project Vine . 25 | All rights reserved. 26 | 27 | Redistribution and use in source and binary forms, with or without 28 | modification, are permitted provided that the following conditions 29 | are met: 30 | 1. Redistributions of source code must retain the above copyright notice, 31 | this list of conditions and the following disclaimer. 32 | 2. Redistributions in binary form must reproduce the above copyright notice, 33 | this list of conditions and the following disclaimer in the documentation 34 | and/or other materials provided with the distribution. 35 | 3. Neither the name of the Wada Laboratory, the University of Tokyo nor 36 | the names of its contributors may be used to endorse or promote products 37 | derived from this software without specific prior written permission. 38 | 39 | THIS SOFTWARE IS PROVIDED BY WADA LABORATORY, THE UNIVERSITY OF TOKYO AND 40 | CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 41 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 42 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE LABORATORY OR 43 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 44 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 45 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; 46 | OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 47 | WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR 48 | OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 49 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 50 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 3.0.0 (2021-12-06) 5 | ------------------ 6 | * Drop python3.6 support 7 | * Use funcparserlib-1.0.0a0 or newer to support new python versions 8 | * Allow to write multiline string via triple quotes (""" ... """) 9 | 10 | 2.0.0 (2020-01-26) 11 | ------------------ 12 | * Drop python2 and python3.4 support 13 | 14 | 0.5.4 (2015-01-01) 15 | ------------------ 16 | * Follow blockdiag-1.5.0 interface 17 | 18 | 0.5.3 (2014-07-02) 19 | ------------------ 20 | * Change interface of docutils node (for sphinxcontrib module) 21 | 22 | 0.5.2 (2014-06-24) 23 | ------------------ 24 | * Add options to blockdiag directive (docutils extension) 25 | - :width: 26 | - :height: 27 | - :scale: 28 | - :align: 29 | - :name: 30 | - :class: 31 | - :figwidth: 32 | - :figclass: 33 | 34 | 0.5.1 (2013-10-22) 35 | ------------------ 36 | * Fix bugs 37 | 38 | 0.5.0 (2013-10-05) 39 | ------------------ 40 | * Support python 3.2 and 3.3 (thanks to @masayuko) 41 | * Drop supports for python 2.4 and 2.5 42 | * Replace dependency: PIL -> Pillow 43 | 44 | 0.4.3 (2013-02-10) 45 | ------------------ 46 | * Fix bugs 47 | 48 | 0.4.2 (2013-02-10) 49 | ------------------ 50 | * Fix bugs 51 | 52 | 0.4.1 (2012-10-28) 53 | ------------------ 54 | * Fix bugs 55 | 56 | 0.4.0 (2012-10-22) 57 | ------------------ 58 | * Optimize algorithm for rendering shadow 59 | * Add options to docutils directive 60 | * Fix bugs 61 | 62 | 0.3.4 (2012-09-29) 63 | ------------------ 64 | * Fix bugs 65 | 66 | 0.3.3 (2012-04-23) 67 | ------------------ 68 | * Set hyperlinks to header of lanes on SVG image 69 | * Fill background of lane header with lane.color attribute 70 | 71 | 0.3.2 (2012-03-15) 72 | ------------------ 73 | * Fix bugs 74 | 75 | 0.3.1 (2012-02-15) 76 | ------------------ 77 | * Add autolane plugin 78 | * Update to new package structure (blockdiag >= 1.1.2) 79 | 80 | 0.3.0 (2011-11-19) 81 | ------------------ 82 | * Add fontfamily attribute for switching fontface 83 | * Fix bugs 84 | 85 | 0.2.4 (2011-11-10) 86 | ------------------ 87 | * Fix dependencies (do not depend PIL directly for pillow users) 88 | 89 | 0.2.3 (2011-11-06) 90 | ------------------ 91 | * Add docutils exetension 92 | * Fix bugs 93 | 94 | 0.2.2 (2011-11-01) 95 | ------------------ 96 | * Add class feature (experimental) 97 | 98 | 0.2.1 (2011-11-01) 99 | ------------------ 100 | * Follow blockdiag-0.9.7 interface 101 | 102 | 0.2.0 (2011-10-19) 103 | ------------------ 104 | * Follow blockdiag-0.9.5 interface 105 | 106 | 0.1.9 (2011-10-11) 107 | ------------------ 108 | * Fix bugs 109 | 110 | 0.1.8 (2011-09-30) 111 | ------------------ 112 | * Add diagram attribute: default_text_color 113 | 114 | 0.1.7 (2011-07-05) 115 | ------------------ 116 | * Fix bugs 117 | 118 | 0.1.6 (2011-07-03) 119 | ------------------ 120 | * Support input from stdin 121 | 122 | 0.1.5 (2011-05-15) 123 | ------------------ 124 | * Fix bugs 125 | 126 | 0.1.4 (2011-05-14) 127 | ------------------ 128 | * Change license to Apache License 2.0 129 | * Support blockdiag 0.8.1 core interface 130 | 131 | 0.1.3 (2011-04-19) 132 | ------------------ 133 | * Fix bugs 134 | 135 | 0.1.2 (2011-04-11) 136 | ------------------ 137 | * Fix bugs 138 | 139 | 0.1.1 (2011-04-10) 140 | ------------------ 141 | * Fix bugs 142 | 143 | 0.1.0 (2011-04-09) 144 | ------------------ 145 | * First release 146 | -------------------------------------------------------------------------------- /src/actdiag/metrics.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import division 17 | 18 | from collections import namedtuple 19 | 20 | import blockdiag.metrics 21 | from blockdiag.utils import XY, Box 22 | 23 | from actdiag import elements 24 | 25 | 26 | class DiagramMetrics(blockdiag.metrics.DiagramMetrics): 27 | def __init__(self, diagram, **kwargs): 28 | super(DiagramMetrics, self).__init__(diagram, **kwargs) 29 | 30 | if diagram.page_padding is None: 31 | if diagram.orientation == 'landscape': 32 | padding = self.node_width + self.span_width 33 | self.page_padding = [0, 0, 0, padding] 34 | else: 35 | padding = self.node_height + self.span_height 36 | self.page_padding = [padding, 0, 0, 0] 37 | 38 | def pagesize(self, width=None, height=None): 39 | if width: 40 | self.colwidth = width 41 | else: 42 | width = self.colwidth 43 | 44 | if height: 45 | self.colheight = height 46 | else: 47 | height = self.colheight 48 | 49 | return super(DiagramMetrics, self).pagesize(width, height) 50 | 51 | def frame(self, lanes): 52 | dummy = elements.DiagramNode(None) 53 | dummy.xy = XY(0, 0) 54 | dummy.colwidth = self.colwidth 55 | dummy.colheight = self.colheight 56 | cell = self.cell(dummy, use_padding=False) 57 | 58 | headerbox = Box(cell.topleft.x - self.span_width // 2, 59 | (cell.topleft.y - self.node_height - 60 | self.span_height - 2), 61 | cell.topright.x + self.span_width // 2, 62 | cell.topright.y - self.span_height // 2) 63 | 64 | outline = Box(headerbox[0], headerbox[1], headerbox[2], 65 | cell.bottom.y + self.span_height // 2) 66 | 67 | separators = [(XY(headerbox[0], headerbox[3]), 68 | XY(headerbox[2], headerbox[3]))] 69 | 70 | for lane in lanes[:-1]: 71 | x = lane.xy.x + lane.colwidth + 1 72 | 73 | m = self.cell(lane, use_padding=False) 74 | span_width = self.spreadsheet.span_width[x] // 2 75 | x1 = m.right.x + span_width 76 | 77 | xy = (XY(x1, outline[1]), XY(x1, outline[3])) 78 | separators.append(xy) 79 | 80 | Frame = namedtuple('Frame', 'headerbox outline separators') 81 | return Frame(headerbox, outline, separators) 82 | 83 | def lane_textbox(self, lane): 84 | headerbox = self.frame([]).headerbox 85 | m = self.cell(lane, use_padding=False) 86 | x1 = m.left.x 87 | x2 = m.right.x 88 | 89 | return Box(x1, headerbox[1], x2, headerbox[3]) 90 | 91 | def lane_headerbox(self, lane): 92 | headerbox = self.frame([]).headerbox 93 | m = self.cell(lane) 94 | x1 = m.left.x - self.spreadsheet.span_width[lane.xy.x] // 2 95 | x2 = m.right.x + self.spreadsheet.span_width[lane.xy.x + 1] // 2 96 | 97 | return Box(x1, headerbox[1], x2, headerbox[3]) 98 | -------------------------------------------------------------------------------- /src/actdiag/parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2008/2009 Andrey Vlasovskikh 4 | # 5 | # Permission is hereby granted, free of charge, to any person obtaining 6 | # a copy of this software and associated documentation files (the 7 | # "Software"), to deal in the Software without restriction, including 8 | # without limitation the rights to use, copy, modify, merge, publish, 9 | # distribute, sublicense, and/or sell copies of the Software, and to 10 | # permit persons to whom the Software is furnished to do so, subject to 11 | # the following conditions: 12 | # 13 | # The above copyright notice and this permission notice shall be included 14 | # in all copies or substantial portions of the Software. 15 | # 16 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | r'''A DOT language parser using funcparserlib. 25 | 26 | The parser is based on [the DOT grammar][1]. It is pretty complete with a few 27 | not supported things: 28 | 29 | * Ports and compass points 30 | * XML identifiers 31 | 32 | At the moment, the parser builds only a parse tree, not an abstract syntax tree 33 | (AST) or an API for dealing with DOT. 34 | 35 | [1]: http://www.graphviz.org/doc/info/lang.html 36 | ''' 37 | 38 | import io 39 | from collections import namedtuple 40 | from re import DOTALL, MULTILINE 41 | 42 | from blockdiag.parser import create_mapper, oneplus_to_list 43 | from funcparserlib.lexer import LexerError, Token, make_tokenizer 44 | from funcparserlib.parser import a, finished, many, maybe, skip, some 45 | 46 | Diagram = namedtuple('Diagram', 'id stmts') 47 | Lane = namedtuple('Lane', 'id stmts') 48 | Node = namedtuple('Node', 'id attrs') 49 | Attr = namedtuple('Attr', 'name value') 50 | Edge = namedtuple('Edge', 'from_nodes edge_type to_nodes attrs') 51 | Extension = namedtuple('Extension', 'type name attrs') 52 | Statements = namedtuple('Statements', 'stmts') 53 | 54 | 55 | class ParseException(Exception): 56 | pass 57 | 58 | 59 | def tokenize(string): 60 | """str -> Sequence(Token)""" 61 | # flake8: NOQA 62 | specs = [ # NOQA 63 | ('Comment', (r'/\*(.|[\r\n])*?\*/', MULTILINE)), # NOQA 64 | ('Comment', (r'(//|#).*',)), # NOQA 65 | ('NL', (r'[\r\n]+',)), # NOQA 66 | ('Space', (r'[ \t\r\n]+',)), # NOQA 67 | ('Name', ('[A-Za-z_0-9\u0080-\uffff]' + # NOQA 68 | '[A-Za-z_\\-.0-9\u0080-\uffff]*',)), # NOQA 69 | ('Op', (r'[{};,=\[\]]|(<->)|(<-)|(--)|(->)',)), # NOQA 70 | ('Number', (r'-?(\.[0-9]+)|([0-9]+(\.[0-9]*)?)',)), # NOQA 71 | ('String', (r'(?P(""")|(\'\'\')|"|\').*?(? object""" 80 | tokval = lambda x: x.value 81 | op = lambda s: a(Token('Op', s)) >> tokval 82 | op_ = lambda s: skip(op(s)) 83 | _id = some(lambda t: t.type in ['Name', 'Number', 'String']) >> tokval 84 | keyword = lambda s: a(Token('Name', s)) >> tokval 85 | 86 | def make_node_list(node_list, attrs): 87 | return Statements([Node(node, attrs) for node in node_list]) 88 | 89 | def make_edge(first, edge_type, second, followers, attrs): 90 | edges = [Edge(first, edge_type, second, attrs)] 91 | 92 | from_node = second 93 | for edge_type, to_node in followers: 94 | edges.append(Edge(from_node, edge_type, to_node, attrs)) 95 | from_node = to_node 96 | 97 | return Statements(edges) 98 | 99 | # 100 | # parts of syntax 101 | # 102 | node_list = ( 103 | _id + 104 | many(op_(',') + _id) 105 | >> create_mapper(oneplus_to_list) 106 | ) 107 | option_stmt = ( 108 | _id + 109 | maybe(op_('=') + _id) 110 | >> create_mapper(Attr) 111 | ) 112 | option_list = ( 113 | maybe(op_('[') + option_stmt + many(op_(',') + option_stmt) + op_(']')) 114 | >> create_mapper(oneplus_to_list, default_value=[]) 115 | ) 116 | 117 | # node (node list) statement:: 118 | # A; 119 | # B [attr = value, attr = value]; 120 | # C, D [attr = value, attr = value]; 121 | # 122 | node_stmt = ( 123 | node_list + option_list 124 | >> create_mapper(make_node_list) 125 | ) 126 | 127 | # edge statement:: 128 | # A -> B; 129 | # A <- B; 130 | # 131 | edge_relation = ( 132 | op('->') | op('--') | op('<-') | op('<->') 133 | ) 134 | edge_stmt = ( 135 | node_list + 136 | edge_relation + 137 | node_list + 138 | many(edge_relation + node_list) + 139 | option_list 140 | >> create_mapper(make_edge) 141 | ) 142 | 143 | # attributes statement:: 144 | # default_shape = box; 145 | # default_fontsize = 16; 146 | # 147 | attribute_stmt = ( 148 | _id + op_('=') + _id 149 | >> create_mapper(Attr) 150 | ) 151 | 152 | # extension statement (class, plugin):: 153 | # class red [color = red]; 154 | # plugin attributes [name = Name]; 155 | # 156 | extension_stmt = ( 157 | (keyword('class') | keyword('plugin')) + 158 | _id + 159 | option_list 160 | >> create_mapper(Extension) 161 | ) 162 | 163 | # lane statement:: 164 | # lane A [color = red]; 165 | # lane { 166 | # A; 167 | # } 168 | # 169 | lane_declare_stmt = ( 170 | skip(keyword('lane')) + 171 | _id + 172 | option_list 173 | >> create_mapper(Lane) 174 | ) 175 | lane_inline_stmt = ( 176 | edge_stmt | 177 | attribute_stmt | 178 | node_stmt 179 | ) 180 | lane_inline_stmt_list = ( 181 | many(lane_inline_stmt + skip(maybe(op(';')))) 182 | ) 183 | lane_stmt = ( 184 | skip(keyword('lane')) + 185 | maybe(_id) + 186 | op_('{') + 187 | lane_inline_stmt_list + 188 | op_('}') 189 | >> create_mapper(Lane) 190 | ) 191 | 192 | # 193 | # diagram statement:: 194 | # actdiag { 195 | # A; 196 | # } 197 | # 198 | diagram_id = ( 199 | (keyword('diagram') | keyword('actdiag')) + 200 | maybe(_id) 201 | >> list 202 | ) 203 | diagram_inline_stmt = ( 204 | extension_stmt | 205 | edge_stmt | 206 | lane_stmt | 207 | lane_declare_stmt | 208 | attribute_stmt | 209 | node_stmt 210 | ) 211 | diagram_inline_stmt_list = ( 212 | many(diagram_inline_stmt + skip(maybe(op(';')))) 213 | ) 214 | diagram = ( 215 | maybe(diagram_id) + 216 | op_('{') + 217 | diagram_inline_stmt_list + 218 | op_('}') 219 | >> create_mapper(Diagram) 220 | ) 221 | dotfile = diagram + skip(finished) 222 | 223 | return dotfile.parse(seq) 224 | 225 | 226 | def sort_tree(tree): 227 | def weight(node): 228 | if isinstance(node, (Attr, Extension)): 229 | return 1 230 | else: 231 | return 2 232 | 233 | if hasattr(tree, 'stmts'): 234 | tree.stmts.sort(key=lambda x: weight(x)) 235 | for stmt in tree.stmts: 236 | sort_tree(stmt) 237 | 238 | return tree 239 | 240 | 241 | def parse_string(string): 242 | try: 243 | tree = parse(tokenize(string)) 244 | return sort_tree(tree) 245 | except LexerError as e: 246 | message = "Got unexpected token at line %d column %d" % e.place 247 | raise ParseException(message) 248 | except Exception as e: 249 | raise ParseException(str(e)) 250 | 251 | 252 | def parse_file(path): 253 | input = io.open(path, 'r', encoding='utf-8-sig').read() 254 | return parse_string(input) 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/actdiag/builder.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | from __future__ import print_function 17 | 18 | from blockdiag.utils import XY, unquote 19 | from blockdiag.utils.compat import cmp_to_key 20 | 21 | from actdiag import parser 22 | from actdiag.elements import Diagram, DiagramEdge, DiagramNode, NodeGroup 23 | 24 | 25 | class DiagramTreeBuilder(object): 26 | def build(self, tree): 27 | self.diagram = Diagram() 28 | diagram = self.instantiate(self.diagram, tree) 29 | 30 | self.bind_edges(diagram) 31 | 32 | if len(self.diagram.lanes) == 0: 33 | self.diagram.lanes.append(NodeGroup.get(None)) 34 | 35 | for node in self.diagram.nodes: 36 | if node.lane is None: 37 | edges = DiagramEdge.find(None, node) 38 | parents = [e.node1 for e in edges if e.node1.lane] 39 | parents.sort(key=lambda x: x.order) 40 | 41 | if parents: 42 | node.lane = parents[0].lane 43 | node.lane.nodes.append(node) 44 | else: 45 | node.lane = self.diagram.lanes[0] 46 | node.lane.nodes.append(node) 47 | 48 | for lane in diagram.lanes: 49 | if len(lane.nodes) == 0: 50 | diagram.lanes.remove(lane) 51 | 52 | return diagram 53 | 54 | def belong_to(self, node, lane): 55 | if lane and node.lane and node.lane != lane: 56 | print(node, node.lane, lane) 57 | msg = "DiagramNode could not belong to two lanes" 58 | raise RuntimeError(msg) 59 | 60 | node.group = self.diagram 61 | if lane: 62 | node.lane = lane 63 | lane.nodes.append(node) 64 | 65 | if node not in self.diagram.nodes: 66 | self.diagram.nodes.append(node) 67 | 68 | def instantiate(self, group, tree, lane=None): 69 | for stmt in tree.stmts: 70 | if isinstance(stmt, parser.Node): 71 | node = DiagramNode.get(stmt.id) 72 | node.set_attributes(stmt.attrs) 73 | self.belong_to(node, lane) 74 | 75 | elif isinstance(stmt, parser.Edge): 76 | from_nodes = [DiagramNode.get(n) for n in stmt.from_nodes] 77 | to_nodes = [DiagramNode.get(n) for n in stmt.to_nodes] 78 | 79 | for node in from_nodes + to_nodes: 80 | self.belong_to(node, lane) 81 | 82 | for node1 in from_nodes: 83 | for node2 in to_nodes: 84 | edge = DiagramEdge.get(node1, node2) 85 | edge.set_dir(stmt.edge_type) 86 | edge.set_attributes(stmt.attrs) 87 | 88 | elif isinstance(stmt, parser.Lane): 89 | _lane = NodeGroup.get(stmt.id) 90 | if _lane not in self.diagram.lanes: 91 | self.diagram.lanes.append(_lane) 92 | 93 | self.instantiate(group, stmt, _lane) 94 | 95 | elif isinstance(stmt, parser.Attr): 96 | if lane: 97 | lane.set_attribute(stmt) 98 | else: 99 | self.diagram.set_attribute(stmt) 100 | 101 | elif isinstance(stmt, parser.Extension): 102 | if stmt.type == 'class': 103 | name = unquote(stmt.name) 104 | Diagram.classes[name] = stmt 105 | if stmt.type == 'plugin': 106 | self.diagram.set_plugin(stmt.name, stmt.attrs) 107 | 108 | elif isinstance(stmt, parser.Statements): 109 | self.instantiate(group, stmt, lane) 110 | 111 | group.update_order() 112 | return group 113 | 114 | def bind_edges(self, group): 115 | for node in group.nodes: 116 | if isinstance(node, DiagramNode): 117 | group.edges += DiagramEdge.find(node) 118 | else: 119 | self.bind_edges(node) 120 | 121 | 122 | class DiagramLayoutManager: 123 | def __init__(self, diagram): 124 | self.diagram = diagram 125 | 126 | self.circulars = [] 127 | self.heightRefs = [] 128 | 129 | def run(self): 130 | self.edges = [e for e in DiagramEdge.find_all()] 131 | self.do_layout() 132 | self.diagram.fixiate() 133 | self.fixiate_lanes() 134 | 135 | def fixiate_lanes(self): 136 | height = 0 137 | for lane in self.diagram.lanes: 138 | if self.coordinates[lane]: 139 | for node in self.diagram.nodes: 140 | if node.lane == lane: 141 | node.xy = XY(node.xy.x, node.xy.y + height) 142 | 143 | height += max(xy.y for xy in self.coordinates[lane]) + 1 144 | 145 | nodes = [n for n in self.diagram.nodes if n.lane == lane] 146 | x = min(n.xy.x for n in nodes) 147 | y = min(n.xy.y for n in nodes) 148 | lane.xy = XY(x, y) 149 | lane.colwidth = max(n.xy.x + n.colwidth for n in nodes) - x 150 | lane.colheight = max(n.xy.y + n.colheight for n in nodes) - y 151 | 152 | def do_layout(self): 153 | self.detect_circulars() 154 | 155 | self.set_node_width() 156 | self.adjust_node_order() 157 | 158 | height = 0 159 | self.initialize_markers() 160 | for node in self.diagram.traverse_nodes(): 161 | if node.xy.x == 0: 162 | lane = node.lane 163 | if self.coordinates[lane]: 164 | height = max(xy.y for xy in self.coordinates[lane]) + 1 165 | else: 166 | height = 0 167 | self.set_node_height(node, height) 168 | 169 | def get_related_nodes(self, node, parent=False, child=False): 170 | uniq = {} 171 | for edge in self.edges: 172 | if edge.folded: 173 | continue 174 | 175 | if parent and edge.node2 == node: 176 | uniq[edge.node1] = 1 177 | elif child and edge.node1 == node: 178 | uniq[edge.node2] = 1 179 | 180 | related = [] 181 | for uniq_node in uniq.keys(): 182 | if uniq_node == node: 183 | pass 184 | else: 185 | related.append(uniq_node) 186 | 187 | related.sort(key=lambda x: x.order) 188 | return related 189 | 190 | def get_parent_nodes(self, node): 191 | return self.get_related_nodes(node, parent=True) 192 | 193 | def get_child_nodes(self, node): 194 | return self.get_related_nodes(node, child=True) 195 | 196 | def detect_circulars(self): 197 | for node in self.diagram.nodes: 198 | if not [x for x in self.circulars if node in x]: 199 | self.detect_circulars_sub(node, [node]) 200 | 201 | # remove part of other circular 202 | for c1 in self.circulars: 203 | for c2 in self.circulars: 204 | intersect = set(c1) & set(c2) 205 | 206 | if c1 != c2 and set(c1) == intersect: 207 | self.circulars.remove(c1) 208 | break 209 | 210 | def detect_circulars_sub(self, node, parents): 211 | for child in self.get_child_nodes(node): 212 | if child in parents: 213 | i = parents.index(child) 214 | self.circulars.append(parents[i:]) 215 | else: 216 | self.detect_circulars_sub(child, parents + [child]) 217 | 218 | def is_circular_ref(self, node1, node2): 219 | for circular in self.circulars: 220 | if node1 in circular and node2 in circular: 221 | parents = [] 222 | for node in circular: 223 | for parent in self.get_parent_nodes(node): 224 | if parent not in circular: 225 | parents.append(parent) 226 | 227 | parents.sort(key=lambda x: x.order) 228 | 229 | for parent in parents: 230 | children = self.get_child_nodes(parent) 231 | if node1 in children and node2 in children: 232 | if circular.index(node1) > circular.index(node2): 233 | return True 234 | elif node2 in children: 235 | return True 236 | elif node1 in children: 237 | return False 238 | else: 239 | if circular.index(node1) > circular.index(node2): 240 | return True 241 | 242 | return False 243 | 244 | def set_node_width(self, depth=0): 245 | for node in self.diagram.traverse_nodes(): 246 | if node.xy.x != depth: 247 | continue 248 | 249 | for child in self.get_child_nodes(node): 250 | if self.is_circular_ref(node, child): 251 | pass 252 | elif node == child: 253 | pass 254 | elif child.xy.x > node.xy.x + node.colwidth: 255 | pass 256 | else: 257 | child.xy = XY(node.xy.x + node.colwidth, 0) 258 | 259 | nodes_iter = self.diagram.traverse_nodes() 260 | depther_node = [x for x in nodes_iter if x.xy.x > depth] 261 | if len(depther_node) > 0: 262 | self.set_node_width(depth + 1) 263 | 264 | def adjust_node_order(self): 265 | for node in self.diagram.traverse_nodes(): 266 | parents = self.get_parent_nodes(node) 267 | if len(set(parents)) > 1: 268 | for i in range(1, len(parents)): 269 | idx1 = self.diagram.nodes.index(parents[i - 1]) 270 | idx2 = self.diagram.nodes.index(parents[i]) 271 | if idx1 < idx2: 272 | self.diagram.nodes.remove(parents[i]) 273 | self.diagram.nodes.insert(idx1 + 1, parents[i]) 274 | else: 275 | self.diagram.nodes.remove(parents[i - 1]) 276 | self.diagram.nodes.insert(idx2 + 1, parents[i - 1]) 277 | 278 | if isinstance(node, NodeGroup): 279 | nodes = [n for n in node.nodes if n in self.diagram.nodes] 280 | if nodes: 281 | idx = min(self.diagram.nodes.index(n) for n in nodes) 282 | if idx < self.diagram.nodes.index(node): 283 | self.diagram.nodes.remove(node) 284 | self.diagram.nodes.insert(idx + 1, node) 285 | 286 | self.diagram.update_order() 287 | 288 | def initialize_markers(self): 289 | self.coordinates = {} 290 | for lane in self.diagram.lanes: 291 | self.coordinates[lane] = [] 292 | 293 | def mark_xy(self, node): 294 | xy = node.xy 295 | for w in range(node.colwidth): 296 | for h in range(node.colheight): 297 | self.coordinates[node.lane].append(XY(xy.x + w, xy.y + h)) 298 | 299 | def is_marked(self, lane, xy): 300 | return xy in self.coordinates[lane] 301 | 302 | def set_node_height(self, node, height=0): 303 | xy = XY(node.xy.x, height) 304 | if self.is_marked(node.lane, xy): 305 | return False 306 | node.xy = xy 307 | self.mark_xy(node) 308 | 309 | def cmp(x, y): 310 | if x.xy.x < y.xy.y: 311 | return -1 312 | elif x.xy.x == y.xy.y: 313 | return 0 314 | else: 315 | return 1 316 | 317 | count = 0 318 | children = self.get_child_nodes(node) 319 | children.sort(key=cmp_to_key(cmp)) 320 | for child in children: 321 | if child.id in self.heightRefs: 322 | pass 323 | elif node is not None and node.xy.x >= child.xy.x: 324 | pass 325 | else: 326 | if node.lane == child.lane: 327 | h = height 328 | else: 329 | h = 0 330 | 331 | while True: 332 | if self.set_node_height(child, h): 333 | child.xy = XY(child.xy.x, h) 334 | self.mark_xy(child) 335 | self.heightRefs.append(child.id) 336 | 337 | count += 1 338 | break 339 | elif node.lane != child.lane: 340 | h += 1 341 | else: 342 | if count == 0: 343 | return False 344 | 345 | h += 1 346 | 347 | if node.lane == child.lane: 348 | height = h + 1 349 | 350 | return True 351 | 352 | 353 | class ScreenNodeBuilder(object): 354 | @classmethod 355 | def build(cls, tree, separate=False): 356 | DiagramNode.clear() 357 | DiagramEdge.clear() 358 | NodeGroup.clear() 359 | 360 | diagram = DiagramTreeBuilder().build(tree) 361 | DiagramLayoutManager(diagram).run() 362 | diagram.fixiate(True) 363 | 364 | if diagram.orientation == 'portrait': 365 | cls.rotate_diagram(diagram) 366 | 367 | return diagram 368 | 369 | @classmethod 370 | def rotate_diagram(cls, diagram): 371 | for node in diagram.traverse_nodes(): 372 | node.xy = XY(node.xy.y, node.xy.x) 373 | node.colwidth, node.colheight = (node.colheight, node.colwidth) 374 | 375 | for lane in diagram.lanes: 376 | lane.xy = XY(lane.xy.y, lane.xy.x) 377 | lane.colwidth, lane.colheight = (lane.colheight, lane.colwidth) 378 | 379 | size = (diagram.colheight, diagram.colwidth) 380 | diagram.colwidth, diagram.colheight = size 381 | -------------------------------------------------------------------------------- /src/actdiag/tests/test_rst_directives.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright 2011 Takeshi KOMIYA 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | 16 | import os 17 | import sys 18 | import unittest 19 | 20 | from blockdiag.tests.utils import TemporaryDirectory, capture_stderr, with_pil 21 | from docutils import nodes 22 | from docutils.core import publish_doctree 23 | from docutils.parsers.rst import directives as docutils 24 | 25 | from actdiag.utils.rst import directives 26 | 27 | 28 | class TestRstDirectives(unittest.TestCase): 29 | def setUp(self): 30 | self._tmpdir = TemporaryDirectory() 31 | 32 | def tearDown(self): 33 | if 'actdiag' in docutils._directives: 34 | del docutils._directives['actdiag'] 35 | 36 | self._tmpdir.clean() 37 | 38 | @property 39 | def tmpdir(self): 40 | return self._tmpdir.name 41 | 42 | def test_setup(self): 43 | directives.setup() 44 | options = directives.directive_options 45 | 46 | self.assertIn('actdiag', docutils._directives) 47 | self.assertEqual(directives.ActdiagDirective, 48 | docutils._directives['actdiag']) 49 | self.assertEqual('PNG', options['format']) 50 | self.assertEqual(False, options['antialias']) 51 | self.assertEqual(None, options['fontpath']) 52 | self.assertEqual(False, options['nodoctype']) 53 | self.assertEqual(False, options['noviewbox']) 54 | self.assertEqual(False, options['inline_svg']) 55 | 56 | def test_setup_with_args(self): 57 | directives.setup(format='SVG', antialias=True, fontpath='/dev/null', 58 | nodoctype=True, noviewbox=True, inline_svg=True) 59 | options = directives.directive_options 60 | 61 | self.assertIn('actdiag', docutils._directives) 62 | self.assertEqual(directives.ActdiagDirective, 63 | docutils._directives['actdiag']) 64 | self.assertEqual('SVG', options['format']) 65 | self.assertEqual(True, options['antialias']) 66 | self.assertEqual('/dev/null', options['fontpath']) 67 | self.assertEqual(True, options['nodoctype']) 68 | self.assertEqual(True, options['noviewbox']) 69 | self.assertEqual(True, options['inline_svg']) 70 | 71 | @capture_stderr 72 | def test_cleanup(self): 73 | directives.setup(format='SVG', outputdir=self.tmpdir, noviewbox=False) 74 | text = (".. actdiag::\n" 75 | "\n" 76 | " plugin autoclass\n" 77 | " A -> B") 78 | publish_doctree(text) 79 | 80 | from blockdiag import plugins 81 | self.assertEqual([], plugins.loaded_plugins) 82 | 83 | def test_setup_fontpath1(self): 84 | with self.assertRaises(RuntimeError): 85 | directives.setup(format='SVG', fontpath=['dummy.ttf'], 86 | outputdir=self.tmpdir) 87 | text = (".. actdiag::\n" 88 | "\n" 89 | " A -> B") 90 | publish_doctree(text) 91 | 92 | def test_setup_fontpath2(self): 93 | with self.assertRaises(RuntimeError): 94 | directives.setup(format='SVG', fontpath='dummy.ttf', 95 | outputdir=self.tmpdir) 96 | text = (".. actdiag::\n" 97 | "\n" 98 | " A -> B") 99 | publish_doctree(text) 100 | 101 | def test_setup_nodoctype_is_true(self): 102 | directives.setup(format='SVG', outputdir=self.tmpdir, nodoctype=True) 103 | text = (".. actdiag::\n" 104 | "\n" 105 | " A -> B") 106 | doctree = publish_doctree(text) 107 | self.assertEqual(1, len(doctree)) 108 | self.assertEqual(nodes.image, type(doctree[-1])) 109 | svg = open(doctree[0]['uri']).read() 110 | self.assertNotEqual("\n" 111 | " B") 118 | doctree = publish_doctree(text) 119 | self.assertEqual(1, len(doctree)) 120 | self.assertEqual(nodes.image, type(doctree[0])) 121 | svg = open(doctree[0]['uri']).read() 122 | self.assertEqual("\n" 123 | " B") 130 | doctree = publish_doctree(text) 131 | self.assertEqual(1, len(doctree)) 132 | self.assertEqual(nodes.image, type(doctree[0])) 133 | svg = open(doctree[0]['uri']).read() 134 | self.assertRegexpMatches(svg, r' B") 141 | doctree = publish_doctree(text) 142 | self.assertEqual(1, len(doctree)) 143 | self.assertEqual(nodes.image, type(doctree[0])) 144 | svg = open(doctree[0]['uri']).read() 145 | self.assertRegexpMatches(svg, r' B") 152 | doctree = publish_doctree(text) 153 | self.assertEqual(1, len(doctree)) 154 | self.assertEqual(nodes.raw, type(doctree[0])) 155 | self.assertEqual('html', doctree[0]['format']) 156 | self.assertEqual(nodes.Text, type(doctree[0][0])) 157 | self.assertEqual("\n" 158 | " B") 166 | doctree = publish_doctree(text) 167 | self.assertEqual(1, len(doctree)) 168 | self.assertEqual(nodes.image, type(doctree[0])) 169 | self.assertEqual(1, len(os.listdir(self.tmpdir))) 170 | 171 | @with_pil 172 | def test_setup_inline_svg_is_true_but_format_isnt_svg(self): 173 | directives.setup(format='PNG', outputdir=self.tmpdir, inline_svg=True) 174 | text = (".. actdiag::\n" 175 | "\n" 176 | " A -> B") 177 | doctree = publish_doctree(text) 178 | self.assertEqual(1, len(doctree)) 179 | self.assertEqual(nodes.image, type(doctree[0])) 180 | 181 | def test_setup_inline_svg_is_true_and_width_option1(self): 182 | directives.setup(format='SVG', outputdir=self.tmpdir, 183 | nodoctype=True, noviewbox=True, inline_svg=True) 184 | text = (".. actdiag::\n" 185 | " :width: 100\n" 186 | "\n" 187 | " A -> B") 188 | doctree = publish_doctree(text) 189 | self.assertEqual(1, len(doctree)) 190 | self.assertEqual(nodes.raw, type(doctree[0])) 191 | self.assertEqual(nodes.Text, type(doctree[0][0])) 192 | self.assertRegexpMatches(doctree[0][0], 193 | r' B") 202 | doctree = publish_doctree(text) 203 | self.assertEqual(1, len(doctree)) 204 | self.assertEqual(nodes.raw, type(doctree[0])) 205 | self.assertEqual(nodes.Text, type(doctree[0][0])) 206 | self.assertRegexpMatches(doctree[0][0], 207 | r' B") 216 | doctree = publish_doctree(text) 217 | self.assertEqual(1, len(doctree)) 218 | self.assertEqual(nodes.raw, type(doctree[0])) 219 | self.assertEqual(nodes.Text, type(doctree[0][0])) 220 | self.assertRegexpMatches(doctree[0][0], 221 | r' B") 230 | doctree = publish_doctree(text) 231 | self.assertEqual(1, len(doctree)) 232 | self.assertEqual(nodes.raw, type(doctree[0])) 233 | self.assertEqual(nodes.Text, type(doctree[0][0])) 234 | self.assertRegexpMatches(doctree[0][0], 235 | r' B") 245 | doctree = publish_doctree(text) 246 | self.assertEqual(1, len(doctree)) 247 | self.assertEqual(nodes.raw, type(doctree[0])) 248 | self.assertEqual(nodes.Text, type(doctree[0][0])) 249 | self.assertRegexpMatches(doctree[0][0], 250 | ' B" 259 | " }") 260 | doctree = publish_doctree(text) 261 | self.assertEqual(1, len(doctree)) 262 | self.assertEqual(nodes.image, type(doctree[0])) 263 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 264 | 265 | def test_call_without_braces(self): 266 | directives.setup(format='SVG', outputdir=self.tmpdir) 267 | text = (".. actdiag::\n" 268 | "\n" 269 | " A -> B") 270 | doctree = publish_doctree(text) 271 | self.assertEqual(1, len(doctree)) 272 | self.assertEqual(nodes.image, type(doctree[0])) 273 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 274 | 275 | def test_alt_option(self): 276 | directives.setup(format='SVG', outputdir=self.tmpdir) 277 | text = (".. actdiag::\n" 278 | " :alt: hello world\n" 279 | "\n" 280 | " A -> B") 281 | doctree = publish_doctree(text) 282 | self.assertEqual(1, len(doctree)) 283 | self.assertEqual(nodes.image, type(doctree[0])) 284 | self.assertEqual('hello world', doctree[0]['alt']) 285 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 286 | 287 | def test_align_option1(self): 288 | directives.setup(format='SVG', outputdir=self.tmpdir) 289 | text = (".. actdiag::\n" 290 | " :align: left\n" 291 | "\n" 292 | " A -> B") 293 | doctree = publish_doctree(text) 294 | self.assertEqual(1, len(doctree)) 295 | self.assertEqual(nodes.image, type(doctree[0])) 296 | self.assertEqual('left', doctree[0]['align']) 297 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 298 | 299 | def test_align_option2(self): 300 | directives.setup(format='SVG', outputdir=self.tmpdir) 301 | text = (".. actdiag::\n" 302 | " :align: center\n" 303 | "\n" 304 | " A -> B") 305 | doctree = publish_doctree(text) 306 | self.assertEqual(1, len(doctree)) 307 | self.assertEqual(nodes.image, type(doctree[0])) 308 | self.assertEqual('center', doctree[0]['align']) 309 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 310 | 311 | def test_align_option3(self): 312 | directives.setup(format='SVG', outputdir=self.tmpdir) 313 | text = (".. actdiag::\n" 314 | " :align: right\n" 315 | "\n" 316 | " A -> B") 317 | doctree = publish_doctree(text) 318 | self.assertEqual(1, len(doctree)) 319 | self.assertEqual(nodes.image, type(doctree[0])) 320 | self.assertEqual('right', doctree[0]['align']) 321 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 322 | 323 | @capture_stderr 324 | def test_align_option4(self): 325 | directives.setup(format='SVG', outputdir=self.tmpdir) 326 | text = (".. actdiag::\n" 327 | " :align: unknown\n" 328 | "\n" 329 | " A -> B") 330 | doctree = publish_doctree(text) 331 | self.assertEqual(1, len(doctree)) 332 | self.assertEqual(nodes.system_message, type(doctree[0])) 333 | 334 | # clear stderr outputs (ignore ERROR) 335 | from io import StringIO 336 | sys.stderr = StringIO() 337 | 338 | def test_caption_option(self): 339 | directives.setup(format='SVG', outputdir=self.tmpdir) 340 | text = (".. actdiag::\n" 341 | " :caption: hello world\n" 342 | "\n" 343 | " A -> B") 344 | doctree = publish_doctree(text) 345 | self.assertEqual(1, len(doctree)) 346 | self.assertEqual(nodes.figure, type(doctree[0])) 347 | self.assertEqual(2, len(doctree[0])) 348 | self.assertEqual(nodes.image, type(doctree[0][0])) 349 | self.assertEqual(nodes.caption, type(doctree[0][1])) 350 | self.assertEqual(1, len(doctree[0][1])) 351 | self.assertEqual(nodes.Text, type(doctree[0][1][0])) 352 | self.assertEqual('hello world', doctree[0][1][0]) 353 | 354 | def test_caption_option_and_align_option(self): 355 | directives.setup(format='SVG', outputdir=self.tmpdir) 356 | text = (".. actdiag::\n" 357 | " :align: left\n" 358 | " :caption: hello world\n" 359 | "\n" 360 | " A -> B") 361 | doctree = publish_doctree(text) 362 | self.assertEqual(1, len(doctree)) 363 | self.assertEqual(nodes.figure, type(doctree[0])) 364 | self.assertEqual('left', doctree[0]['align']) 365 | self.assertEqual(2, len(doctree[0])) 366 | self.assertEqual(nodes.image, type(doctree[0][0])) 367 | self.assertNotIn('align', doctree[0][0]) 368 | self.assertEqual(nodes.caption, type(doctree[0][1])) 369 | self.assertEqual(1, len(doctree[0][1])) 370 | self.assertEqual(nodes.Text, type(doctree[0][1][0])) 371 | self.assertEqual('hello world', doctree[0][1][0]) 372 | 373 | @capture_stderr 374 | def test_maxwidth_option(self): 375 | directives.setup(format='SVG', outputdir=self.tmpdir) 376 | text = (".. actdiag::\n" 377 | " :maxwidth: 100\n" 378 | "\n" 379 | " A -> B") 380 | doctree = publish_doctree(text) 381 | self.assertEqual(2, len(doctree)) 382 | self.assertEqual(nodes.image, type(doctree[0])) 383 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 384 | self.assertEqual(nodes.system_message, type(doctree[1])) 385 | 386 | def test_width_option(self): 387 | directives.setup(format='SVG', outputdir=self.tmpdir) 388 | text = (".. actdiag::\n" 389 | " :width: 100\n" 390 | "\n" 391 | " A -> B") 392 | doctree = publish_doctree(text) 393 | self.assertEqual(1, len(doctree)) 394 | self.assertEqual(nodes.image, type(doctree[0])) 395 | self.assertEqual('100', doctree[0]['width']) 396 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 397 | 398 | def test_height_option(self): 399 | directives.setup(format='SVG', outputdir=self.tmpdir) 400 | text = (".. actdiag::\n" 401 | " :height: 100\n" 402 | "\n" 403 | " A -> B") 404 | doctree = publish_doctree(text) 405 | self.assertEqual(1, len(doctree)) 406 | self.assertEqual(nodes.image, type(doctree[0])) 407 | self.assertEqual('100', doctree[0]['height']) 408 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 409 | 410 | def test_scale_option(self): 411 | directives.setup(format='SVG', outputdir=self.tmpdir) 412 | text = (".. actdiag::\n" 413 | " :scale: 50%\n" 414 | "\n" 415 | " A -> B") 416 | doctree = publish_doctree(text) 417 | self.assertEqual(1, len(doctree)) 418 | self.assertEqual(nodes.image, type(doctree[0])) 419 | self.assertEqual(50, doctree[0]['scale']) 420 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 421 | 422 | def test_name_option(self): 423 | directives.setup(format='SVG', outputdir=self.tmpdir) 424 | text = (".. actdiag::\n" 425 | " :name: foo%\n" 426 | "\n" 427 | " A -> B") 428 | doctree = publish_doctree(text) 429 | self.assertEqual(1, len(doctree)) 430 | self.assertEqual(nodes.image, type(doctree[0])) 431 | self.assertEqual(['foo%'], doctree[0]['names']) 432 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 433 | 434 | def test_class_option(self): 435 | directives.setup(format='SVG', outputdir=self.tmpdir) 436 | text = (".. actdiag::\n" 437 | " :class: bar%\n" 438 | "\n" 439 | " A -> B") 440 | doctree = publish_doctree(text) 441 | self.assertEqual(1, len(doctree)) 442 | self.assertEqual(nodes.image, type(doctree[0])) 443 | self.assertEqual(['bar'], doctree[0]['classes']) 444 | self.assertEqual(0, doctree[0]['uri'].index(self.tmpdir)) 445 | 446 | def test_figwidth_option1(self): 447 | directives.setup(format='SVG', outputdir=self.tmpdir) 448 | text = (".. actdiag::\n" 449 | " :caption: hello world\n" 450 | " :figwidth: 100\n" 451 | "\n" 452 | " A -> B") 453 | doctree = publish_doctree(text) 454 | self.assertEqual(1, len(doctree)) 455 | self.assertEqual(nodes.figure, type(doctree[0])) 456 | self.assertEqual('100px', doctree[0]['width']) 457 | 458 | def test_figwidth_option2(self): 459 | directives.setup(format='SVG', outputdir=self.tmpdir) 460 | text = (".. actdiag::\n" 461 | " :caption: hello world\n" 462 | " :figwidth: image\n" 463 | "\n" 464 | " A -> B") 465 | doctree = publish_doctree(text) 466 | self.assertEqual(1, len(doctree)) 467 | self.assertEqual(nodes.figure, type(doctree[0])) 468 | self.assertEqual('256px', doctree[0]['width']) 469 | 470 | def test_figclass_option(self): 471 | directives.setup(format='SVG', outputdir=self.tmpdir) 472 | text = (".. actdiag::\n" 473 | " :caption: hello world\n" 474 | " :figclass: baz\n" 475 | "\n" 476 | " A -> B") 477 | doctree = publish_doctree(text) 478 | self.assertEqual(1, len(doctree)) 479 | self.assertEqual(nodes.figure, type(doctree[0])) 480 | self.assertEqual(['baz'], doctree[0]['classes']) 481 | 482 | def test_desctable(self): 483 | directives.setup(format='SVG', outputdir=self.tmpdir) 484 | text = (".. actdiag::\n" 485 | " :desctable:\n" 486 | "\n" 487 | " A [description = foo]" 488 | " B [description = bar]") 489 | doctree = publish_doctree(text) 490 | self.assertEqual(2, len(doctree)) 491 | self.assertEqual(nodes.image, type(doctree[0])) 492 | self.assertEqual(nodes.table, type(doctree[1])) 493 | 494 | # tgroup 495 | self.assertEqual(4, len(doctree[1][0])) 496 | self.assertEqual(nodes.colspec, type(doctree[1][0][0])) 497 | self.assertEqual(nodes.colspec, type(doctree[1][0][1])) 498 | self.assertEqual(nodes.thead, type(doctree[1][0][2])) 499 | self.assertEqual(nodes.tbody, type(doctree[1][0][3])) 500 | 501 | # colspec 502 | self.assertEqual(50, doctree[1][0][0]['colwidth']) 503 | self.assertEqual(50, doctree[1][0][1]['colwidth']) 504 | 505 | # thead 506 | thead = doctree[1][0][2] 507 | self.assertEqual(2, len(thead[0])) 508 | self.assertEqual('Name', thead[0][0][0][0]) 509 | self.assertEqual('Description', thead[0][1][0][0]) 510 | 511 | # tbody 512 | tbody = doctree[1][0][3] 513 | self.assertEqual(2, len(tbody)) 514 | self.assertEqual('A', tbody[0][0][0][0]) 515 | self.assertEqual('foo', tbody[0][1][0][0]) 516 | self.assertEqual('B', tbody[1][0][0][0]) 517 | self.assertEqual('bar', tbody[1][1][0][0]) 518 | 519 | def test_desctable_option_without_description(self): 520 | directives.setup(format='SVG', outputdir=self.tmpdir) 521 | text = (".. actdiag::\n" 522 | " :desctable:\n" 523 | "\n" 524 | " A -> B") 525 | doctree = publish_doctree(text) 526 | self.assertEqual(1, len(doctree)) 527 | self.assertEqual(nodes.image, type(doctree[0])) 528 | 529 | def test_desctable_option_with_rest_markups(self): 530 | directives.setup(format='SVG', outputdir=self.tmpdir) 531 | text = (".. actdiag::\n" 532 | " :desctable:\n" 533 | "\n" 534 | " A [description = \"foo *bar* **baz**\"]" 535 | " B [description = \"**foo** *bar* baz\"]") 536 | doctree = publish_doctree(text) 537 | self.assertEqual(2, len(doctree)) 538 | self.assertEqual(nodes.image, type(doctree[0])) 539 | self.assertEqual(nodes.table, type(doctree[1])) 540 | 541 | # tgroup 542 | self.assertEqual(4, len(doctree[1][0])) 543 | self.assertEqual(nodes.colspec, type(doctree[1][0][0])) 544 | self.assertEqual(nodes.colspec, type(doctree[1][0][1])) 545 | self.assertEqual(nodes.thead, type(doctree[1][0][2])) 546 | self.assertEqual(nodes.tbody, type(doctree[1][0][3])) 547 | 548 | # colspec 549 | self.assertEqual(50, doctree[1][0][0]['colwidth']) 550 | self.assertEqual(50, doctree[1][0][1]['colwidth']) 551 | 552 | # thead 553 | thead = doctree[1][0][2] 554 | self.assertEqual(2, len(thead[0])) 555 | self.assertEqual('Name', thead[0][0][0][0]) 556 | self.assertEqual('Description', thead[0][1][0][0]) 557 | 558 | # tbody 559 | tbody = doctree[1][0][3] 560 | self.assertEqual(2, len(tbody)) 561 | self.assertEqual('A', tbody[0][0][0][0]) 562 | self.assertEqual(4, len(tbody[0][1][0])) 563 | self.assertEqual(nodes.Text, type(tbody[0][1][0][0])) 564 | self.assertEqual('foo ', str(tbody[0][1][0][0])) 565 | self.assertEqual(nodes.emphasis, type(tbody[0][1][0][1])) 566 | self.assertEqual(nodes.Text, type(tbody[0][1][0][1][0])) 567 | self.assertEqual('bar', tbody[0][1][0][1][0]) 568 | self.assertEqual(nodes.Text, type(tbody[0][1][0][2])) 569 | self.assertEqual(' ', str(tbody[0][1][0][2])) 570 | self.assertEqual(nodes.strong, type(tbody[0][1][0][3])) 571 | self.assertEqual(nodes.Text, type(tbody[0][1][0][3][0])) 572 | self.assertEqual('baz', str(tbody[0][1][0][3][0])) 573 | 574 | self.assertEqual('B', tbody[1][0][0][0]) 575 | self.assertEqual(4, len(tbody[1][1][0])) 576 | self.assertEqual(nodes.strong, type(tbody[1][1][0][0])) 577 | self.assertEqual(nodes.Text, type(tbody[1][1][0][0][0])) 578 | self.assertEqual('foo', str(tbody[1][1][0][0][0])) 579 | self.assertEqual(nodes.Text, type(tbody[1][1][0][1])) 580 | self.assertEqual(' ', str(tbody[1][1][0][1])) 581 | self.assertEqual(nodes.emphasis, type(tbody[1][1][0][2])) 582 | self.assertEqual(nodes.Text, type(tbody[1][1][0][2][0])) 583 | self.assertEqual('bar', str(tbody[1][1][0][2][0])) 584 | self.assertEqual(nodes.Text, type(tbody[1][1][0][3])) 585 | self.assertEqual(' baz', str(tbody[1][1][0][3])) 586 | 587 | def test_desctable_option_with_numbered(self): 588 | directives.setup(format='SVG', outputdir=self.tmpdir) 589 | text = (".. actdiag::\n" 590 | " :desctable:\n" 591 | "\n" 592 | " A [numbered = 2]" 593 | " B [numbered = 1]") 594 | doctree = publish_doctree(text) 595 | self.assertEqual(2, len(doctree)) 596 | self.assertEqual(nodes.image, type(doctree[0])) 597 | self.assertEqual(nodes.table, type(doctree[1])) 598 | 599 | # tgroup 600 | self.assertEqual(4, len(doctree[1][0])) 601 | self.assertEqual(nodes.colspec, type(doctree[1][0][0])) 602 | self.assertEqual(nodes.colspec, type(doctree[1][0][1])) 603 | self.assertEqual(nodes.thead, type(doctree[1][0][2])) 604 | self.assertEqual(nodes.tbody, type(doctree[1][0][3])) 605 | 606 | # colspec 607 | self.assertEqual(25, doctree[1][0][0]['colwidth']) 608 | self.assertEqual(50, doctree[1][0][1]['colwidth']) 609 | 610 | # thead 611 | thead = doctree[1][0][2] 612 | self.assertEqual(2, len(thead[0])) 613 | self.assertEqual('No', thead[0][0][0][0]) 614 | self.assertEqual('Name', thead[0][1][0][0]) 615 | 616 | # tbody 617 | tbody = doctree[1][0][3] 618 | self.assertEqual(2, len(tbody)) 619 | self.assertEqual('1', tbody[0][0][0][0]) 620 | self.assertEqual('B', tbody[0][1][0][0]) 621 | self.assertEqual('2', tbody[1][0][0][0]) 622 | self.assertEqual('A', tbody[1][1][0][0]) 623 | 624 | def test_desctable_option_with_numbered_and_description(self): 625 | directives.setup(format='SVG', outputdir=self.tmpdir) 626 | text = (".. actdiag::\n" 627 | " :desctable:\n" 628 | "\n" 629 | " A [description = foo, numbered = 2]" 630 | " B [description = bar, numbered = 1]") 631 | doctree = publish_doctree(text) 632 | self.assertEqual(2, len(doctree)) 633 | self.assertEqual(nodes.image, type(doctree[0])) 634 | self.assertEqual(nodes.table, type(doctree[1])) 635 | 636 | # tgroup 637 | self.assertEqual(5, len(doctree[1][0])) 638 | self.assertEqual(nodes.colspec, type(doctree[1][0][0])) 639 | self.assertEqual(nodes.colspec, type(doctree[1][0][1])) 640 | self.assertEqual(nodes.colspec, type(doctree[1][0][2])) 641 | self.assertEqual(nodes.thead, type(doctree[1][0][3])) 642 | self.assertEqual(nodes.tbody, type(doctree[1][0][4])) 643 | 644 | # colspec 645 | self.assertEqual(25, doctree[1][0][0]['colwidth']) 646 | self.assertEqual(50, doctree[1][0][1]['colwidth']) 647 | self.assertEqual(50, doctree[1][0][2]['colwidth']) 648 | 649 | # thead 650 | thead = doctree[1][0][3] 651 | self.assertEqual(3, len(thead[0])) 652 | self.assertEqual('No', thead[0][0][0][0]) 653 | self.assertEqual('Name', thead[0][1][0][0]) 654 | self.assertEqual('Description', thead[0][2][0][0]) 655 | 656 | # tbody 657 | tbody = doctree[1][0][4] 658 | self.assertEqual(2, len(tbody)) 659 | self.assertEqual('1', tbody[0][0][0][0]) 660 | self.assertEqual('B', tbody[0][1][0][0]) 661 | self.assertEqual(1, len(tbody[0][2])) 662 | self.assertEqual('bar', tbody[0][2][0][0]) 663 | self.assertEqual('2', tbody[1][0][0][0]) 664 | self.assertEqual('A', tbody[1][1][0][0]) 665 | self.assertEqual('foo', tbody[1][2][0][0]) 666 | 667 | def test_desctable_option_for_edges(self): 668 | directives.setup(format='SVG', outputdir=self.tmpdir) 669 | text = (".. actdiag::\n" 670 | " :desctable:\n" 671 | "\n" 672 | " A -> B [description = \"foo\"]" 673 | " C -> D [description = \"bar\"]" 674 | " C [label = \"label_C\"]" 675 | " D [label = \"label_D\"]") 676 | doctree = publish_doctree(text) 677 | self.assertEqual(2, len(doctree)) 678 | self.assertEqual(nodes.image, type(doctree[0])) 679 | self.assertEqual(nodes.table, type(doctree[1])) 680 | 681 | # tgroup 682 | self.assertEqual(4, len(doctree[1][0])) 683 | self.assertEqual(nodes.colspec, type(doctree[1][0][0])) 684 | self.assertEqual(nodes.colspec, type(doctree[1][0][1])) 685 | self.assertEqual(nodes.thead, type(doctree[1][0][2])) 686 | self.assertEqual(nodes.tbody, type(doctree[1][0][3])) 687 | 688 | # colspec 689 | self.assertEqual(25, doctree[1][0][0]['colwidth']) 690 | self.assertEqual(50, doctree[1][0][1]['colwidth']) 691 | 692 | # thead 693 | thead = doctree[1][0][2] 694 | self.assertEqual(2, len(thead[0])) 695 | self.assertEqual('Name', thead[0][0][0][0]) 696 | self.assertEqual('Description', thead[0][1][0][0]) 697 | 698 | # tbody 699 | tbody = doctree[1][0][3] 700 | self.assertEqual(2, len(tbody)) 701 | self.assertEqual('A -> B', tbody[0][0][0][0]) 702 | self.assertEqual(1, len(tbody[0][1][0])) 703 | self.assertEqual(nodes.Text, type(tbody[0][1][0][0])) 704 | self.assertEqual('foo', str(tbody[0][1][0][0])) 705 | self.assertEqual('label_C -> label_D', tbody[1][0][0][0]) 706 | self.assertEqual(1, len(tbody[1][1][0])) 707 | self.assertEqual(nodes.Text, type(tbody[1][1][0][0])) 708 | self.assertEqual('bar', str(tbody[1][1][0][0])) 709 | 710 | def test_desctable_option_for_nodes_and_edges(self): 711 | directives.setup(format='SVG', outputdir=self.tmpdir) 712 | text = (".. actdiag::\n" 713 | " :desctable:\n" 714 | "\n" 715 | " A -> B [description = \"foo\"]" 716 | " C -> D [description = \"bar\"]" 717 | " C [label = \"label_C\", description = foo]" 718 | " D [label = \"label_D\"]") 719 | doctree = publish_doctree(text) 720 | self.assertEqual(3, len(doctree)) 721 | self.assertEqual(nodes.image, type(doctree[0])) 722 | self.assertEqual(nodes.table, type(doctree[1])) 723 | self.assertEqual(nodes.table, type(doctree[2])) 724 | 725 | # tgroup 726 | self.assertEqual(4, len(doctree[2][0])) 727 | self.assertEqual(nodes.colspec, type(doctree[2][0][0])) 728 | self.assertEqual(nodes.colspec, type(doctree[2][0][1])) 729 | self.assertEqual(nodes.thead, type(doctree[2][0][2])) 730 | self.assertEqual(nodes.tbody, type(doctree[2][0][3])) 731 | 732 | # colspec 733 | self.assertEqual(25, doctree[2][0][0]['colwidth']) 734 | self.assertEqual(50, doctree[2][0][1]['colwidth']) 735 | 736 | # thead 737 | thead = doctree[2][0][2] 738 | self.assertEqual(2, len(thead[0])) 739 | self.assertEqual('Name', thead[0][0][0][0]) 740 | self.assertEqual('Description', thead[0][1][0][0]) 741 | 742 | # tbody 743 | tbody = doctree[2][0][3] 744 | self.assertEqual(2, len(tbody)) 745 | self.assertEqual('A -> B', tbody[0][0][0][0]) 746 | self.assertEqual(1, len(tbody[0][1][0])) 747 | self.assertEqual(nodes.Text, type(tbody[0][1][0][0])) 748 | self.assertEqual('foo', str(tbody[0][1][0][0])) 749 | self.assertEqual('label_C -> label_D', tbody[1][0][0][0]) 750 | self.assertEqual(1, len(tbody[1][1][0])) 751 | self.assertEqual(nodes.Text, type(tbody[1][1][0][0])) 752 | self.assertEqual('bar', str(tbody[1][1][0][0])) 753 | --------------------------------------------------------------------------------