htmlcmp 1.0.0__tar.gz → 1.0.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlcmp
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Compare HTML files by rendered output
5
5
  Home-page: https://github.com/opendocument-app/compare-html
6
6
  Author: Andreas Stefl
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "htmlcmp"
3
- version = "1.0.0"
3
+ version = "1.0.1"
4
4
  description = "Compare HTML files by rendered output"
5
5
  classifiers = []
6
6
  authors = [
@@ -7,7 +7,7 @@ long_description = (this_directory / "README.md").read_text()
7
7
 
8
8
  setup(
9
9
  name="htmlcmp",
10
- version="1.0.0",
10
+ version="1.0.1",
11
11
  author="Andreas Stefl",
12
12
  author_email="stefl.andreas@gmail.com",
13
13
  description="Compare HTML files by rendered output",
File without changes
@@ -0,0 +1,10 @@
1
+ class bcolors:
2
+ HEADER = "\033[95m"
3
+ OKBLUE = "\033[94m"
4
+ OKCYAN = "\033[96m"
5
+ OKGREEN = "\033[92m"
6
+ WARNING = "\033[93m"
7
+ FAIL = "\033[91m"
8
+ ENDC = "\033[0m"
9
+ BOLD = "\033[1m"
10
+ UNDERLINE = "\033[4m"
@@ -0,0 +1,251 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import sys
6
+ import argparse
7
+ import json
8
+ import threading
9
+ import filecmp
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from html_render_diff import get_browser, html_render_diff
12
+ from common import bcolors
13
+
14
+
15
+ class Config:
16
+ thread_local = threading.local()
17
+
18
+
19
+ def parse_json(path):
20
+ with open(path) as f:
21
+ return json.load(f)
22
+
23
+
24
+ def compare_json(a, b):
25
+ json_a = json.dumps(parse_json(a), sort_keys=True)
26
+ json_b = json.dumps(parse_json(b), sort_keys=True)
27
+ return json_a == json_b
28
+
29
+
30
+ def compare_html(a, b, browser=None, diff_output=None):
31
+ if browser is None:
32
+ browser = get_browser()
33
+ diff, (image_a, image_b) = html_render_diff(a, b, browser=browser)
34
+ result = True if diff.getbbox() is None else False
35
+ if diff_output is not None and not result:
36
+ os.makedirs(diff_output, exist_ok=True)
37
+ image_a.save(os.path.join(diff_output, "a.png"))
38
+ image_b.save(os.path.join(diff_output, "b.png"))
39
+ diff.save(os.path.join(diff_output, "diff.png"))
40
+ return result
41
+
42
+
43
+ def compare_files(a, b, **kwargs):
44
+ if filecmp.cmp(a, b):
45
+ return True
46
+ if a.endswith(".json"):
47
+ return compare_json(a, b)
48
+ if a.endswith(".html"):
49
+ return compare_html(a, b, **kwargs)
50
+
51
+
52
+ def comparable_file(path):
53
+ if path.endswith(".json"):
54
+ return True
55
+ if path.endswith(".html"):
56
+ return True
57
+ return False
58
+
59
+
60
+ def submit_compare_dirs(a, b, executor, diff_output=None, **kwargs):
61
+ results = {
62
+ "common_dirs": [],
63
+ "common_files": [],
64
+ "left_files_missing": [],
65
+ "right_files_missing": [],
66
+ "left_dirs_missing": [],
67
+ "right_dirs_missing": [],
68
+ }
69
+
70
+ left = sorted(os.listdir(a))
71
+ right = sorted(os.listdir(b))
72
+
73
+ left_files = sorted(
74
+ [
75
+ name
76
+ for name in left
77
+ if os.path.isfile(os.path.join(a, name))
78
+ and comparable_file(os.path.join(a, name))
79
+ ]
80
+ )
81
+ right_files = sorted(
82
+ [
83
+ name
84
+ for name in right
85
+ if os.path.isfile(os.path.join(b, name))
86
+ and comparable_file(os.path.join(b, name))
87
+ ]
88
+ )
89
+ common_files = [name for name in left_files if name in right_files]
90
+
91
+ def compare(path_a, path_b, diff_output):
92
+ browser = getattr(Config.thread_local, "browser", None)
93
+ return compare_files(path_a, path_b, browser=browser, diff_output=diff_output)
94
+
95
+ for name in common_files:
96
+ future = executor.submit(
97
+ compare,
98
+ os.path.join(a, name),
99
+ os.path.join(b, name),
100
+ diff_output=(
101
+ None if diff_output is None else os.path.join(diff_output, name)
102
+ ),
103
+ )
104
+ results["common_files"].append((name, future))
105
+
106
+ results["left_files_missing"] = [
107
+ name for name in right_files if name not in left_files
108
+ ]
109
+ results["right_files_missing"] = [
110
+ name for name in left_files if name not in right_files
111
+ ]
112
+
113
+ left_dirs = sorted([name for name in left if os.path.isdir(os.path.join(a, name))])
114
+ right_dirs = sorted(
115
+ [name for name in right if os.path.isdir(os.path.join(b, name))]
116
+ )
117
+ common_dirs = [path for path in left_dirs if path in right_dirs]
118
+
119
+ for name in common_dirs:
120
+ sub_results = submit_compare_dirs(
121
+ os.path.join(a, name),
122
+ os.path.join(b, name),
123
+ executor=executor,
124
+ diff_output=(
125
+ None if diff_output is None else os.path.join(diff_output, name)
126
+ ),
127
+ **kwargs,
128
+ )
129
+ results["common_dirs"].append((name, sub_results))
130
+
131
+ results["left_dirs_missing"] = [
132
+ name for name in right_dirs if name not in left_dirs
133
+ ]
134
+ results["right_dirs_missing"] = [
135
+ name for name in left_dirs if name not in right_dirs
136
+ ]
137
+
138
+ return results
139
+
140
+
141
+ def print_results(results, a, b, level=0, prefix=""):
142
+ prefix_file = prefix + "├── "
143
+ if level == 0:
144
+ print(f"compare dir {a} with {b}")
145
+
146
+ result = {
147
+ "left_files_missing": [],
148
+ "right_files_missing": [],
149
+ "left_dirs_missing": [],
150
+ "right_dirs_missing": [],
151
+ "files_different": [],
152
+ }
153
+
154
+ left_files_missing = " ".join(results["left_files_missing"])
155
+ if left_files_missing:
156
+ print(
157
+ f"{prefix_file}{bcolors.FAIL}missing files left: {left_files_missing} ✘{bcolors.ENDC}"
158
+ )
159
+ result["left_files_missing"].extend(
160
+ [os.path.join(a, name) for name in results["left_files_missing"]]
161
+ )
162
+ right_files_missing = " ".join(results["right_files_missing"])
163
+ if right_files_missing:
164
+ print(
165
+ f"{prefix_file}{bcolors.FAIL}missing files right: {right_files_missing} ✘{bcolors.ENDC}"
166
+ )
167
+ result["right_files_missing"].extend(
168
+ [os.path.join(a, name) for name in results["right_files_missing"]]
169
+ )
170
+
171
+ for name, future in results["common_files"]:
172
+ cmp = future.result()
173
+ if cmp:
174
+ print(f"{prefix_file}{bcolors.OKGREEN}{name} ✓{bcolors.ENDC}")
175
+ else:
176
+ print(f"{prefix_file}{bcolors.FAIL}{name} ✘{bcolors.ENDC}")
177
+ result["files_different"].append(os.path.join(a, name))
178
+
179
+ left_dirs_missing = " ".join(results["left_dirs_missing"])
180
+ if left_dirs_missing:
181
+ print(
182
+ f"{prefix_file}{bcolors.FAIL}missing dirs left: {left_dirs_missing} ✘{bcolors.ENDC}"
183
+ )
184
+ result["left_dirs_missing"].extend(
185
+ [os.path.join(a, name) for name in results["left_dirs_missing"]]
186
+ )
187
+ right_dirs_missing = " ".join(results["right_dirs_missing"])
188
+ if right_dirs_missing:
189
+ print(
190
+ f"{prefix_file}{bcolors.FAIL}missing dirs right: {right_dirs_missing} ✘{bcolors.ENDC}"
191
+ )
192
+ result["right_dirs_missing"].extend(
193
+ [os.path.join(a, name) for name in results["right_dirs_missing"]]
194
+ )
195
+
196
+ for name, sub_results in results["common_dirs"]:
197
+ print(prefix + "├── " + name)
198
+ sub_result = print_results(
199
+ sub_results,
200
+ os.path.join(a, name),
201
+ os.path.join(b, name),
202
+ level=level + 1,
203
+ prefix=prefix + "│ ",
204
+ )
205
+ result["left_files_missing"].extend(sub_result["left_files_missing"])
206
+ result["right_files_missing"].extend(sub_result["right_files_missing"])
207
+ result["left_dirs_missing"].extend(sub_result["left_dirs_missing"])
208
+ result["right_dirs_missing"].extend(sub_result["right_dirs_missing"])
209
+ result["files_different"].extend(sub_result["files_different"])
210
+
211
+ return result
212
+
213
+
214
+ def main():
215
+ parser = argparse.ArgumentParser()
216
+ parser.add_argument("a")
217
+ parser.add_argument("b")
218
+ parser.add_argument(
219
+ "--driver", choices=["chrome", "firefox", "phantomjs"], default="firefox"
220
+ )
221
+ parser.add_argument("--diff-output")
222
+ parser.add_argument("--max-workers", type=int, default=1)
223
+ args = parser.parse_args()
224
+
225
+ def initializer():
226
+ browser = getattr(Config.thread_local, "browser", None)
227
+ if browser is None:
228
+ browser = get_browser(driver=args.driver)
229
+ Config.thread_local.browser = browser
230
+
231
+ executor = ThreadPoolExecutor(max_workers=args.max_workers, initializer=initializer)
232
+
233
+ results = submit_compare_dirs(
234
+ args.a, args.b, executor=executor, diff_output=args.diff_output
235
+ )
236
+
237
+ result = print_results(results, args.a, args.b)
238
+ if (
239
+ result["left_files_missing"]
240
+ or result["right_files_missing"]
241
+ or result["left_dirs_missing"]
242
+ or result["right_dirs_missing"]
243
+ or result["files_different"]
244
+ ):
245
+ return 1
246
+
247
+ return 0
248
+
249
+
250
+ if __name__ == "__main__":
251
+ sys.exit(main())
@@ -0,0 +1,310 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import sys
6
+ import argparse
7
+ import threading
8
+ import io
9
+ from concurrent.futures import ThreadPoolExecutor
10
+ from html_render_diff import get_browser, html_render_diff
11
+ from compare_output import comparable_file, compare_files
12
+ from flask import Flask, send_from_directory, send_file
13
+ import watchdog.observers
14
+ import watchdog.events
15
+
16
+
17
+ class Config:
18
+ path_a = None
19
+ path_b = None
20
+ driver = None
21
+ observer = None
22
+ comparator = None
23
+ browser = None
24
+ thread_local = threading.local()
25
+
26
+
27
+ class Observer:
28
+ def __init__(self):
29
+ class Handler(watchdog.events.FileSystemEventHandler):
30
+ def __init__(self, path):
31
+ self._path = path
32
+
33
+ def dispatch(self, event):
34
+ if event.event_type in ["opened"]:
35
+ return
36
+
37
+ if os.path.isfile(event.src_path):
38
+ Config.comparator.submit(
39
+ os.path.relpath(event.src_path, self._path)
40
+ )
41
+
42
+ self._observer = watchdog.observers.Observer()
43
+ self._observer.schedule(Handler(Config.path_a), Config.path_a, recursive=True)
44
+ self._observer.schedule(Handler(Config.path_b), Config.path_b, recursive=True)
45
+
46
+ def start(self):
47
+ self._observer.start()
48
+
49
+ def init_compare(a, b):
50
+ common_path = os.path.relpath(a, Config.path_a)
51
+
52
+ left = sorted(os.listdir(a))
53
+ right = sorted(os.listdir(b))
54
+
55
+ common = [name for name in left if name in right]
56
+
57
+ for name in common:
58
+ if os.path.isfile(os.path.join(a, name)) and comparable_file(
59
+ os.path.join(a, name)
60
+ ):
61
+ Config.comparator.submit(os.path.join(common_path, name))
62
+ elif os.path.isdir(os.path.join(a, name)):
63
+ init_compare(os.path.join(a, name), os.path.join(b, name))
64
+
65
+ init_compare(Config.path_a, Config.path_b)
66
+
67
+ def stop(self):
68
+ self._observer.stop()
69
+
70
+ def join(self):
71
+ self._observer.join()
72
+
73
+
74
+ class Comparator:
75
+ def __init__(self, max_workers):
76
+ def initializer():
77
+ browser = getattr(Config.thread_local, "browser", None)
78
+ if browser is None:
79
+ browser = get_browser(driver=Config.driver)
80
+ Config.thread_local.browser = browser
81
+
82
+ self._executor = ThreadPoolExecutor(
83
+ max_workers=max_workers, initializer=initializer
84
+ )
85
+ self._result = {}
86
+ self._future = {}
87
+
88
+ def submit(self, path):
89
+ if path in self._future:
90
+ try:
91
+ self._future[path].cancel()
92
+ self._future[path].result()
93
+ self._future.pop(path)
94
+ except Exception:
95
+ pass
96
+
97
+ self._result[path] = "pending"
98
+ self._future[path] = self._executor.submit(self.compare, path)
99
+
100
+ def compare(self, path):
101
+ browser = getattr(Config.thread_local, "browser", None)
102
+ result = compare_files(
103
+ os.path.join(Config.path_a, path),
104
+ os.path.join(Config.path_b, path),
105
+ browser=browser,
106
+ )
107
+ self._result[path] = "same" if result else "different"
108
+ self._future.pop(path)
109
+
110
+ def result(self, path):
111
+ if path in self._result:
112
+ return self._result[path]
113
+ return "unknown"
114
+
115
+ def result_symbol(self, path):
116
+ result = self.result(path)
117
+ if result == "pending":
118
+ return "🔄"
119
+ if result == "same":
120
+ return "✔"
121
+ if result == "different":
122
+ return "❌"
123
+ return "⛔"
124
+
125
+ def result_css(self, path):
126
+ result = self.result(path)
127
+ if result == "pending":
128
+ return "color:blue;"
129
+ if result == "same":
130
+ return "color:green;"
131
+ if result == "different":
132
+ return "color:orange;"
133
+ return "color:red;"
134
+
135
+
136
+ app = Flask("compare")
137
+
138
+
139
+ @app.route("/")
140
+ def root():
141
+ def print_tree(a, b):
142
+ common_path = os.path.relpath(a, Config.path_a)
143
+
144
+ left = sorted(os.listdir(a))
145
+ right = sorted(os.listdir(b))
146
+
147
+ left_files = sorted(
148
+ [
149
+ name
150
+ for name in left
151
+ if os.path.isfile(os.path.join(a, name))
152
+ and comparable_file(os.path.join(a, name))
153
+ ]
154
+ )
155
+ right_files = sorted(
156
+ [
157
+ name
158
+ for name in right
159
+ if os.path.isfile(os.path.join(b, name))
160
+ and comparable_file(os.path.join(b, name))
161
+ ]
162
+ )
163
+
164
+ left_dirs = sorted(
165
+ [name for name in left if os.path.isdir(os.path.join(a, name))]
166
+ )
167
+ right_dirs = sorted(
168
+ [name for name in right if os.path.isdir(os.path.join(b, name))]
169
+ )
170
+
171
+ common_files = [name for name in left_files if name in right_files]
172
+ common_dirs = [name for name in left_dirs if name in right_dirs]
173
+
174
+ result = "<ul>"
175
+
176
+ left_files_missing = " ".join(
177
+ [name for name in right_files if name not in left_files]
178
+ )
179
+ right_files_missing = " ".join(
180
+ [name for name in left_files if name not in right_files]
181
+ )
182
+ left_dirs_missing = " ".join(
183
+ [name for name in right_dirs if name not in left_dirs]
184
+ )
185
+ right_dirs_missing = " ".join(
186
+ [name for name in left_dirs if name not in right_dirs]
187
+ )
188
+ if left_files_missing:
189
+ result += f"<li><b>A files missing: {left_files_missing}</b></li>"
190
+ if right_files_missing:
191
+ result += f"<li><b>B files missing: {right_files_missing}</b></li>"
192
+ if left_dirs_missing:
193
+ result += f"<li><b>A dirs missing: {left_dirs_missing}</b></li>"
194
+ if right_dirs_missing:
195
+ result += f"<li><b>B dirs missing: {right_dirs_missing}</b></li>"
196
+
197
+ for name in common_files:
198
+ if Config.comparator is None:
199
+ result += f'<li><a href="/compare/{os.path.join(common_path, name)}">{name}</a></li>'
200
+ else:
201
+ symbol = Config.comparator.result_symbol(
202
+ os.path.join(common_path, name)
203
+ )
204
+ css = Config.comparator.result_css(os.path.join(common_path, name))
205
+ result += f'<li style="{css}"><a style="{css}" href="/compare/{os.path.join(common_path, name)}">{name}</a> {symbol}</li>'
206
+
207
+ for name in common_dirs:
208
+ result += f"<li>{name}"
209
+ result += print_tree(os.path.join(a, name), os.path.join(b, name))
210
+ result += "</li>"
211
+
212
+ result += "</ul>"
213
+ return result
214
+
215
+ result = ""
216
+ result += f"<p>compare A {Config.path_a} vs B {Config.path_b}</p>"
217
+ result += print_tree(Config.path_a, Config.path_b)
218
+ return result
219
+
220
+
221
+ @app.route("/compare/<path:path>")
222
+ def compare(path):
223
+ return f"""<!DOCTYPE html>
224
+ <html>
225
+ <head>
226
+ <style>
227
+ html,body {{height:100%;margin:0;}}
228
+ </style>
229
+ </head>
230
+ <body style="display:flex;flex-flow:row;">
231
+ <div style="display:flex;flex:1;flex-flow:column;margin:5px;">
232
+ <a href="/file/a/{path}">{os.path.join(Config.path_a, path)}</a>
233
+ <iframe id="a" src="/file/a/{path}" title="a" frameborder="0" align="left" style="flex:1;"></iframe>
234
+ </div>
235
+ <div style="display:flex;flex:0 0 50px;flex-flow:column;">
236
+ <a href="/image_diff/{path}">diff</a>
237
+ <img src="/image_diff/{path}" width="50" height="0" style="flex:1;">
238
+ </div>
239
+ <div style="display:flex;flex:1;flex-flow:column;margin:5px;">
240
+ <a href="/file/b/{path}">{os.path.join(Config.path_b, path)}</a>
241
+ <iframe id="b" src="/file/b/{path}" title="b" frameborder="0" align="right" style="flex:1;"></iframe>
242
+ </div>
243
+ <script>
244
+ var iframe_a = document.getElementById('a');
245
+ var iframe_b = document.getElementById('b');
246
+ iframe_a.contentWindow.addEventListener('scroll', function(event) {{
247
+ iframe_b.contentWindow.scrollTo(iframe_a.contentWindow.scrollX, iframe_a.contentWindow.scrollY);
248
+ }});
249
+ iframe_b.contentWindow.addEventListener('scroll', function(event) {{
250
+ iframe_a.contentWindow.scrollTo(iframe_b.contentWindow.scrollX, iframe_b.contentWindow.scrollY);
251
+ }});
252
+ </script>
253
+ </body>
254
+ </html>
255
+ """
256
+
257
+
258
+ @app.route("/image_diff/<path:path>")
259
+ def image_diff(path):
260
+ diff, _ = html_render_diff(
261
+ os.path.join(Config.path_a, path),
262
+ os.path.join(Config.path_b, path),
263
+ Config.browser,
264
+ )
265
+ tmp = io.BytesIO()
266
+ diff.save(tmp, "JPEG", quality=70)
267
+ tmp.seek(0)
268
+ return send_file(tmp, mimetype="image/jpeg")
269
+
270
+
271
+ @app.route("/file/<variant>/<path:path>")
272
+ def file(variant, path):
273
+ variant_root = Config.path_a if variant == "a" else Config.path_b
274
+ return send_from_directory(variant_root, path)
275
+
276
+
277
+ def main():
278
+ parser = argparse.ArgumentParser()
279
+ parser.add_argument("a")
280
+ parser.add_argument("b")
281
+ parser.add_argument(
282
+ "--driver", choices=["chrome", "firefox", "phantomjs"], default="firefox"
283
+ )
284
+ parser.add_argument("--max-workers", type=int, default=1)
285
+ parser.add_argument("--compare", action="store_true")
286
+ parser.add_argument("--port", type=int, default=5000)
287
+ args = parser.parse_args()
288
+
289
+ Config.path_a = args.a
290
+ Config.path_b = args.b
291
+ Config.driver = args.driver
292
+ Config.browser = get_browser(driver=args.driver)
293
+
294
+ if args.compare:
295
+ Config.comparator = Comparator(max_workers=args.max_workers)
296
+
297
+ Config.observer = Observer()
298
+ Config.observer.start()
299
+
300
+ app.run(host="0.0.0.0", port=args.port)
301
+
302
+ if args.compare:
303
+ Config.observer.stop()
304
+ Config.observer.join()
305
+
306
+ return 0
307
+
308
+
309
+ if __name__ == "__main__":
310
+ sys.exit(main())
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import sys
6
+ import argparse
7
+ import io
8
+ import time
9
+
10
+ from PIL import Image, ImageChops
11
+ from selenium import webdriver
12
+ from selenium.webdriver.common.by import By
13
+ from selenium.webdriver.support import expected_conditions
14
+ from selenium.webdriver.support.ui import WebDriverWait
15
+ import pathlib
16
+
17
+
18
+ def to_url(something):
19
+ if os.path.isfile(something):
20
+ return pathlib.Path(os.path.abspath(something)).as_uri()
21
+ return something
22
+
23
+
24
+ def screenshot(browser, url):
25
+ browser.get(url)
26
+
27
+ target_find_by = By.TAG_NAME
28
+ target = "body"
29
+ loaded_page_settling_time = 0
30
+
31
+ # Selenium doesn't like when we try to screenshot <body> element of documents generated by pdf2htmlEX
32
+ if "poppler" in url:
33
+ target_find_by = By.ID
34
+ target = "page-container"
35
+ loaded_page_settling_time = 1
36
+
37
+ web_driver_wait = WebDriverWait(browser, 5)
38
+ web_driver_wait.until(
39
+ expected_conditions.presence_of_element_located((target_find_by, target))
40
+ )
41
+ web_driver_wait.until(
42
+ lambda driver: driver.execute_script("return document.readyState") == "complete"
43
+ )
44
+ if loaded_page_settling_time != 0:
45
+ time.sleep(loaded_page_settling_time)
46
+
47
+ target_element = browser.find_element(target_find_by, target)
48
+
49
+ png = target_element.screenshot_as_png
50
+ return Image.open(io.BytesIO(png))
51
+
52
+
53
+ def get_browser(driver="firefox", max_width=1000, max_height=10000):
54
+ if driver == "phantomjs":
55
+ browser = webdriver.PhantomJS()
56
+ elif driver == "firefox":
57
+ options = webdriver.FirefoxOptions()
58
+ options.add_argument("--headless")
59
+ browser = webdriver.Firefox(options=options)
60
+ else: # chrome or unknown
61
+ options = webdriver.ChromeOptions()
62
+ options.add_argument("--headless=new")
63
+ browser = webdriver.Chrome(options=options)
64
+ browser.set_window_size(max_width, max_height)
65
+ return browser
66
+
67
+
68
+ def html_render_diff(a, b, browser):
69
+ image_a = screenshot(browser, to_url(a))
70
+ image_b = screenshot(browser, to_url(b))
71
+
72
+ image_a = image_a.convert("RGB")
73
+ image_b = image_b.convert("RGB")
74
+ diff = ImageChops.difference(image_a, image_b)
75
+ return diff, (image_a, image_b)
76
+
77
+
78
+ def main():
79
+ parser = argparse.ArgumentParser()
80
+ parser.add_argument("a")
81
+ parser.add_argument("b")
82
+ parser.add_argument(
83
+ "--driver", choices=["chrome", "firefox", "phantomjs"], default="firefox"
84
+ )
85
+ parser.add_argument("--max-width", default=1000)
86
+ parser.add_argument("--max-height", default=10000)
87
+ args = parser.parse_args()
88
+
89
+ browser = get_browser(args.driver, args.max_width, args.max_height)
90
+ diff, _ = html_render_diff(args.a, args.b, browser)
91
+ browser.quit()
92
+ bounding_box = diff.getbbox()
93
+
94
+ if bounding_box:
95
+ print("images are different")
96
+ print("first error at %f%%" % (1e2 * bounding_box[1] / diff.height))
97
+ print(
98
+ "bounding box %f%%"
99
+ % (
100
+ 1e2
101
+ * (
102
+ (bounding_box[2] - bounding_box[0])
103
+ * (bounding_box[3] - bounding_box[1])
104
+ )
105
+ / (diff.width * diff.height)
106
+ )
107
+ )
108
+ return 1
109
+
110
+ print("images are the same")
111
+ return 0
112
+
113
+
114
+ if __name__ == "__main__":
115
+ sys.exit(main())
@@ -0,0 +1,102 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import os
5
+ import sys
6
+ import argparse
7
+ import json
8
+ import subprocess
9
+ import shlex
10
+ from common import bcolors
11
+
12
+
13
+ def tidy_json(path):
14
+ try:
15
+ with open(path, "r") as f:
16
+ json.load(f)
17
+ return 0
18
+ except ValueError:
19
+ return 1
20
+
21
+
22
+ def tidy_html(path):
23
+ config_path = os.path.join(
24
+ os.path.dirname(os.path.realpath(__file__)), ".html-tidy"
25
+ )
26
+ cmd = shlex.split(f'tidy -config "{config_path}" -q "{path}"')
27
+ result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
28
+ if result.returncode == 1:
29
+ return 1
30
+ if result.returncode > 1:
31
+ return 2
32
+ return 0
33
+
34
+
35
+ def tidy_file(path):
36
+ if path.endswith(".json"):
37
+ return tidy_json(path)
38
+ elif path.endswith(".html"):
39
+ return tidy_html(path)
40
+
41
+
42
+ def tidyable_file(path):
43
+ if path.endswith(".json"):
44
+ return True
45
+ if path.endswith(".html"):
46
+ return True
47
+ return False
48
+
49
+
50
+ def tidy_dir(path, level=0, prefix=""):
51
+ prefix_file = prefix + "├── "
52
+ if level == 0:
53
+ print(f"tidy dir {path}")
54
+
55
+ result = {
56
+ "warning": [],
57
+ "error": [],
58
+ }
59
+
60
+ items = [os.path.join(path, name) for name in os.listdir(path)]
61
+ files = sorted(
62
+ [path for path in items if os.path.isfile(path) and tidyable_file(path)]
63
+ )
64
+ dirs = sorted([path for path in items if os.path.isdir(path)])
65
+
66
+ for filename in [os.path.basename(path) for path in files]:
67
+ filepath = os.path.join(path, filename)
68
+ tidy = tidy_file(filepath)
69
+ if tidy == 0:
70
+ print(f"{prefix_file}{bcolors.OKGREEN}{filename} ✓{bcolors.ENDC}")
71
+ elif tidy == 1:
72
+ print(f"{prefix_file}{bcolors.WARNING}{filename} ✓{bcolors.ENDC}")
73
+ result["warning"].append(filepath)
74
+ elif tidy > 1:
75
+ print(f"{prefix_file}{bcolors.FAIL}{filename} ✘{bcolors.ENDC}")
76
+ result["error"].append(filepath)
77
+
78
+ for dirname in [os.path.basename(path) for path in dirs]:
79
+ print(prefix + "├── " + dirname)
80
+ subresult = tidy_dir(
81
+ os.path.join(path, dirname), level=level + 1, prefix=prefix + "│ "
82
+ )
83
+ result["warning"].extend(subresult["warning"])
84
+ result["error"].extend(subresult["error"])
85
+
86
+ return result
87
+
88
+
89
+ def main():
90
+ parser = argparse.ArgumentParser()
91
+ parser.add_argument("a")
92
+ args = parser.parse_args()
93
+
94
+ result = tidy_dir(args.a)
95
+ if result["error"]:
96
+ return 1
97
+
98
+ return 0
99
+
100
+
101
+ if __name__ == "__main__":
102
+ sys.exit(main())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: htmlcmp
3
- Version: 1.0.0
3
+ Version: 1.0.1
4
4
  Summary: Compare HTML files by rendered output
5
5
  Home-page: https://github.com/opendocument-app/compare-html
6
6
  Author: Andreas Stefl
@@ -0,0 +1,14 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.py
4
+ src/htmlcmp/__init__.py
5
+ src/htmlcmp/common.py
6
+ src/htmlcmp/compare_output.py
7
+ src/htmlcmp/compare_output_server.py
8
+ src/htmlcmp/html_render_diff.py
9
+ src/htmlcmp/tidy_output.py
10
+ src/htmlcmp.egg-info/PKG-INFO
11
+ src/htmlcmp.egg-info/SOURCES.txt
12
+ src/htmlcmp.egg-info/dependency_links.txt
13
+ src/htmlcmp.egg-info/entry_points.txt
14
+ src/htmlcmp.egg-info/top_level.txt
@@ -0,0 +1 @@
1
+ htmlcmp
@@ -1,8 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- setup.py
4
- src/htmlcmp.egg-info/PKG-INFO
5
- src/htmlcmp.egg-info/SOURCES.txt
6
- src/htmlcmp.egg-info/dependency_links.txt
7
- src/htmlcmp.egg-info/entry_points.txt
8
- src/htmlcmp.egg-info/top_level.txt
File without changes
File without changes