pyDiffTools 0.1.31__tar.gz → 0.1.33__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {pydifftools-0.1.31 → pydifftools-0.1.33}/MANIFEST.in +2 -0
  2. {pydifftools-0.1.31/pyDiffTools.egg-info → pydifftools-0.1.33}/PKG-INFO +1 -1
  3. {pydifftools-0.1.31 → pydifftools-0.1.33/pyDiffTools.egg-info}/PKG-INFO +1 -1
  4. {pydifftools-0.1.31 → pydifftools-0.1.33}/pyDiffTools.egg-info/SOURCES.txt +2 -0
  5. pydifftools-0.1.33/pydifftools/doc_contents.py +206 -0
  6. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/flowchart/comparison.py +3 -4
  7. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/flowchart/graph.py +4 -1
  8. pydifftools-0.1.33/pydifftools/flowchart/svg-pan-zoom.min.js +30 -0
  9. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/flowchart/watch_graph.py +38 -7
  10. pydifftools-0.1.33/pydifftools/flowchart/wgrph_view.js +212 -0
  11. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/outline.py +2 -2
  12. {pydifftools-0.1.31 → pydifftools-0.1.33}/pyproject.toml +5 -1
  13. pydifftools-0.1.31/pydifftools/doc_contents.py +0 -162
  14. {pydifftools-0.1.31 → pydifftools-0.1.33}/LICENSE.md +0 -0
  15. {pydifftools-0.1.31 → pydifftools-0.1.33}/README.rst +0 -0
  16. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/_quarto.yml +0 -0
  17. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/__version__.txt +0 -0
  18. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/1a724af72b16f5a9e607e12b1c721645/base.ipynb +0 -0
  19. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/1b28fc9daac9081847e5161b2c546f8a/base.ipynb +0 -0
  20. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/231f64eee282fa225d1104935cf80a24/base.ipynb +0 -0
  21. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/26e56f6b0ff54851a45145157f2f0dc4/base.ipynb +0 -0
  22. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/311fabd7029ffd050d056e2f316eb50f/base.ipynb +0 -0
  23. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/57da2021e5b156ac3adf01398201c723/base.ipynb +0 -0
  24. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/62b24ea7da75011d92b0f8924faa208d/base.ipynb +0 -0
  25. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/7f1b20d69d889514ab5d1cc92e3cb14f/base.ipynb +0 -0
  26. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/86f74c8c54a87ff892d9b15dd714e8f0/base.ipynb +0 -0
  27. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/88893b4234eac2945d9d6cb2e277f186/base.ipynb +0 -0
  28. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/9a40046ada6f582ee34af00fbdbfb417/base.ipynb +0 -0
  29. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/a1bf4d270d0641ff41faf1d7cce3439a/base.ipynb +0 -0
  30. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/a3789f7d9585a781f2a1c60ce95ff10d/base.ipynb +0 -0
  31. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/be46ecba858d39ad5f0c46902ddf1c02/base.ipynb +0 -0
  32. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/d0cbc57a12f2ccce710a5afd04cc05e7/base.ipynb +0 -0
  33. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/d3e12d320b14228f701231ae32ddd7dd/base.ipynb +0 -0
  34. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/eb434f61555438d020a6970a5dbf9ee8/base.ipynb +0 -0
  35. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/f6b0e73aa7fa029134665d4dde57e096/base.ipynb +0 -0
  36. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/f719a6e4ff09873cb0ffb06ec9d232f9/base.ipynb +0 -0
  37. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/executed/feeee244a7ce3d60e1a227eb604df823/base.ipynb +0 -0
  38. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/.jupyter_cache/global.db +0 -0
  39. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/example.qmd +0 -0
  40. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/example.tex +0 -0
  41. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/index.qmd +0 -0
  42. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/.jupyter_cache/__version__.txt +0 -0
  43. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/.jupyter_cache/executed/ca90e4df5f4f0583df6554156a68dc7f/base.ipynb +0 -0
  44. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/.jupyter_cache/global.db +0 -0
  45. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/end_vs_he_sketch.jpg +0 -0
  46. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/independent.qmd +0 -0
  47. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/index.qmd +0 -0
  48. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/tasks.qmd +0 -0
  49. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/test_include.qmd +0 -0
  50. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/subproject1/tryforerror.qmd +0 -0
  51. {pydifftools-0.1.31 → pydifftools-0.1.33}/example_notebook/project1/test_include.qmd +0 -0
  52. {pydifftools-0.1.31 → pydifftools-0.1.33}/pyDiffTools.egg-info/dependency_links.txt +0 -0
  53. {pydifftools-0.1.31 → pydifftools-0.1.33}/pyDiffTools.egg-info/entry_points.txt +0 -0
  54. {pydifftools-0.1.31 → pydifftools-0.1.33}/pyDiffTools.egg-info/requires.txt +0 -0
  55. {pydifftools-0.1.31 → pydifftools-0.1.33}/pyDiffTools.egg-info/top_level.txt +0 -0
  56. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/__init__.py +0 -0
  57. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/browser_lifecycle.py +0 -0
  58. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/check_numbers.py +0 -0
  59. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/command_line.py +0 -0
  60. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/command_registry.py +0 -0
  61. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/comment_functions.py +0 -0
  62. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/comment_tags.lua +0 -0
  63. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/comment_tags_margin.lua +0 -0
  64. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/comment_tags_no_comments.lua +0 -0
  65. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/comment_toggle.js +0 -0
  66. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/comments.css +0 -0
  67. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/continuous.py +0 -0
  68. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/copy_files.py +0 -0
  69. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/diff-doc.js +0 -0
  70. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/flowchart/__init__.py +0 -0
  71. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/flowchart/dot_to_yaml.py +0 -0
  72. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/forward_search.py +0 -0
  73. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/git_gd.py +0 -0
  74. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/git_gd_history.py +0 -0
  75. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/git_gd_image.py +0 -0
  76. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/git_gd_qt.py +0 -0
  77. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/html_comments.py +0 -0
  78. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/html_uncomments.py +0 -0
  79. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/log_example.py +0 -0
  80. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/match_spaces.py +0 -0
  81. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/notebook/__init__.py +0 -0
  82. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/notebook/display.py +0 -0
  83. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/notebook/fast_build.py +0 -0
  84. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/notebook/tex_to_qmd.py +0 -0
  85. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/onewordify.py +0 -0
  86. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/onewordify_undo.py +0 -0
  87. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/rearrange_tex.py +0 -0
  88. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/searchacro.py +0 -0
  89. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/separate_comments.py +0 -0
  90. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/split_conflict.py +0 -0
  91. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/unseparate_comments.py +0 -0
  92. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/update_check.py +0 -0
  93. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/wrap_sentences.py +0 -0
  94. {pydifftools-0.1.31 → pydifftools-0.1.33}/pydifftools/xml2xlsx.vbs +0 -0
  95. {pydifftools-0.1.31 → pydifftools-0.1.33}/setup.cfg +0 -0
  96. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_browser_lifecycle.py +0 -0
  97. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_check_single_use.py +0 -0
  98. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_command_line_help.py +0 -0
  99. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_continuous_shutdown.py +0 -0
  100. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_forward_search.py +0 -0
  101. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_git_gd_history.py +0 -0
  102. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_git_gd_image.py +0 -0
  103. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_git_gd_qt.py +0 -0
  104. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_rrng.py +0 -0
  105. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_tex_to_qmd.py +0 -0
  106. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_update_check.py +0 -0
  107. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_wrap_check.py +0 -0
  108. {pydifftools-0.1.31 → pydifftools-0.1.33}/tests/test_wrapping.py +0 -0
@@ -8,4 +8,6 @@ include pydifftools/comment_tags.lua
8
8
  include pydifftools/comment_tags_margin.lua
9
9
  include pydifftools/comment_tags_no_comments.lua
10
10
  include pydifftools/comment_toggle.js
11
+ include pydifftools/flowchart/svg-pan-zoom.min.js
12
+ include pydifftools/flowchart/wgrph_view.js
11
13
  recursive-include example_notebook *
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.31
3
+ Version: 0.1.33
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyDiffTools
3
- Version: 0.1.31
3
+ Version: 0.1.33
4
4
  Summary: Diff tools
5
5
  Author: J M Franck
6
6
  License: Copyright (c) 2015, jmfranck
@@ -83,7 +83,9 @@ pydifftools/flowchart/__init__.py
83
83
  pydifftools/flowchart/comparison.py
84
84
  pydifftools/flowchart/dot_to_yaml.py
85
85
  pydifftools/flowchart/graph.py
86
+ pydifftools/flowchart/svg-pan-zoom.min.js
86
87
  pydifftools/flowchart/watch_graph.py
88
+ pydifftools/flowchart/wgrph_view.js
87
89
  pydifftools/notebook/__init__.py
88
90
  pydifftools/notebook/display.py
89
91
  pydifftools/notebook/fast_build.py
@@ -0,0 +1,206 @@
1
+ from collections import OrderedDict
2
+ import re
3
+ from fuzzywuzzy import fuzz
4
+
5
+
6
+ class doc_contents_class(object):
7
+ def __init__(self, format_type="latex"):
8
+ self.contents = OrderedDict()
9
+ self.contents["header"] = ""
10
+ self.types = {}
11
+ self.types["header"] = "header"
12
+ self._aliases = {}
13
+ self._processed_titles = []
14
+ self.set_format(format_type)
15
+
16
+ def set_format(self, format_type):
17
+ if format_type == "markdown":
18
+ # markdown levels go deeper, so include subparagraph mapping
19
+ self.level_numbers = {
20
+ "section": 1,
21
+ "subsection": 2,
22
+ "subsubsection": 3,
23
+ "paragraph": 4,
24
+ "subparagraph": 5,
25
+ }
26
+ else:
27
+ # default to latex behavior
28
+ self.level_numbers = {
29
+ "section": 1,
30
+ "subsection": 2,
31
+ "subsubsection": 3,
32
+ "paragraph": 4,
33
+ "subparagraph": 5,
34
+ }
35
+ # map indentation back to section type for outline parsing
36
+ self.inv_prefix = {
37
+ (level - 1) * "\t": section
38
+ for section, level in self.level_numbers.items()
39
+ }
40
+ self.format_type = format_type
41
+
42
+ def start_sec(self, thistype, thistitle):
43
+ assert thistitle not in self.contents.keys(), (
44
+ "more than one section with the name:\n" + thistitle
45
+ )
46
+ self.contents[thistitle] = ""
47
+ self.types[thistitle] = thistype
48
+ print("added", thistitle)
49
+
50
+ def __setstate__(self, d):
51
+ "set the info from a pickle"
52
+ self.contents = d["contents"]
53
+ self.types = d["types"]
54
+ self._aliases = {} # doesn't exist, but still needed
55
+ self._processed_titles = []
56
+ if "format_type" in d:
57
+ self.set_format(d["format_type"])
58
+ else:
59
+ self.set_format("latex")
60
+ return
61
+
62
+ def __getstate__(self):
63
+ "return info for a pickle"
64
+ return {
65
+ "contents": self.contents,
66
+ "types": self.types,
67
+ "format_type": self.format_type,
68
+ }
69
+
70
+ def __iadd__(self, value):
71
+ self.contents[next(reversed(self.contents))] += value
72
+ return self
73
+
74
+ def __str__(self):
75
+ if len(self._processed_titles) > 0:
76
+ raise ValueError(
77
+ "the following section titles are missing from the"
78
+ " outline:\n\t"
79
+ + "\n\t".join(self._processed_titles)
80
+ + "\nEvery existing section must appear in the outline, so"
81
+ " that entire sections of text are never dropped by"
82
+ " accident. If you really want to get rid of a section,"
83
+ " add a new heading (e.g. 'for deletion') to the outline"
84
+ " and move the unwanted sections underneath it; then delete"
85
+ " that part of the file by hand."
86
+ )
87
+ retval = ""
88
+ for j in self.contents.keys():
89
+ if self.types[j] != "header":
90
+ new_name = j
91
+ if j in self._aliases.keys():
92
+ new_name = self._aliases[j]
93
+ if self.format_type == "markdown":
94
+ retval += "#" * self.level_numbers[self.types[j]]
95
+ retval += f" {new_name}\n"
96
+ else:
97
+ retval += f"\\{self.types[j]}{{{new_name}}}"
98
+ retval += f"{self.contents[j]}"
99
+ return retval
100
+
101
+ @property
102
+ def outline(self):
103
+ retval = []
104
+ for j in self.contents.keys():
105
+ if self.types[j] != "header":
106
+ indent = (self.level_numbers[self.types[j]] - 1) * "\t"
107
+ thistitle = (indent + "\t").join(j.split("\n"))
108
+ retval.append(indent + "*\t" + thistitle)
109
+ return "\n".join(retval)
110
+
111
+ def outline_in_order(self, outline_lines):
112
+ self._processed_titles = [
113
+ j for j in self.contents.keys() if self.types[j] != "header"
114
+ ]
115
+ # {{{ parse (level, title) from each outline line
116
+ parsed = []
117
+ for thisline in outline_lines:
118
+ if not thisline.strip():
119
+ continue
120
+ ilevel = 0
121
+ spacelevel = 0
122
+ hitmarker = False
123
+ for j, thischar in enumerate(thisline):
124
+ if not hitmarker:
125
+ if thischar == " ":
126
+ spacelevel += 1
127
+ if spacelevel == 4 or thischar == "\t":
128
+ ilevel += 1
129
+ spacelevel = 0
130
+ elif thischar == "*":
131
+ hitmarker = True
132
+ else:
133
+ assert thischar in [" ", "\t"]
134
+ title = thisline[j + 1 :]
135
+ break
136
+ if not hitmarker:
137
+ raise ValueError("somehow, there wasn't a * marker!")
138
+ parsed.append((ilevel, title))
139
+ # }}}
140
+ # {{{ resolve titles that aren't existing sections -- only sections
141
+ # that appear nowhere in the outline can have been renamed
142
+ outline_titles = {title for _, title in parsed}
143
+ orphans = [
144
+ j
145
+ for j in self.contents.keys()
146
+ if self.types[j] != "header" and j not in outline_titles
147
+ ]
148
+ renamed = {}
149
+ for _, title in parsed:
150
+ if title in self.contents.keys() or title in renamed:
151
+ continue
152
+ if re.search(r"\bNEW\b", title):
153
+ print(f"adding\n\t{title}\nas a new section (labeled NEW)")
154
+ elif len(orphans) == 0:
155
+ print(
156
+ f"adding\n\t{title}\nas a new section (no unplaced"
157
+ " sections are left that it could be a rename of)"
158
+ )
159
+ else:
160
+ ranked = sorted(
161
+ orphans,
162
+ key=lambda j: fuzz.token_sort_ratio(title, j),
163
+ reverse=True,
164
+ )[:5]
165
+ choices = "".join(
166
+ f"\n {n + 1}) [{fuzz.token_sort_ratio(title, j)}] {j}"
167
+ for n, j in enumerate(ranked)
168
+ )
169
+ prompt = (
170
+ f"didn't find\n\t{title}\namong the existing sections."
171
+ " Is it a renamed version of one of these sections that"
172
+ f" don't appear in the outline?{choices}\nenter a number"
173
+ " to rename, n to add it as a new, empty section, or q"
174
+ " to quit without writing anything: "
175
+ )
176
+ while True:
177
+ answer = input(prompt).strip()
178
+ if answer in ("n", "q"):
179
+ break
180
+ if answer.isdigit() and 1 <= int(answer) <= len(ranked):
181
+ break
182
+ print(f"didn't understand {answer!r}")
183
+ if answer == "q":
184
+ raise ValueError("aborted -- nothing was written")
185
+ if answer != "n":
186
+ original = ranked[int(answer) - 1]
187
+ self._aliases[original] = title # will be replaced later
188
+ renamed[title] = original
189
+ orphans.remove(original)
190
+ continue
191
+ # a new section, so it isn't on the list of titles to place
192
+ self.contents[title] = ""
193
+ self._processed_titles.append(title)
194
+ # }}}
195
+ # {{{ reorder sections to match the outline
196
+ for ilevel, title in parsed:
197
+ title = renamed.get(title, title)
198
+ if title not in self._processed_titles:
199
+ raise ValueError(
200
+ f"the section\n\t{title}\nappears more than once in the"
201
+ " outline"
202
+ )
203
+ self.contents.move_to_end(title)
204
+ self._processed_titles.remove(title)
205
+ self.types[title] = self.inv_prefix[ilevel * "\t"]
206
+ # }}}
@@ -230,8 +230,9 @@ class PlanComparison:
230
230
  before.get("text", ""), after.get("text", ""), width
231
231
  )
232
232
  if state in ("added", "removed"):
233
- prefix = "added:" if state == "added" else "deleted:"
234
- body = f"<b>{prefix}</b><br/>{body}"
233
+ node["_comparison_status"] = (
234
+ "added" if state == "added" else "deleted"
235
+ )
235
236
  if previous is not None and previous != name:
236
237
  body = (
237
238
  f'<font color="#c62828"><s>{html.escape(previous)}</s>'
@@ -327,5 +328,3 @@ class PlanComparison:
327
328
  item.set("fill", "#888888")
328
329
  if state == "removed":
329
330
  item.set("text-decoration", "line-through")
330
- elif state == "changed" and item.text in self.node_states:
331
- item.set("fill", "#16803c")
@@ -477,8 +477,11 @@ def _append_node(
477
477
  )
478
478
  if "_comparison_label" in node:
479
479
  label = "<" + node["_comparison_label"] + ">"
480
+ status = node.get("_comparison_status")
481
+ status_prefix = f"<b>{status}:</b> " if status else ""
480
482
  task_link_line = (
481
- f'<font point-size="9">__WGRPH_TASK_LINK__:{node_name}</font>'
483
+ f'<font point-size="9">{status_prefix}'
484
+ f"__WGRPH_TASK_LINK__:{node_name}</font>"
482
485
  )
483
486
  if label:
484
487
  label = "<" + task_link_line + '<br align="left"/>' + label[1:]
@@ -0,0 +1,30 @@
1
+ /*
2
+ * Vendored unmodified from svg-pan-zoom 3.6.2 (BSD 2-Clause license):
3
+ *
4
+ * Copyright 2009-2010 Andrea Leofreddi <a.leofreddi@vleo.net>
5
+ * All rights reserved.
6
+ *
7
+ * Redistribution and use in source and binary forms, with or without modification,
8
+ * are permitted provided that the following conditions are met:
9
+ *
10
+ * * Redistributions of source code must retain the above copyright notice, this
11
+ * list of conditions and the following disclaimer.
12
+ *
13
+ * * Redistributions in binary form must reproduce the above copyright notice, this
14
+ * list of conditions and the following disclaimer in the documentation and/or
15
+ * other materials provided with the distribution.
16
+ *
17
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
18
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
21
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
24
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
26
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+ */
28
+ // svg-pan-zoom v3.6.2
29
+ // https://github.com/bumbu/svg-pan-zoom
30
+ !function s(r,a,l){function u(e,t){if(!a[e]){if(!r[e]){var o="function"==typeof require&&require;if(!t&&o)return o(e,!0);if(h)return h(e,!0);var n=new Error("Cannot find module '"+e+"'");throw n.code="MODULE_NOT_FOUND",n}var i=a[e]={exports:{}};r[e][0].call(i.exports,function(t){return u(r[e][1][t]||t)},i,i.exports,s,r,a,l)}return a[e].exports}for(var h="function"==typeof require&&require,t=0;t<l.length;t++)u(l[t]);return u}({1:[function(t,e,o){var s=t("./svg-utilities");e.exports={enable:function(t){var e=t.svg.querySelector("defs");if(e||(e=document.createElementNS(s.svgNS,"defs"),t.svg.appendChild(e)),!e.querySelector("style#svg-pan-zoom-controls-styles")){var o=document.createElementNS(s.svgNS,"style");o.setAttribute("id","svg-pan-zoom-controls-styles"),o.setAttribute("type","text/css"),o.textContent=".svg-pan-zoom-control { cursor: pointer; fill: black; fill-opacity: 0.333; } .svg-pan-zoom-control:hover { fill-opacity: 0.8; } .svg-pan-zoom-control-background { fill: white; fill-opacity: 0.5; } .svg-pan-zoom-control-background { fill-opacity: 0.8; }",e.appendChild(o)}var n=document.createElementNS(s.svgNS,"g");n.setAttribute("id","svg-pan-zoom-controls"),n.setAttribute("transform","translate("+(t.width-70)+" "+(t.height-76)+") scale(0.75)"),n.setAttribute("class","svg-pan-zoom-control"),n.appendChild(this._createZoomIn(t)),n.appendChild(this._createZoomReset(t)),n.appendChild(this._createZoomOut(t)),t.svg.appendChild(n),t.controlIcons=n},_createZoomIn:function(t){var e=document.createElementNS(s.svgNS,"g");e.setAttribute("id","svg-pan-zoom-zoom-in"),e.setAttribute("transform","translate(30.5 5) scale(0.015)"),e.setAttribute("class","svg-pan-zoom-control"),e.addEventListener("click",function(){t.getPublicInstance().zoomIn()},!1),e.addEventListener("touchstart",function(){t.getPublicInstance().zoomIn()},!1);var o=document.createElementNS(s.svgNS,"rect");o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("width","1500"),o.setAttribute("height","1400"),o.setAttribute("class","svg-pan-zoom-control-background"),e.appendChild(o);var n=document.createElementNS(s.svgNS,"path");return n.setAttribute("d","M1280 576v128q0 26 -19 45t-45 19h-320v320q0 26 -19 45t-45 19h-128q-26 0 -45 -19t-19 -45v-320h-320q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h320v-320q0 -26 19 -45t45 -19h128q26 0 45 19t19 45v320h320q26 0 45 19t19 45zM1536 1120v-960 q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5t84.5 -203.5z"),n.setAttribute("class","svg-pan-zoom-control-element"),e.appendChild(n),e},_createZoomReset:function(t){var e=document.createElementNS(s.svgNS,"g");e.setAttribute("id","svg-pan-zoom-reset-pan-zoom"),e.setAttribute("transform","translate(5 35) scale(0.4)"),e.setAttribute("class","svg-pan-zoom-control"),e.addEventListener("click",function(){t.getPublicInstance().reset()},!1),e.addEventListener("touchstart",function(){t.getPublicInstance().reset()},!1);var o=document.createElementNS(s.svgNS,"rect");o.setAttribute("x","2"),o.setAttribute("y","2"),o.setAttribute("width","182"),o.setAttribute("height","58"),o.setAttribute("class","svg-pan-zoom-control-background"),e.appendChild(o);var n=document.createElementNS(s.svgNS,"path");n.setAttribute("d","M33.051,20.632c-0.742-0.406-1.854-0.609-3.338-0.609h-7.969v9.281h7.769c1.543,0,2.701-0.188,3.473-0.562c1.365-0.656,2.048-1.953,2.048-3.891C35.032,22.757,34.372,21.351,33.051,20.632z"),n.setAttribute("class","svg-pan-zoom-control-element"),e.appendChild(n);var i=document.createElementNS(s.svgNS,"path");return i.setAttribute("d","M170.231,0.5H15.847C7.102,0.5,0.5,5.708,0.5,11.84v38.861C0.5,56.833,7.102,61.5,15.847,61.5h154.384c8.745,0,15.269-4.667,15.269-10.798V11.84C185.5,5.708,178.976,0.5,170.231,0.5z M42.837,48.569h-7.969c-0.219-0.766-0.375-1.383-0.469-1.852c-0.188-0.969-0.289-1.961-0.305-2.977l-0.047-3.211c-0.03-2.203-0.41-3.672-1.142-4.406c-0.732-0.734-2.103-1.102-4.113-1.102h-7.05v13.547h-7.055V14.022h16.524c2.361,0.047,4.178,0.344,5.45,0.891c1.272,0.547,2.351,1.352,3.234,2.414c0.731,0.875,1.31,1.844,1.737,2.906s0.64,2.273,0.64,3.633c0,1.641-0.414,3.254-1.242,4.84s-2.195,2.707-4.102,3.363c1.594,0.641,2.723,1.551,3.387,2.73s0.996,2.98,0.996,5.402v2.32c0,1.578,0.063,2.648,0.19,3.211c0.19,0.891,0.635,1.547,1.333,1.969V48.569z M75.579,48.569h-26.18V14.022h25.336v6.117H56.454v7.336h16.781v6H56.454v8.883h19.125V48.569z M104.497,46.331c-2.44,2.086-5.887,3.129-10.34,3.129c-4.548,0-8.125-1.027-10.731-3.082s-3.909-4.879-3.909-8.473h6.891c0.224,1.578,0.662,2.758,1.316,3.539c1.196,1.422,3.246,2.133,6.15,2.133c1.739,0,3.151-0.188,4.236-0.562c2.058-0.719,3.087-2.055,3.087-4.008c0-1.141-0.504-2.023-1.512-2.648c-1.008-0.609-2.607-1.148-4.796-1.617l-3.74-0.82c-3.676-0.812-6.201-1.695-7.576-2.648c-2.328-1.594-3.492-4.086-3.492-7.477c0-3.094,1.139-5.664,3.417-7.711s5.623-3.07,10.036-3.07c3.685,0,6.829,0.965,9.431,2.895c2.602,1.93,3.966,4.73,4.093,8.402h-6.938c-0.128-2.078-1.057-3.555-2.787-4.43c-1.154-0.578-2.587-0.867-4.301-0.867c-1.907,0-3.428,0.375-4.565,1.125c-1.138,0.75-1.706,1.797-1.706,3.141c0,1.234,0.561,2.156,1.682,2.766c0.721,0.406,2.25,0.883,4.589,1.43l6.063,1.43c2.657,0.625,4.648,1.461,5.975,2.508c2.059,1.625,3.089,3.977,3.089,7.055C108.157,41.624,106.937,44.245,104.497,46.331z M139.61,48.569h-26.18V14.022h25.336v6.117h-18.281v7.336h16.781v6h-16.781v8.883h19.125V48.569z M170.337,20.14h-10.336v28.43h-7.266V20.14h-10.383v-6.117h27.984V20.14z"),i.setAttribute("class","svg-pan-zoom-control-element"),e.appendChild(i),e},_createZoomOut:function(t){var e=document.createElementNS(s.svgNS,"g");e.setAttribute("id","svg-pan-zoom-zoom-out"),e.setAttribute("transform","translate(30.5 70) scale(0.015)"),e.setAttribute("class","svg-pan-zoom-control"),e.addEventListener("click",function(){t.getPublicInstance().zoomOut()},!1),e.addEventListener("touchstart",function(){t.getPublicInstance().zoomOut()},!1);var o=document.createElementNS(s.svgNS,"rect");o.setAttribute("x","0"),o.setAttribute("y","0"),o.setAttribute("width","1500"),o.setAttribute("height","1400"),o.setAttribute("class","svg-pan-zoom-control-background"),e.appendChild(o);var n=document.createElementNS(s.svgNS,"path");return n.setAttribute("d","M1280 576v128q0 26 -19 45t-45 19h-896q-26 0 -45 -19t-19 -45v-128q0 -26 19 -45t45 -19h896q26 0 45 19t19 45zM1536 1120v-960q0 -119 -84.5 -203.5t-203.5 -84.5h-960q-119 0 -203.5 84.5t-84.5 203.5v960q0 119 84.5 203.5t203.5 84.5h960q119 0 203.5 -84.5 t84.5 -203.5z"),n.setAttribute("class","svg-pan-zoom-control-element"),e.appendChild(n),e},disable:function(t){t.controlIcons&&(t.controlIcons.parentNode.removeChild(t.controlIcons),t.controlIcons=null)}}},{"./svg-utilities":5}],2:[function(t,e,o){function n(t,e){this.init(t,e)}var i=t("./svg-utilities"),r=t("./utilities");n.prototype.init=function(t,e){this.viewport=t,this.options=e,this.originalState={zoom:1,x:0,y:0},this.activeState={zoom:1,x:0,y:0},this.updateCTMCached=r.proxy(this.updateCTM,this),this.requestAnimationFrame=r.createRequestAnimationFrame(this.options.refreshRate),this.viewBox={x:0,y:0,width:0,height:0},this.cacheViewBox();var o=this.processCTM();this.setCTM(o),this.updateCTM()},n.prototype.cacheViewBox=function(){var t=this.options.svg.getAttribute("viewBox");if(t){var e=t.split(/[\s\,]/).filter(function(t){return t}).map(parseFloat);this.viewBox.x=e[0],this.viewBox.y=e[1],this.viewBox.width=e[2],this.viewBox.height=e[3];var o=Math.min(this.options.width/this.viewBox.width,this.options.height/this.viewBox.height);this.activeState.zoom=o,this.activeState.x=(this.options.width-this.viewBox.width*o)/2,this.activeState.y=(this.options.height-this.viewBox.height*o)/2,this.updateCTMOnNextFrame(),this.options.svg.removeAttribute("viewBox")}else this.simpleViewBoxCache()},n.prototype.simpleViewBoxCache=function(){var t=this.viewport.getBBox();this.viewBox.x=t.x,this.viewBox.y=t.y,this.viewBox.width=t.width,this.viewBox.height=t.height},n.prototype.getViewBox=function(){return r.extend({},this.viewBox)},n.prototype.processCTM=function(){var t,e=this.getCTM();(this.options.fit||this.options.contain)&&(t=this.options.fit?Math.min(this.options.width/this.viewBox.width,this.options.height/this.viewBox.height):Math.max(this.options.width/this.viewBox.width,this.options.height/this.viewBox.height),e.a=t,e.d=t,e.e=-this.viewBox.x*t,e.f=-this.viewBox.y*t);if(this.options.center){var o=.5*(this.options.width-(this.viewBox.width+2*this.viewBox.x)*e.a),n=.5*(this.options.height-(this.viewBox.height+2*this.viewBox.y)*e.a);e.e=o,e.f=n}return this.originalState.zoom=e.a,this.originalState.x=e.e,this.originalState.y=e.f,e},n.prototype.getOriginalState=function(){return r.extend({},this.originalState)},n.prototype.getState=function(){return r.extend({},this.activeState)},n.prototype.getZoom=function(){return this.activeState.zoom},n.prototype.getRelativeZoom=function(){return this.activeState.zoom/this.originalState.zoom},n.prototype.computeRelativeZoom=function(t){return t/this.originalState.zoom},n.prototype.getPan=function(){return{x:this.activeState.x,y:this.activeState.y}},n.prototype.getCTM=function(){var t=this.options.svg.createSVGMatrix();return t.a=this.activeState.zoom,t.b=0,t.c=0,t.d=this.activeState.zoom,t.e=this.activeState.x,t.f=this.activeState.y,t},n.prototype.setCTM=function(t){var e=this.isZoomDifferent(t),o=this.isPanDifferent(t);if(e||o){if(e&&(!1===this.options.beforeZoom(this.getRelativeZoom(),this.computeRelativeZoom(t.a))?(t.a=t.d=this.activeState.zoom,e=!1):(this.updateCache(t),this.options.onZoom(this.getRelativeZoom()))),o){var n=this.options.beforePan(this.getPan(),{x:t.e,y:t.f}),i=!1,s=!1;!1===n?(t.e=this.getPan().x,t.f=this.getPan().y,i=s=!0):r.isObject(n)&&(!1===n.x?(t.e=this.getPan().x,i=!0):r.isNumber(n.x)&&(t.e=n.x),!1===n.y?(t.f=this.getPan().y,s=!0):r.isNumber(n.y)&&(t.f=n.y)),i&&s||!this.isPanDifferent(t)?o=!1:(this.updateCache(t),this.options.onPan(this.getPan()))}(e||o)&&this.updateCTMOnNextFrame()}},n.prototype.isZoomDifferent=function(t){return this.activeState.zoom!==t.a},n.prototype.isPanDifferent=function(t){return this.activeState.x!==t.e||this.activeState.y!==t.f},n.prototype.updateCache=function(t){this.activeState.zoom=t.a,this.activeState.x=t.e,this.activeState.y=t.f},n.prototype.pendingUpdate=!1,n.prototype.updateCTMOnNextFrame=function(){this.pendingUpdate||(this.pendingUpdate=!0,this.requestAnimationFrame.call(window,this.updateCTMCached))},n.prototype.updateCTM=function(){var t=this.getCTM();i.setCTM(this.viewport,t,this.defs),this.pendingUpdate=!1,this.options.onUpdatedCTM&&this.options.onUpdatedCTM(t)},e.exports=function(t,e){return new n(t,e)}},{"./svg-utilities":5,"./utilities":7}],3:[function(t,e,o){var n,i=t("./svg-pan-zoom.js");n=window,document,"function"==typeof define&&define.amd?define("svg-pan-zoom",function(){return i}):void 0!==e&&e.exports&&(e.exports=i,n.svgPanZoom=i)},{"./svg-pan-zoom.js":4}],4:[function(t,e,o){function i(t,e){this.init(t,e)}var n=t("./uniwheel"),s=t("./control-icons"),r=t("./utilities"),a=t("./svg-utilities"),l=t("./shadow-viewport"),u={viewportSelector:".svg-pan-zoom_viewport",panEnabled:!0,controlIconsEnabled:!1,zoomEnabled:!0,dblClickZoomEnabled:!0,mouseWheelZoomEnabled:!0,preventMouseEventsDefault:!0,zoomScaleSensitivity:.1,minZoom:.5,maxZoom:10,fit:!0,contain:!1,center:!0,refreshRate:"auto",beforeZoom:null,onZoom:null,beforePan:null,onPan:null,customEventsHandler:null,eventsListenerElement:null,onUpdatedCTM:null},h={passive:!0};i.prototype.init=function(t,e){var o=this;this.svg=t,this.defs=t.querySelector("defs"),a.setupSvgAttributes(this.svg),this.options=r.extend(r.extend({},u),e),this.state="none";var n=a.getBoundingClientRectNormalized(t);this.width=n.width,this.height=n.height,this.viewport=l(a.getOrCreateViewport(this.svg,this.options.viewportSelector),{svg:this.svg,width:this.width,height:this.height,fit:this.options.fit,contain:this.options.contain,center:this.options.center,refreshRate:this.options.refreshRate,beforeZoom:function(t,e){if(o.viewport&&o.options.beforeZoom)return o.options.beforeZoom(t,e)},onZoom:function(t){if(o.viewport&&o.options.onZoom)return o.options.onZoom(t)},beforePan:function(t,e){if(o.viewport&&o.options.beforePan)return o.options.beforePan(t,e)},onPan:function(t){if(o.viewport&&o.options.onPan)return o.options.onPan(t)},onUpdatedCTM:function(t){if(o.viewport&&o.options.onUpdatedCTM)return o.options.onUpdatedCTM(t)}});var i=this.getPublicInstance();i.setBeforeZoom(this.options.beforeZoom),i.setOnZoom(this.options.onZoom),i.setBeforePan(this.options.beforePan),i.setOnPan(this.options.onPan),i.setOnUpdatedCTM(this.options.onUpdatedCTM),this.options.controlIconsEnabled&&s.enable(this),this.lastMouseWheelEventTime=Date.now(),this.setupHandlers()},i.prototype.setupHandlers=function(){var o=this,n=null;if(this.eventListeners={mousedown:function(t){var e=o.handleMouseDown(t,n);return n=t,e},touchstart:function(t){var e=o.handleMouseDown(t,n);return n=t,e},mouseup:function(t){return o.handleMouseUp(t)},touchend:function(t){return o.handleMouseUp(t)},mousemove:function(t){return o.handleMouseMove(t)},touchmove:function(t){return o.handleMouseMove(t)},mouseleave:function(t){return o.handleMouseUp(t)},touchleave:function(t){return o.handleMouseUp(t)},touchcancel:function(t){return o.handleMouseUp(t)}},null!=this.options.customEventsHandler){this.options.customEventsHandler.init({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()});var t=this.options.customEventsHandler.haltEventListeners;if(t&&t.length)for(var e=t.length-1;0<=e;e--)this.eventListeners.hasOwnProperty(t[e])&&delete this.eventListeners[t[e]]}for(var i in this.eventListeners)(this.options.eventsListenerElement||this.svg).addEventListener(i,this.eventListeners[i],!this.options.preventMouseEventsDefault&&h);this.options.mouseWheelZoomEnabled&&(this.options.mouseWheelZoomEnabled=!1,this.enableMouseWheelZoom())},i.prototype.enableMouseWheelZoom=function(){if(!this.options.mouseWheelZoomEnabled){var e=this;this.wheelListener=function(t){return e.handleMouseWheel(t)};var t=!this.options.preventMouseEventsDefault;n.on(this.options.eventsListenerElement||this.svg,this.wheelListener,t),this.options.mouseWheelZoomEnabled=!0}},i.prototype.disableMouseWheelZoom=function(){if(this.options.mouseWheelZoomEnabled){var t=!this.options.preventMouseEventsDefault;n.off(this.options.eventsListenerElement||this.svg,this.wheelListener,t),this.options.mouseWheelZoomEnabled=!1}},i.prototype.handleMouseWheel=function(t){if(this.options.zoomEnabled&&"none"===this.state){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1);var e=t.deltaY||1,o=Date.now()-this.lastMouseWheelEventTime,n=3+Math.max(0,30-o);this.lastMouseWheelEventTime=Date.now(),"deltaMode"in t&&0===t.deltaMode&&t.wheelDelta&&(e=0===t.deltaY?0:Math.abs(t.wheelDelta)/t.deltaY),e=-.3<e&&e<.3?e:(0<e?1:-1)*Math.log(Math.abs(e)+10)/n;var i=this.svg.getScreenCTM().inverse(),s=a.getEventPoint(t,this.svg).matrixTransform(i),r=Math.pow(1+this.options.zoomScaleSensitivity,-1*e);this.zoomAtPoint(r,s)}},i.prototype.zoomAtPoint=function(t,e,o){var n=this.viewport.getOriginalState();o?(t=Math.max(this.options.minZoom*n.zoom,Math.min(this.options.maxZoom*n.zoom,t)),t/=this.getZoom()):this.getZoom()*t<this.options.minZoom*n.zoom?t=this.options.minZoom*n.zoom/this.getZoom():this.getZoom()*t>this.options.maxZoom*n.zoom&&(t=this.options.maxZoom*n.zoom/this.getZoom());var i=this.viewport.getCTM(),s=e.matrixTransform(i.inverse()),r=this.svg.createSVGMatrix().translate(s.x,s.y).scale(t).translate(-s.x,-s.y),a=i.multiply(r);a.a!==i.a&&this.viewport.setCTM(a)},i.prototype.zoom=function(t,e){this.zoomAtPoint(t,a.getSvgCenterPoint(this.svg,this.width,this.height),e)},i.prototype.publicZoom=function(t,e){e&&(t=this.computeFromRelativeZoom(t)),this.zoom(t,e)},i.prototype.publicZoomAtPoint=function(t,e,o){if(o&&(t=this.computeFromRelativeZoom(t)),"SVGPoint"!==r.getType(e)){if(!("x"in e&&"y"in e))throw new Error("Given point is invalid");e=a.createSVGPoint(this.svg,e.x,e.y)}this.zoomAtPoint(t,e,o)},i.prototype.getZoom=function(){return this.viewport.getZoom()},i.prototype.getRelativeZoom=function(){return this.viewport.getRelativeZoom()},i.prototype.computeFromRelativeZoom=function(t){return t*this.viewport.getOriginalState().zoom},i.prototype.resetZoom=function(){var t=this.viewport.getOriginalState();this.zoom(t.zoom,!0)},i.prototype.resetPan=function(){this.pan(this.viewport.getOriginalState())},i.prototype.reset=function(){this.resetZoom(),this.resetPan()},i.prototype.handleDblClick=function(t){var e;if((this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),this.options.controlIconsEnabled)&&-1<(t.target.getAttribute("class")||"").indexOf("svg-pan-zoom-control"))return!1;e=t.shiftKey?1/(2*(1+this.options.zoomScaleSensitivity)):2*(1+this.options.zoomScaleSensitivity);var o=a.getEventPoint(t,this.svg).matrixTransform(this.svg.getScreenCTM().inverse());this.zoomAtPoint(e,o)},i.prototype.handleMouseDown=function(t,e){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),r.mouseAndTouchNormalize(t,this.svg),this.options.dblClickZoomEnabled&&r.isDblClick(t,e)?this.handleDblClick(t):(this.state="pan",this.firstEventCTM=this.viewport.getCTM(),this.stateOrigin=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()))},i.prototype.handleMouseMove=function(t){if(this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&this.options.panEnabled){var e=a.getEventPoint(t,this.svg).matrixTransform(this.firstEventCTM.inverse()),o=this.firstEventCTM.translate(e.x-this.stateOrigin.x,e.y-this.stateOrigin.y);this.viewport.setCTM(o)}},i.prototype.handleMouseUp=function(t){this.options.preventMouseEventsDefault&&(t.preventDefault?t.preventDefault():t.returnValue=!1),"pan"===this.state&&(this.state="none")},i.prototype.fit=function(){var t=this.viewport.getViewBox(),e=Math.min(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.contain=function(){var t=this.viewport.getViewBox(),e=Math.max(this.width/t.width,this.height/t.height);this.zoom(e,!0)},i.prototype.center=function(){var t=this.viewport.getViewBox(),e=.5*(this.width-(t.width+2*t.x)*this.getZoom()),o=.5*(this.height-(t.height+2*t.y)*this.getZoom());this.getPublicInstance().pan({x:e,y:o})},i.prototype.updateBBox=function(){this.viewport.simpleViewBoxCache()},i.prototype.pan=function(t){var e=this.viewport.getCTM();e.e=t.x,e.f=t.y,this.viewport.setCTM(e)},i.prototype.panBy=function(t){var e=this.viewport.getCTM();e.e+=t.x,e.f+=t.y,this.viewport.setCTM(e)},i.prototype.getPan=function(){var t=this.viewport.getState();return{x:t.x,y:t.y}},i.prototype.resize=function(){var t=a.getBoundingClientRectNormalized(this.svg);this.width=t.width,this.height=t.height;var e=this.viewport;e.options.width=this.width,e.options.height=this.height,e.processCTM(),this.options.controlIconsEnabled&&(this.getPublicInstance().disableControlIcons(),this.getPublicInstance().enableControlIcons())},i.prototype.destroy=function(){var e=this;for(var t in this.beforeZoom=null,this.onZoom=null,this.beforePan=null,this.onPan=null,(this.onUpdatedCTM=null)!=this.options.customEventsHandler&&this.options.customEventsHandler.destroy({svgElement:this.svg,eventsListenerElement:this.options.eventsListenerElement,instance:this.getPublicInstance()}),this.eventListeners)(this.options.eventsListenerElement||this.svg).removeEventListener(t,this.eventListeners[t],!this.options.preventMouseEventsDefault&&h);this.disableMouseWheelZoom(),this.getPublicInstance().disableControlIcons(),this.reset(),c=c.filter(function(t){return t.svg!==e.svg}),delete this.options,delete this.viewport,delete this.publicInstance,delete this.pi,this.getPublicInstance=function(){return null}},i.prototype.getPublicInstance=function(){var o=this;return this.publicInstance||(this.publicInstance=this.pi={enablePan:function(){return o.options.panEnabled=!0,o.pi},disablePan:function(){return o.options.panEnabled=!1,o.pi},isPanEnabled:function(){return!!o.options.panEnabled},pan:function(t){return o.pan(t),o.pi},panBy:function(t){return o.panBy(t),o.pi},getPan:function(){return o.getPan()},setBeforePan:function(t){return o.options.beforePan=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnPan:function(t){return o.options.onPan=null===t?null:r.proxy(t,o.publicInstance),o.pi},enableZoom:function(){return o.options.zoomEnabled=!0,o.pi},disableZoom:function(){return o.options.zoomEnabled=!1,o.pi},isZoomEnabled:function(){return!!o.options.zoomEnabled},enableControlIcons:function(){return o.options.controlIconsEnabled||(o.options.controlIconsEnabled=!0,s.enable(o)),o.pi},disableControlIcons:function(){return o.options.controlIconsEnabled&&(o.options.controlIconsEnabled=!1,s.disable(o)),o.pi},isControlIconsEnabled:function(){return!!o.options.controlIconsEnabled},enableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!0,o.pi},disableDblClickZoom:function(){return o.options.dblClickZoomEnabled=!1,o.pi},isDblClickZoomEnabled:function(){return!!o.options.dblClickZoomEnabled},enableMouseWheelZoom:function(){return o.enableMouseWheelZoom(),o.pi},disableMouseWheelZoom:function(){return o.disableMouseWheelZoom(),o.pi},isMouseWheelZoomEnabled:function(){return!!o.options.mouseWheelZoomEnabled},setZoomScaleSensitivity:function(t){return o.options.zoomScaleSensitivity=t,o.pi},setMinZoom:function(t){return o.options.minZoom=t,o.pi},setMaxZoom:function(t){return o.options.maxZoom=t,o.pi},setBeforeZoom:function(t){return o.options.beforeZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},setOnZoom:function(t){return o.options.onZoom=null===t?null:r.proxy(t,o.publicInstance),o.pi},zoom:function(t){return o.publicZoom(t,!0),o.pi},zoomBy:function(t){return o.publicZoom(t,!1),o.pi},zoomAtPoint:function(t,e){return o.publicZoomAtPoint(t,e,!0),o.pi},zoomAtPointBy:function(t,e){return o.publicZoomAtPoint(t,e,!1),o.pi},zoomIn:function(){return this.zoomBy(1+o.options.zoomScaleSensitivity),o.pi},zoomOut:function(){return this.zoomBy(1/(1+o.options.zoomScaleSensitivity)),o.pi},getZoom:function(){return o.getRelativeZoom()},setOnUpdatedCTM:function(t){return o.options.onUpdatedCTM=null===t?null:r.proxy(t,o.publicInstance),o.pi},resetZoom:function(){return o.resetZoom(),o.pi},resetPan:function(){return o.resetPan(),o.pi},reset:function(){return o.reset(),o.pi},fit:function(){return o.fit(),o.pi},contain:function(){return o.contain(),o.pi},center:function(){return o.center(),o.pi},updateBBox:function(){return o.updateBBox(),o.pi},resize:function(){return o.resize(),o.pi},getSizes:function(){return{width:o.width,height:o.height,realZoom:o.getZoom(),viewBox:o.viewport.getViewBox()}},destroy:function(){return o.destroy(),o.pi}}),this.publicInstance};var c=[];e.exports=function(t,e){var o=r.getSvg(t);if(null===o)return null;for(var n=c.length-1;0<=n;n--)if(c[n].svg===o)return c[n].instance.getPublicInstance();return c.push({svg:o,instance:new i(o,e)}),c[c.length-1].instance.getPublicInstance()}},{"./control-icons":1,"./shadow-viewport":2,"./svg-utilities":5,"./uniwheel":6,"./utilities":7}],5:[function(t,e,o){var l=t("./utilities"),s="unknown";document.documentMode&&(s="ie"),e.exports={svgNS:"http://www.w3.org/2000/svg",xmlNS:"http://www.w3.org/XML/1998/namespace",xmlnsNS:"http://www.w3.org/2000/xmlns/",xlinkNS:"http://www.w3.org/1999/xlink",evNS:"http://www.w3.org/2001/xml-events",getBoundingClientRectNormalized:function(t){if(t.clientWidth&&t.clientHeight)return{width:t.clientWidth,height:t.clientHeight};if(t.getBoundingClientRect())return t.getBoundingClientRect();throw new Error("Cannot get BoundingClientRect for SVG.")},getOrCreateViewport:function(t,e){var o=null;if(!(o=l.isElement(e)?e:t.querySelector(e))){var n=Array.prototype.slice.call(t.childNodes||t.children).filter(function(t){return"defs"!==t.nodeName&&"#text"!==t.nodeName});1===n.length&&"g"===n[0].nodeName&&null===n[0].getAttribute("transform")&&(o=n[0])}if(!o){var i="viewport-"+(new Date).toISOString().replace(/\D/g,"");(o=document.createElementNS(this.svgNS,"g")).setAttribute("id",i);var s=t.childNodes||t.children;if(s&&0<s.length)for(var r=s.length;0<r;r--)"defs"!==s[s.length-r].nodeName&&o.appendChild(s[s.length-r]);t.appendChild(o)}var a=[];return o.getAttribute("class")&&(a=o.getAttribute("class").split(" ")),~a.indexOf("svg-pan-zoom_viewport")||(a.push("svg-pan-zoom_viewport"),o.setAttribute("class",a.join(" "))),o},setupSvgAttributes:function(t){if(t.setAttribute("xmlns",this.svgNS),t.setAttributeNS(this.xmlnsNS,"xmlns:xlink",this.xlinkNS),t.setAttributeNS(this.xmlnsNS,"xmlns:ev",this.evNS),null!==t.parentNode){var e=t.getAttribute("style")||"";-1===e.toLowerCase().indexOf("overflow")&&t.setAttribute("style","overflow: hidden; "+e)}},internetExplorerRedisplayInterval:300,refreshDefsGlobal:l.throttle(function(){for(var t=document.querySelectorAll("defs"),e=t.length,o=0;o<e;o++){var n=t[o];n.parentNode.insertBefore(n,n)}},this?this.internetExplorerRedisplayInterval:null),setCTM:function(t,e,o){var n=this,i="matrix("+e.a+","+e.b+","+e.c+","+e.d+","+e.e+","+e.f+")";t.setAttributeNS(null,"transform",i),"transform"in t.style?t.style.transform=i:"-ms-transform"in t.style?t.style["-ms-transform"]=i:"-webkit-transform"in t.style&&(t.style["-webkit-transform"]=i),"ie"===s&&o&&(o.parentNode.insertBefore(o,o),window.setTimeout(function(){n.refreshDefsGlobal()},n.internetExplorerRedisplayInterval))},getEventPoint:function(t,e){var o=e.createSVGPoint();return l.mouseAndTouchNormalize(t,e),o.x=t.clientX,o.y=t.clientY,o},getSvgCenterPoint:function(t,e,o){return this.createSVGPoint(t,e/2,o/2)},createSVGPoint:function(t,e,o){var n=t.createSVGPoint();return n.x=e,n.y=o,n}}},{"./utilities":7}],6:[function(t,e,o){function s(t,o){function e(t){var e={originalEvent:t=t||window.event,target:t.target||t.srcElement,type:"wheel",deltaMode:"MozMousePixelScroll"==t.type?0:1,deltaX:0,delatZ:0,preventDefault:function(){t.preventDefault?t.preventDefault():t.returnValue=!1}};return"mousewheel"==l?(e.deltaY=-.025*t.wheelDelta,t.wheelDeltaX&&(e.deltaX=-.025*t.wheelDeltaX)):e.deltaY=t.detail,o(e)}return h.push({element:t,fn:e}),e}function n(t,e,o,n){var i;i="wheel"===l?o:s(t,o),t[r](u+e,i,n?c:p)}function i(t,e,o,n){var i;i="wheel"===l?o:function(t){for(var e=0;e<h.length;e++)if(h[e].element===t)return h[e].fn;return function(){}}(t),t[a](u+e,i,n?c:p),function(t){for(var e=0;e<h.length;e++)if(h[e].element===t)return h.splice(e,1)}(t)}var r,a,l,u,h,c,p;e.exports=(h=[],p={passive:!(c={passive:!(u="")})},window.addEventListener?(r="addEventListener",a="removeEventListener"):(r="attachEvent",a="detachEvent",u="on"),l="onwheel"in document.createElement("div")?"wheel":void 0!==document.onmousewheel?"mousewheel":"DOMMouseScroll",{on:function(t,e,o){n(t,l,e,o),"DOMMouseScroll"==l&&n(t,"MozMousePixelScroll",e,o)},off:function(t,e,o){i(t,l,e,o),"DOMMouseScroll"==l&&i(t,"MozMousePixelScroll",e,o)}})},{}],7:[function(t,e,o){function n(e){return function(t){window.setTimeout(t,e)}}e.exports={extend:function(t,e){for(var o in t=t||{},e)this.isObject(e[o])?t[o]=this.extend(t[o],e[o]):t[o]=e[o];return t},isElement:function(t){return t instanceof HTMLElement||t instanceof SVGElement||t instanceof SVGSVGElement||t&&"object"==typeof t&&null!==t&&1===t.nodeType&&"string"==typeof t.nodeName},isObject:function(t){return"[object Object]"===Object.prototype.toString.call(t)},isNumber:function(t){return!isNaN(parseFloat(t))&&isFinite(t)},getSvg:function(t){var e,o;if(this.isElement(t))e=t;else{if(!("string"==typeof t||t instanceof String))throw new Error("Provided selector is not an HTML object nor String");if(!(e=document.querySelector(t)))throw new Error("Provided selector did not find any elements. Selector: "+t)}if("svg"===e.tagName.toLowerCase())o=e;else if("object"===e.tagName.toLowerCase())o=e.contentDocument.documentElement;else{if("embed"!==e.tagName.toLowerCase())throw"img"===e.tagName.toLowerCase()?new Error('Cannot script an SVG in an "img" element. Please use an "object" element or an in-line SVG.'):new Error("Cannot get SVG.");o=e.getSVGDocument().documentElement}return o},proxy:function(t,e){return function(){return t.apply(e,arguments)}},getType:function(t){return Object.prototype.toString.apply(t).replace(/^\[object\s/,"").replace(/\]$/,"")},mouseAndTouchNormalize:function(t,e){if(void 0===t.clientX||null===t.clientX)if(t.clientX=0,void(t.clientY=0)!==t.touches&&t.touches.length){if(void 0!==t.touches[0].clientX)t.clientX=t.touches[0].clientX,t.clientY=t.touches[0].clientY;else if(void 0!==t.touches[0].pageX){var o=e.getBoundingClientRect();t.clientX=t.touches[0].pageX-o.left,t.clientY=t.touches[0].pageY-o.top}}else void 0!==t.originalEvent&&void 0!==t.originalEvent.clientX&&(t.clientX=t.originalEvent.clientX,t.clientY=t.originalEvent.clientY)},isDblClick:function(t,e){if(2===t.detail)return!0;if(null==e)return!1;var o=t.timeStamp-e.timeStamp,n=Math.sqrt(Math.pow(t.clientX-e.clientX,2)+Math.pow(t.clientY-e.clientY,2));return o<250&&n<10},now:Date.now||function(){return(new Date).getTime()},throttle:function(o,n,i){var s,r,a,l=this,u=null,h=0;i=i||{};function c(){h=!1===i.leading?0:l.now(),u=null,a=o.apply(s,r),u||(s=r=null)}return function(){var t=l.now();h||!1!==i.leading||(h=t);var e=n-(t-h);return s=this,r=arguments,e<=0||n<e?(clearTimeout(u),u=null,h=t,a=o.apply(s,r),u||(s=r=null)):u||!1===i.trailing||(u=setTimeout(c,e)),a}},createRequestAnimationFrame:function(t){var e=null;return"auto"!==t&&t<60&&1<t&&(e=Math.floor(1e3/t)),null===e?window.requestAnimationFrame||n(33):n(e)}}},{}]},{},[3]);
@@ -145,8 +145,6 @@ def _watch_html(
145
145
  filter_completed=False,
146
146
  comparison=None,
147
147
  ):
148
- # Keep the SVG as the page's main content so browser zoom behavior matches
149
- # the original watcher experience (the graph scales, not just footer text).
150
148
  # {{{ Build links for the other preview views
151
149
  links = []
152
150
  diff_query = (
@@ -184,13 +182,37 @@ def _watch_html(
184
182
  + comparison.commit[:12]
185
183
  + ")"
186
184
  )
185
+ # The graph viewport fills the window above the footer links. Pan/zoom
186
+ # (wheel, drag, toolbar, box zoom) is handled inside the embedded SVG by
187
+ # wgrph_view.js, so it never changes the browser zoom level.
187
188
  return (
188
- "<html><body style='margin:0'>"
189
- "<embed id='svg-view' style='display:block;' type='image/svg+xml'"
189
+ "<html><head><style>"
190
+ "html,body{height:100%;margin:0;overflow:hidden;}"
191
+ "body{display:flex;flex-direction:column;}"
192
+ "#graph-area{position:relative;flex:1 1 auto;min-height:0;}"
193
+ "#svg-view{display:block;width:100%;height:100%;}"
194
+ "#wgrph-toolbar{position:absolute;top:6px;right:6px;display:flex;"
195
+ "gap:2px;}"
196
+ "#wgrph-toolbar button{font-size:15px;min-width:2em;cursor:pointer;"
197
+ "background:#f4f4f4;border:1px solid #aaa;border-radius:3px;}"
198
+ "#wgrph-toolbar button.active{background:#cde;border-color:#468;}"
199
+ "</style></head><body>"
200
+ "<div id='graph-area'>"
201
+ "<embed id='svg-view' type='image/svg+xml'"
190
202
  f" src='{svg_url}'/>"
203
+ "<div id='wgrph-toolbar'>"
204
+ "<button id='wgrph-home' title='Home: fit whole graph'>&#8962;"
205
+ "</button>"
206
+ "<button id='wgrph-zoom-in' title='Zoom in'>+</button>"
207
+ "<button id='wgrph-zoom-out' title='Zoom out'>&#8722;</button>"
208
+ "<button id='wgrph-box-zoom' title='Zoom to rectangle (Esc to exit)'>"
209
+ "&#9633;</button>"
210
+ "</div></div>"
191
211
  "<p style='margin:0.4em 0.8em;font-family:sans-serif;font-size:13px;'>"
192
212
  f"{footer_html}"
193
213
  "</p>"
214
+ "<script src='/svg-pan-zoom.min.js'></script>"
215
+ "<script src='/wgrph_view.js'></script>"
194
216
  "</body></html>"
195
217
  )
196
218
 
@@ -305,10 +327,12 @@ def build_graph(
305
327
  for index, child in enumerate(list(group)):
306
328
  if child.tag != f"{namespace}text" or child.text is None:
307
329
  continue
308
- if not child.text.startswith(link_marker):
330
+ marker_index = child.text.find(link_marker)
331
+ if marker_index < 0:
309
332
  continue
310
- task_name = child.text[len(link_marker) :]
311
- child.text = task_name
333
+ prefix = child.text[:marker_index]
334
+ task_name = child.text[marker_index + len(link_marker) :]
335
+ child.text = prefix + task_name
312
336
  link = ET.Element(f"{namespace}a")
313
337
  link.set(
314
338
  f"{{{xlink_ns}}}href", f"/?t={urllib.parse.quote(task_name)}"
@@ -705,6 +729,13 @@ class FlowchartPreviewServer:
705
729
  "image/svg+xml; charset=utf-8",
706
730
  )
707
731
  return
732
+ if parsed.path in ("/svg-pan-zoom.min.js", "/wgrph_view.js"):
733
+ _send_preview_response(
734
+ self,
735
+ (Path(__file__).parent / parsed.path[1:]).read_bytes(),
736
+ "text/javascript; charset=utf-8",
737
+ )
738
+ return
708
739
 
709
740
  if parsed.path != "/" and parsed.path != "/index.html":
710
741
  self.send_error(404)
@@ -0,0 +1,212 @@
1
+ // Matplotlib-style pan/zoom for the wgrph preview page.
2
+ //
3
+ // svg-pan-zoom (vendored next to this file) handles wheel zoom and drag pan
4
+ // inside the embedded SVG. This script adds the "home" view (whole graph fit
5
+ // and pinned to the top), a zoom-to-rectangle mode, a guard so that a drag
6
+ // ending on a task link does not navigate, and restoration of the current
7
+ // view when the watcher swaps in a rebuilt SVG.
8
+ (function () {
9
+ const embed = document.getElementById('svg-view');
10
+ const boxButton = document.getElementById('wgrph-box-zoom');
11
+ // Last view seen on this page; null until the first SVG is shown, so only
12
+ // live reloads (same page, new src) restore it.
13
+ let savedView = null;
14
+ let boxMode = false;
15
+ let panZoom = null;
16
+ let initializedDoc = null;
17
+
18
+ function saveView() {
19
+ savedView = {
20
+ realZoom: panZoom.getSizes().realZoom,
21
+ pan: panZoom.getPan(),
22
+ };
23
+ }
24
+
25
+ function home() {
26
+ // Fit the whole graph, pinned to the top and centered horizontally.
27
+ panZoom.resize();
28
+ panZoom.reset();
29
+ const sizes = panZoom.getSizes();
30
+ panZoom.panBy({
31
+ x: (sizes.width - sizes.viewBox.width * sizes.realZoom) / 2,
32
+ y: 0,
33
+ });
34
+ }
35
+
36
+ function setBoxMode(enabled) {
37
+ boxMode = enabled;
38
+ boxButton.classList.toggle('active', enabled);
39
+ if (panZoom === null) {
40
+ return;
41
+ }
42
+ if (enabled) {
43
+ panZoom.disablePan();
44
+ } else {
45
+ panZoom.enablePan();
46
+ }
47
+ }
48
+
49
+ function init() {
50
+ const svgDoc = embed.getSVGDocument();
51
+ if (
52
+ svgDoc === null ||
53
+ svgDoc.readyState !== 'complete' ||
54
+ svgDoc.documentElement === null ||
55
+ svgDoc === initializedDoc
56
+ ) {
57
+ return;
58
+ }
59
+ initializedDoc = svgDoc;
60
+ const svg = svgDoc.documentElement;
61
+ // Let the SVG fill the embed box; svg-pan-zoom then moves the viewBox
62
+ // scaling onto its viewport group.
63
+ svg.setAttribute('width', '100%');
64
+ svg.setAttribute('height', '100%');
65
+ panZoom = svgPanZoom(embed, {
66
+ fit: true,
67
+ center: false,
68
+ dblClickZoomEnabled: false,
69
+ zoomScaleSensitivity: 0.2,
70
+ minZoom: 0.1,
71
+ maxZoom: 50,
72
+ onZoom: saveView,
73
+ onPan: saveView,
74
+ });
75
+ window.wgrphPanZoom = panZoom;
76
+ if (savedView === null) {
77
+ home();
78
+ } else {
79
+ const restore = savedView;
80
+ panZoom.zoom(
81
+ (restore.realZoom / panZoom.getSizes().realZoom) * panZoom.getZoom()
82
+ );
83
+ panZoom.pan(restore.pan);
84
+ }
85
+ saveView();
86
+ setBoxMode(boxMode);
87
+
88
+ // {{{ Suppress link clicks that end a pan or a box selection
89
+ let downPoint = null;
90
+ let dragged = false;
91
+ svgDoc.addEventListener(
92
+ 'mousedown',
93
+ function (evt) {
94
+ downPoint = { x: evt.clientX, y: evt.clientY };
95
+ dragged = false;
96
+ },
97
+ true
98
+ );
99
+ svgDoc.addEventListener(
100
+ 'mousemove',
101
+ function (evt) {
102
+ if (
103
+ downPoint !== null &&
104
+ Math.hypot(evt.clientX - downPoint.x, evt.clientY - downPoint.y) > 4
105
+ ) {
106
+ dragged = true;
107
+ }
108
+ },
109
+ true
110
+ );
111
+ svgDoc.addEventListener(
112
+ 'click',
113
+ function (evt) {
114
+ if (dragged || boxMode) {
115
+ evt.preventDefault();
116
+ evt.stopPropagation();
117
+ }
118
+ downPoint = null;
119
+ dragged = false;
120
+ },
121
+ true
122
+ );
123
+ // }}}
124
+
125
+ // {{{ Zoom-to-rectangle mode
126
+ let boxStart = null;
127
+ let boxRect = null;
128
+ svg.addEventListener('mousedown', function (evt) {
129
+ if (!boxMode || evt.button !== 0) {
130
+ return;
131
+ }
132
+ boxStart = { x: evt.clientX, y: evt.clientY };
133
+ // The rectangle lives on the root <svg>, outside the pan/zoom
134
+ // viewport group, so its coordinates are plain screen pixels.
135
+ boxRect = svgDoc.createElementNS('http://www.w3.org/2000/svg', 'rect');
136
+ boxRect.setAttribute('fill', 'rgba(70,130,180,0.15)');
137
+ boxRect.setAttribute('stroke', 'steelblue');
138
+ boxRect.setAttribute('stroke-dasharray', '4 3');
139
+ boxRect.setAttribute('pointer-events', 'none');
140
+ svg.appendChild(boxRect);
141
+ });
142
+ svg.addEventListener('mousemove', function (evt) {
143
+ if (boxStart === null) {
144
+ return;
145
+ }
146
+ boxRect.setAttribute('x', Math.min(boxStart.x, evt.clientX));
147
+ boxRect.setAttribute('y', Math.min(boxStart.y, evt.clientY));
148
+ boxRect.setAttribute('width', Math.abs(evt.clientX - boxStart.x));
149
+ boxRect.setAttribute('height', Math.abs(evt.clientY - boxStart.y));
150
+ });
151
+ svgDoc.addEventListener('mouseup', function (evt) {
152
+ if (boxStart === null) {
153
+ return;
154
+ }
155
+ const width = Math.abs(evt.clientX - boxStart.x);
156
+ const height = Math.abs(evt.clientY - boxStart.y);
157
+ const center = {
158
+ x: (evt.clientX + boxStart.x) / 2,
159
+ y: (evt.clientY + boxStart.y) / 2,
160
+ };
161
+ boxRect.remove();
162
+ boxRect = null;
163
+ boxStart = null;
164
+ if (width <= 5 || height <= 5) {
165
+ return;
166
+ }
167
+ const sizes = panZoom.getSizes();
168
+ panZoom.zoomAtPointBy(
169
+ Math.min(sizes.width / width, sizes.height / height),
170
+ center
171
+ );
172
+ panZoom.panBy({
173
+ x: sizes.width / 2 - center.x,
174
+ y: sizes.height / 2 - center.y,
175
+ });
176
+ });
177
+ // }}}
178
+ svgDoc.addEventListener('keydown', function (evt) {
179
+ if (evt.key === 'Escape') {
180
+ setBoxMode(false);
181
+ }
182
+ });
183
+ }
184
+
185
+ embed.addEventListener('load', init);
186
+ // The SVG may already be loaded by the time this script runs.
187
+ init();
188
+ window.addEventListener('resize', function () {
189
+ if (panZoom !== null) {
190
+ panZoom.resize();
191
+ }
192
+ });
193
+ document.addEventListener('keydown', function (evt) {
194
+ if (evt.key === 'Escape') {
195
+ setBoxMode(false);
196
+ }
197
+ });
198
+ document.getElementById('wgrph-home').addEventListener('click', home);
199
+ document
200
+ .getElementById('wgrph-zoom-in')
201
+ .addEventListener('click', function () {
202
+ panZoom.zoomBy(1.25);
203
+ });
204
+ document
205
+ .getElementById('wgrph-zoom-out')
206
+ .addEventListener('click', function () {
207
+ panZoom.zoomBy(0.8);
208
+ });
209
+ boxButton.addEventListener('click', function () {
210
+ setBoxMode(!boxMode);
211
+ });
212
+ })();
@@ -171,8 +171,8 @@ def xore(filename):
171
171
  all_contents = pickle.load(fp)
172
172
  all_contents.set_format(format_type)
173
173
  with open(markdownfile, "r", encoding="utf-8") as fp:
174
- for thisline in fp:
175
- all_contents.outline_in_order(thisline.rstrip())
174
+ outline_lines = [j.rstrip() for j in fp]
175
+ all_contents.outline_in_order(outline_lines)
176
176
  # build the text before opening the target, so that an error (e.g. a
177
177
  # section missing from the outline) doesn't leave an empty file
178
178
  reordered = str(all_contents)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "pyDiffTools"
7
- version = "0.1.31"
7
+ version = "0.1.33"
8
8
  authors = [{ name = "J M Franck" }]
9
9
  description = "Diff tools"
10
10
  readme = "README.rst"
@@ -49,6 +49,10 @@ pydifftools = [
49
49
  "comment_tags_no_comments.lua",
50
50
  "comment_toggle.js",
51
51
  ]
52
+ "pydifftools.flowchart" = [
53
+ "svg-pan-zoom.min.js",
54
+ "wgrph_view.js",
55
+ ]
52
56
 
53
57
  [tool.black]
54
58
  line-length = 79
@@ -1,162 +0,0 @@
1
- from collections import OrderedDict
2
- from fuzzywuzzy import process
3
-
4
-
5
- class doc_contents_class(object):
6
- def __init__(self, format_type="latex"):
7
- self.contents = OrderedDict()
8
- self.contents["header"] = ""
9
- self.types = {}
10
- self.types["header"] = "header"
11
- self._reordering_started = False
12
- self._aliases = {}
13
- self._processed_titles = []
14
- self.set_format(format_type)
15
-
16
- def set_format(self, format_type):
17
- if format_type == "markdown":
18
- # markdown levels go deeper, so include subparagraph mapping
19
- self.level_numbers = {
20
- "section": 1,
21
- "subsection": 2,
22
- "subsubsection": 3,
23
- "paragraph": 4,
24
- "subparagraph": 5,
25
- }
26
- else:
27
- # default to latex behavior
28
- self.level_numbers = {
29
- "section": 1,
30
- "subsection": 2,
31
- "subsubsection": 3,
32
- "paragraph": 4,
33
- "subparagraph": 5,
34
- }
35
- # map indentation back to section type for outline parsing
36
- self.inv_prefix = {
37
- (level - 1) * "\t": section
38
- for section, level in self.level_numbers.items()
39
- }
40
- self.format_type = format_type
41
-
42
- def start_sec(self, thistype, thistitle):
43
- assert thistitle not in self.contents.keys(), (
44
- "more than one section with the name:\n" + thistitle
45
- )
46
- self.contents[thistitle] = ""
47
- self.types[thistitle] = thistype
48
- print("added", thistitle)
49
-
50
- def __setstate__(self, d):
51
- "set the info from a pickle"
52
- self.contents = d["contents"]
53
- self.types = d["types"]
54
- self._aliases = {} # doesn't exist, but still needed
55
- self._reordering_started = False
56
- self._processed_titles = []
57
- if "format_type" in d:
58
- self.set_format(d["format_type"])
59
- else:
60
- self.set_format("latex")
61
- return
62
-
63
- def __getstate__(self):
64
- "return info for a pickle"
65
- return {
66
- "contents": self.contents,
67
- "types": self.types,
68
- "format_type": self.format_type,
69
- }
70
-
71
- def __iadd__(self, value):
72
- self.contents[next(reversed(self.contents))] += value
73
- return self
74
-
75
- def __str__(self):
76
- if len(self._processed_titles) > 0:
77
- raise ValueError(
78
- "the following section titles are missing from the"
79
- " outline:\n\t"
80
- + "\n\t".join(self._processed_titles)
81
- + "\nEvery existing section must appear in the outline, so"
82
- " that entire sections of text are never dropped by"
83
- " accident. If you really want to get rid of a section,"
84
- " add a new heading (e.g. 'for deletion') to the outline"
85
- " and move the unwanted sections underneath it; then delete"
86
- " that part of the file by hand."
87
- )
88
- retval = ""
89
- for j in self.contents.keys():
90
- if self.types[j] != "header":
91
- new_name = j
92
- if j in self._aliases.keys():
93
- new_name = self._aliases[j]
94
- if self.format_type == "markdown":
95
- retval += "#" * self.level_numbers[self.types[j]]
96
- retval += f" {new_name}\n"
97
- else:
98
- retval += f"\\{self.types[j]}{{{new_name}}}"
99
- retval += f"{self.contents[j]}"
100
- return retval
101
-
102
- @property
103
- def outline(self):
104
- retval = []
105
- for j in self.contents.keys():
106
- if self.types[j] != "header":
107
- indent = (self.level_numbers[self.types[j]] - 1) * "\t"
108
- thistitle = (indent + "\t").join(j.split("\n"))
109
- retval.append(indent + "*\t" + thistitle)
110
- self._reordering_started = False
111
- return "\n".join(retval)
112
-
113
- def outline_in_order(self, thisline):
114
- if not self._reordering_started:
115
- self._processed_titles = [
116
- j for j in self.contents.keys() if self.types[j] != "header"
117
- ]
118
- self._reordering_started = True
119
- ilevel = 0
120
- spacelevel = 0
121
- hitmarker = False
122
- for j, thischar in enumerate(thisline):
123
- if not hitmarker:
124
- if thischar == " ":
125
- spacelevel += 1
126
- if spacelevel == 4 or thischar == "\t":
127
- ilevel += 1
128
- spacelevel = 0
129
- elif thischar == "*":
130
- hitmarker = True
131
- else:
132
- assert thischar in [" ", "\t"]
133
- title = thisline[j + 1 :]
134
- break
135
- if not hitmarker:
136
- raise ValueError("somehow, there wasn't a * marker!")
137
- if title not in self.contents.keys():
138
- best_match, match_quality = process.extractOne(
139
- title, self.contents.keys()
140
- )
141
- yesorno = input(
142
- f"didn't find\n\t{title}\namong the existing sections."
143
- f" Is it a renamed version of\n\t{best_match}\n?"
144
- " say y to rename, or n to add it as a new, empty section: "
145
- )
146
- if yesorno == "y":
147
- self._aliases[best_match] = title # will be replaced later
148
- title = best_match
149
- elif yesorno == "n":
150
- # a new section, so it isn't on the list of titles to place
151
- self.contents[title] = ""
152
- self._processed_titles.append(title)
153
- else:
154
- raise ValueError("expected y or n")
155
- if title not in self._processed_titles:
156
- raise ValueError(
157
- f"the section\n\t{title}\nappears more than once in the"
158
- " outline"
159
- )
160
- self.contents.move_to_end(title)
161
- self._processed_titles.remove(title)
162
- self.types[title] = self.inv_prefix[ilevel * "\t"]
File without changes
File without changes
File without changes