From eccdf88a599b416d3eb31434ef3cd21dad41eee5 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 2 Jan 2023 16:25:20 +0100 Subject: [PATCH 01/19] Add --nofilename option --- src/wsdiff.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/wsdiff.py b/src/wsdiff.py index 777c2a5..f207911 100644 --- a/src/wsdiff.py +++ b/src/wsdiff.py @@ -616,7 +616,7 @@ class RecordFormatter(Formatter): for _ours_empty, (lineno_theirs, _diff_theirs), change in diff: self.lines.append(f'') - assert change and lineno_theirs + #assert change and lineno_theirs def html_diff_content(old, new, lexer): diff = list(difflib._mdiff(old.splitlines(), new.splitlines())) @@ -629,10 +629,13 @@ def html_diff_content(old, new, lexer): return '\n'.join(chain.from_iterable(zip(fmt_l.lines, fmt_r.lines))) -def html_diff_block(old, new, filename, lexer): +def html_diff_block(old, new, filename, lexer, hide_filename=True): code = html_diff_content(old, new, lexer) + filename = f'
‭{filename}
' + if hide_filename: + filename = '' return textwrap.dedent(f'''
-
‭{filename}
+ {filename}
{code}
@@ -649,6 +652,7 @@ def cli(): parser.add_argument('-o', '--output', default=sys.stdout, type=argparse.FileType('w'), help='Name of output file (default: stdout)') parser.add_argument('--header', action='store_true', help='Only output HTML header with stylesheets and stuff, and no diff') parser.add_argument('--content', action='store_true', help='Only output HTML content, without header') + parser.add_argument('--nofilename', action='store_true', help='Do not output file name headers') parser.add_argument('old', nargs='?', help='source file or directory to compare ("before" file)') parser.add_argument('new', nargs='?', help='source file or directory to compare ("after" file)') args = parser.parse_args() @@ -724,7 +728,7 @@ def cli(): except: lexer = get_lexer_by_name('text') - diff_blocks.append(html_diff_block(old_text, new_text, suffix, lexer)) + diff_blocks.append(html_diff_block(old_text, new_text, suffix, lexer, hide_filename=args.nofilename)) body = '\n'.join(diff_blocks) if args.content: From c3f4bd6b27fd628e70e9ed01358eb0452d097034 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 8 Jan 2023 12:49:42 +0100 Subject: [PATCH 02/19] Remove bad asserts --- src/wsdiff.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/wsdiff.py b/src/wsdiff.py index f207911..2c5dbb5 100644 --- a/src/wsdiff.py +++ b/src/wsdiff.py @@ -570,7 +570,6 @@ class RecordFormatter(Formatter): break else: self.lines.append(f'') - assert lineno_ours == lineno if not change: change_class = '' @@ -616,7 +615,6 @@ class RecordFormatter(Formatter): for _ours_empty, (lineno_theirs, _diff_theirs), change in diff: self.lines.append(f'') - #assert change and lineno_theirs def html_diff_content(old, new, lexer): diff = list(difflib._mdiff(old.splitlines(), new.splitlines())) From 715ddb860287d5eea3d4126a9b9d5a2da88dd0a7 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 8 Jan 2023 12:55:37 +0100 Subject: [PATCH 03/19] Modularize template --- src/wsdiff.py | 207 +++++++++++++++++++++++++------------------------- 1 file changed, 102 insertions(+), 105 deletions(-) diff --git a/src/wsdiff.py b/src/wsdiff.py index 2c5dbb5..b83c523 100644 --- a/src/wsdiff.py +++ b/src/wsdiff.py @@ -40,17 +40,21 @@ from pygments.lexer import RegexLexer from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename, get_all_lexers, LEXERS from pygments import token +DIFF_STYLE_TOGGLE = r''' +
+
+ Split view + +
+
+
+
+
+
+
+''' -HTML_TEMPLATE = r''' - - - - - $title - - - - - +''' + +DIFF_STYLE_SCRIPT = r''' + const findStylesheet = (id => Array.from(document.styleSheets).find(element => element.ownerNode && element.ownerNode.id == id)); + const findRule = ((stylesheet, name) => Array.from(stylesheet.cssRules).find( + element => (element instanceof CSSLayerBlockRule && element.name == name)).cssRules[0]); + + const automaticMediaElement = findRule(findStylesheet('main-style'), 'automatic-media-rule'); + const automaticMediaRule = automaticMediaElement.media[0]; + const impossibleMediaRule = "screen and (max-width: 0px)"; + const tautologicalMediaRule = "screen and (min-width: 0px)"; + + const toggleAuto = document.getElementById("toggle-split-auto"); + const toggleForce = document.getElementById("toggle-split-force"); + toggleAuto.checked = true; + toggleForce.disabled = true; + + toggleAuto.addEventListener('change', (event) => { + const automatic = toggleAuto.checked; + toggleForce.disabled = automatic; + if (automatic) { + automaticMediaElement.media.deleteMedium(automaticMediaElement.media[0]); + automaticMediaElement.media.appendMedium(automaticMediaRule); + } else { + automaticMediaElement.media.deleteMedium(automaticMediaRule); + if (toggleForce.checked) { + automaticMediaElement.media.appendMedium(impossibleMediaRule); + } else { + automaticMediaElement.media.appendMedium(tautologicalMediaRule); + } + } + }); + + toggleForce.addEventListener('change', (event) => { + const automatic = toggleAuto.checked; + if (!automatic) { + automaticMediaElement.media.deleteMedium(automaticMediaElement.media[0]); + if (toggleForce.checked) { + automaticMediaElement.media.appendMedium(impossibleMediaRule); + } else { + automaticMediaElement.media.appendMedium(tautologicalMediaRule); + } + } + }); + + const mediaMatch = window.matchMedia(automaticMediaRule); + mediaMatch.addEventListener('change', (event) => { + const automatic = toggleAuto.checked; + if (automatic) { + toggleForce.checked = !event.matches; + } + }); + toggleForce.checked = !mediaMatch.matches; + + document.getElementById('js-controls').style = 'display: flex'; +''' + +HTML_TEMPLATE = r''' + + + + + $title + + + + + -
-
- Split view - -
-
-
-
-
+ $diff_style_toggle + +
+ $body
-
- -
-$body -
''' @@ -513,22 +524,6 @@ span.clearbg { } ''' -class SexprLexer(RegexLexer): - name = 'KiCad S-Expression' - aliases = ['sexp'] - filenames = ['*.kicad_mod', '*.kicad_sym'] - - tokens = { - 'root': [ - (r'\s+', token.Whitespace), - (r'[()]', token.Punctuation), - (r'([+-]?\d+\.\d+)(?=[)\s])', token.Number), - (r'(-?\d+)(?=[)\s])', token.Number), - (r'"((?:[^"]|\\")*)"(?=[)\s])', token.String), - (r'([^()"\s]+)(?=[)\s])', token.Name), - ] - } - from pygments.formatter import Formatter from pygments.token import STANDARD_TYPES @@ -674,6 +669,9 @@ def cli(): print(string.Template(HTML_TEMPLATE).substitute( title=pagetitle, pygments_css=syntax_css, + main_css=MAIN_CSS, + diff_style_toggle=DIFF_STYLE_TOGGLE, + diff_style_script=DIFF_STYLE_SCRIPT, body='$body'), file=args.output) sys.exit(0) @@ -717,14 +715,10 @@ def cli(): if args.lexer: lexer = get_lexer_by_name(lexer) else: - if new.suffix.lower() in ('.kicad_mod', '.kicad_mod', '.kicad_pcb', '.kicad_sch')\ - or new.name == 'sym_lib_table': - lexer = SexprLexer() - else: - try: - lexer = guess_lexer_for_filename(new, new_text) - except: - lexer = get_lexer_by_name('text') + try: + lexer = guess_lexer_for_filename(new, new_text) + except: + lexer = get_lexer_by_name('text') diff_blocks.append(html_diff_block(old_text, new_text, suffix, lexer, hide_filename=args.nofilename)) body = '\n'.join(diff_blocks) @@ -735,6 +729,9 @@ def cli(): print(string.Template(HTML_TEMPLATE).substitute( title=pagetitle, pygments_css=syntax_css, + main_css=MAIN_CSS, + diff_style_toggle=DIFF_STYLE_TOGGLE, + diff_style_script=DIFF_STYLE_SCRIPT, body=body), file=args.output) if args.open: From 5dec5a9113ff7310f9eaae419222259f5365b8c8 Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 8 Jan 2023 12:56:00 +0100 Subject: [PATCH 04/19] Bump version to v0.1.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 380cfc2..8486f7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wsdiff" -version = "0.1.0" +version = "0.1.1" authors = [{name="jaseg", email="code@jaseg.de"}] description = "wsdiff is a tool that produces a syntax-highlighted, self-contained, static HTML file that will show a colored, syntax-highlighted diff of two files or folders without external dependencies or javascript." requires-python = ">=3.7" From bac31d9e6610d127b69370d93d24928f88fce61b Mon Sep 17 00:00:00 2001 From: jaseg Date: Sun, 8 Jan 2023 13:11:27 +0100 Subject: [PATCH 05/19] Prefix wsdiff HTML IDs and classes --- src/wsdiff.py | 308 ++++++++++++++++++++++---------------------------- 1 file changed, 136 insertions(+), 172 deletions(-) diff --git a/src/wsdiff.py b/src/wsdiff.py index b83c523..442bf34 100644 --- a/src/wsdiff.py +++ b/src/wsdiff.py @@ -41,13 +41,13 @@ from pygments.lexers import get_lexer_by_name, guess_lexer_for_filename, get_all from pygments import token DIFF_STYLE_TOGGLE = r''' -
-
- Split view - -
-
-
+
+
+ Split view + +
+
+
@@ -55,14 +55,8 @@ DIFF_STYLE_TOGGLE = r''' ''' MAIN_CSS = r''' -@layer base-style { - html, body { - margin: 0; - padding: 0; - font-family: sans-serif; - } - - #js-controls { +@layer wsd-base-style { + #wsd-js-controls { display: none; background-color: #f8f8f8; padding: 5px 20px; @@ -76,51 +70,51 @@ MAIN_CSS = r''' } @media screen and (max-width: 40em) { - #js-controls { + #wsd-js-controls { position: initial; } - .diff { + .wsd-diff { border-top: none; } - .file-title { + .wsd-file-title { background-color: #f8f8f8; border-bottom: solid 1px #e0e0e0; } } - input[type="checkbox"] { + #wsd-js-controls input[type="checkbox"] { width: 20px; height: 20px; } - input, label, .control-label { + #wsd-js-controls input, #wsd-js-controls label, #wsd-js-controls .control-label { vertical-align: middle; } - .field-group { + .wsd-field-group { display: inline-block; } - .field { + .wsd-field { white-space: nowrap; display: inline-block; } - label { + #wsd-js-controls label { font-weight: normal; margin-right: .5em; margin-left: 5px; } - .control-label { + .wsd-control-label { margin-right: .5em; margin-left: 5px; padding-bottom: 3px; } - .file-container { + .wsd-file-container { font-family: monospace; font-size: 9pt; background-color: #f8f8f8; @@ -128,7 +122,7 @@ MAIN_CSS = r''' margin: 15px; } - .file-title { + .wsd-file-title { padding: 10px 20px; font-size: 10pt; font-weight: bold; @@ -138,7 +132,7 @@ MAIN_CSS = r''' display: flex; } - .filename { + .wsd-filename { max-width: 30em; text-overflow: ellipsis; overflow: hidden; @@ -146,58 +140,58 @@ MAIN_CSS = r''' direction: rtl; } - .diff { + .wsd-diff { overflow-x: auto; display: grid; align-items: start; border-top: 1px solid #e0e0e0; } - .line { + .wsd-line { padding-left: calc(4em + 5px); text-indent: -4em; padding-top: 2px; } /* Make individual syntax tokens wrap anywhere */ - .line > span { + .wsd-line > span { overflow-wrap: anywhere; white-space: pre-wrap; } - .line { + .wsd-line { min-width: 15em; } - .line.left.change, .line.left.insert { + .wsd-line.wsd-left.wsd-change, .wsd-line.wsd-left.wsd-insert { background-color: #fbe9eb; } - .line.right.change, .line.right.insert { + .wsd-line.wsd-right.wsd-change, .wsd-line.wsd-right.wsd-insert { background-color: #ecfdf0; } - .lineno.left.change, .lineno.left.insert { + .wsd-lineno.wsd-left.wsd-change, .wsd-lineno.wsd-left.wsd-insert { background-color: #f9d7dc; color: #ae969a; } - .lineno.right.change, .lineno.right.insert { + .wsd-lineno.wsd-right.wsd-change, .wsd-lineno.wsd-right.wsd-insert { background-color: #ddfbe6; color: #9bb0a1; } - .right > .word_change { + .wsd-right > .wsd-word-change { background-color: #c7f0d2; color: #004000; } - .left > .word_change { + .wsd-left > .wsd-word-change { background-color: #fac5cd; color: #400000; } - .lineno { + .wsd-lineno { word-break: keep-all; margin: 0; padding-left: 30px; @@ -211,11 +205,11 @@ MAIN_CSS = r''' align-self: stretch; } - .lineno.change, .lineno.insert { + .wsd-lineno.wsd-change, .wsd-lineno.wsd-insert { color: #000000; } - .lineno::after { + .wsd-lineno::after { position: absolute; right: 0; content: "\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳\a↳"; @@ -224,38 +218,38 @@ MAIN_CSS = r''' } /* Default rules for split diff for wide screens (laptops) */ - .diff { + .wsd-diff { grid-template-columns: min-content 1fr min-content 1fr; } - .empty { + .wsd-empty { background-color: #f0f0f0; align-self: stretch; } /* line continuation arrows only in non-empty lines */ - .lineno.empty::after { + .wsd-lineno.wsd-empty::after { content: ""; } } -@layer automatic-media-rule { +@layer wsd-automatic-media-rule { /* Unified diff for narrow screens (phones) */ @media screen and (max-width: 70em) { - .diff { + .wsd-diff { grid-auto-flow: dense; grid-template-columns: min-content min-content 1fr; } - .lineno { + .wsd-lineno { padding-left: 1em; } - .lineno.left { + .wsd-lineno.wsd-left { grid-column: 1; } - .lineno.left.change, .lineno.right.change { + .wsd-lineno.wsd-left.wsd-change, .wsd-lineno.wsd-right.wsd-change { grid-column: 1 / span 2; display: grid; grid-template-columns: 1fr 1fr; @@ -266,7 +260,7 @@ MAIN_CSS = r''' column-gap: 10px; } - .lineno.right.change::before { + .wsd-lineno.wsd-right.wsd-change::before { content: ""; align-self: stretch; grid-column: 1; @@ -274,7 +268,7 @@ MAIN_CSS = r''' margin-right: -6px; /* move border into column gap, and 1px over to align with other borders */ } - .lineno.left.change::before { + .wsd-lineno.wsd-left.wsd-change::before { content: ""; align-self: stretch; grid-column: 2; @@ -282,15 +276,15 @@ MAIN_CSS = r''' margin-left: -5px; } - .lineno.left.insert { + .wsd-lineno.wsd-left.wsd-insert { border-right: 1px solid #e0c8c8; } - .lineno.right.change::after { + .wsd-lineno.wsd-right.wsd-change::after { grid-column: 2; } - .lineno.left.insert { + .wsd-lineno.wsd-left.wsd-insert { grid-column: 1 / span 2; display: grid; grid-template-columns: 1fr 1fr; @@ -300,43 +294,43 @@ MAIN_CSS = r''' padding-right: 0; } - .lineno.right { + .wsd-lineno.wsd-right { grid-column: 2; } - .lineno.right.insert { + .wsd-lineno.wsd-right.wsd-insert { grid-column: 2; } - .line.left, .line.right.empty { + .wsd-line.wsd-left, .wsd-line.wsd-right.wsd-empty { display: none; } - .line { + .wsd-line { grid-column: 3; } - .line.left.insert { + .wsd-line.wsd-left.wsd-insert { display: block; } - .line.left.change { + .wsd-line.wsd-left.wsd-change { display: block; } - .lineno.right.empty { + .wsd-lineno.wsd-right.wsd-empty { display: none; } - .lineno.left.empty { + .wsd-lineno.wsd-left.wsd-empty { background-color: #ddfbe6; } /* line continuation arrows only in right line number column */ - .lineno.left.insert::after { + .wsd-lineno.wsd-left.wsd-insert::after { } - .lineno.left.insert::before { + .wsd-lineno.wsd-left.wsd-insert::before { content: ""; grid-column: 2; border-left: 1px solid #e0c8c8; /* pick a darker border color inside the light red gutter */ @@ -351,13 +345,13 @@ DIFF_STYLE_SCRIPT = r''' const findRule = ((stylesheet, name) => Array.from(stylesheet.cssRules).find( element => (element instanceof CSSLayerBlockRule && element.name == name)).cssRules[0]); - const automaticMediaElement = findRule(findStylesheet('main-style'), 'automatic-media-rule'); + const automaticMediaElement = findRule(findStylesheet('wsd-main-style'), 'wsd-automatic-media-rule'); const automaticMediaRule = automaticMediaElement.media[0]; const impossibleMediaRule = "screen and (max-width: 0px)"; const tautologicalMediaRule = "screen and (min-width: 0px)"; - const toggleAuto = document.getElementById("toggle-split-auto"); - const toggleForce = document.getElementById("toggle-split-force"); + const toggleAuto = document.getElementById("wsd-toggle-split-auto"); + const toggleForce = document.getElementById("wsd-toggle-split-force"); toggleAuto.checked = true; toggleForce.disabled = true; @@ -398,7 +392,7 @@ DIFF_STYLE_SCRIPT = r''' }); toggleForce.checked = !mediaMatch.matches; - document.getElementById('js-controls').style = 'display: flex'; + document.getElementById('wsd-js-controls').style = 'display: flex'; ''' HTML_TEMPLATE = r''' @@ -410,7 +404,13 @@ HTML_TEMPLATE = r''' - + + + + +
+
+ Split view + +
+
+
+
+
+
+
+ + +
+
+
‭example.py
+
+ 1#!/usr/bin/env python3 +1#!/usr/bin/env python3 +2 +2 +3import math +3import math +4import itertools +4import itertools +5import textwrap +5import textwrap +
+6 +6 +7import click +7import click +8from reedmuller import reedmuller +8from reedmuller import reedmuller +9 +9 +10 +10 +11class Tag: +11class Tag: +12 """ Helper class to ease creation of SVG. All API functions that create SVG allow you to substitute this with your +12 """ Helper class to ease creation of SVG. All API functions that create SVG allow you to substitute this with your +13 own implementation by passing a ``tag`` parameter. """ +13 own implementation by passing a ``tag`` parameter. """ +14 +14 +15 def __init__(self, name, children=None, root=False, **attrs): +15 def __init__(self, name, children=None, root=False, **attrs): +16 if (fill := attrs.get('fill')) and isinstance(fill, tuple): +16 if (fill := attrs.get('fill')) and isinstance(fill, tuple): +17 attrs['fill'], attrs['fill-opacity'] = fill +17 attrs['fill'], attrs['fill-opacity'] = fill +18 if (stroke := attrs.get('stroke')) and isinstance(stroke, tuple): +18 if (stroke := attrs.get('stroke')) and isinstance(stroke, tuple): +19 attrs['stroke'], attrs['stroke-opacity'] = stroke +19 attrs['stroke'], attrs['stroke-opacity'] = stroke +20 self.name, self.attrs = name, attrs +20 self.name, self.attrs = name, attrs +21 self.children = children or [] +21 self.children = children or [] +22 self.root = root +22 self.root = root +23 +23 +24 def __str__(self): +24 def __str__(self): +25 prefix = '<?xml version="1.0" encoding="utf-8"?>\n' if self.root else '' +25 prefix = '<?xml version="1.0" encoding="utf-8"?>\n' if self.root else '' +26 opening = ' '.join([self.name] + [f'{key.replace("__", ":").replace("_", "-")}="{value}"' for key, value in self.attrs.items()]) +26 opening = ' '.join([self.name] + [f'{key.replace("__", ":").replace("_", "-")}="{value}"' for key, value in self.attrs.items()]) +27 if self.children: +27 if self.children: +28 children = '\n'.join(textwrap.indent(str(c), ' ') for c in self.children) +28 children = '\n'.join(textwrap.indent(str(c), ' ') for c in self.children) +29 return f'{prefix}<{opening}>\n{children}\n</{self.name}>' +29 return f'{prefix}<{opening}>\n{children}\n</{self.name}>' +
+30 else: +30 else: +31 return f'{prefix}<{opening}/>' +31 return f'{prefix}<{opening}/>' +32 +32 +33 +33 +34 @classmethod +34 @classmethod +35 def setup_svg(kls, tags, bounds, margin=0, unit='mm', pagecolor='white'): +35 def setup_svg(kls, tags, bounds, unit='mm', pagecolor='white', inkscape=False): +36 (min_x, min_y), (max_x, max_y) = bounds +36 (min_x, min_y), (max_x, max_y) = bounds +37 + +38 if margin: + +39 min_x -= margin + +40 min_y -= margin + +41 max_x += margin + +42 max_y += margin + +43 +37 +44 w, h = max_x - min_x, max_y - min_y +38 w, h = max_x - min_x, max_y - min_y +45 w = 1.0 if math.isclose(w, 0.0) else w +39 w = 1.0 if math.isclose(w, 0.0) else w +46 h = 1.0 if math.isclose(h, 0.0) else h +40 h = 1.0 if math.isclose(h, 0.0) else h +47 +41 + +42 if inkscape: + +43 tags.insert(0, kls('sodipodi:namedview', [], id='namedview1', pagecolor=pagecolor, + +44 inkscape__document_units=unit)) +48 namespaces = dict( +45 namespaces = dict( +49 xmlns="http://www.w3.org/2000/svg", +46 xmlns="http://www.w3.org/2000/svg", + +47 xmlns__xlink="http://www.w3.org/1999/xlink", + +48 xmlns__sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + +49 xmlns__inkscape='http://www.inkscape.org/namespaces/inkscape') + +50 + +51 else: + +52 namespaces = dict( + +53 xmlns="http://www.w3.org/2000/svg", +50 xmlns__xlink="http://www.w3.org/1999/xlink") +54 xmlns__xlink="http://www.w3.org/1999/xlink") +51 +55 +52 return kls('svg', tags, +56 return kls('svg', tags, +53 width=f'{w}{unit}', height=f'{h}{unit}', +57 width=f'{w}{unit}', height=f'{h}{unit}', +54 viewBox=f'{min_x} {min_y} {w} {h}', +58 viewBox=f'{min_x} {min_y} {w} {h}', +55 style=f'background-color:{pagecolor}', +59 style=f'background-color:{pagecolor}', +
+56 **namespaces, +60 **namespaces, +57 root=True) +61 root=True) +58 +62 +59 +63 +60@click.command() +64@click.command() +61@click.option('-h', '--height', type=float, default=20, help='Bar height in mm') +65@click.option('-h', '--height', type=float, default=20, help='Bar height in mm') +62@click.option('-t/-n', '--text/--no-text', default=True, help='Whether to add text containing the data under the bar code') +66@click.option('-t/-n', '--text/--no-text', default=True, help='Whether to add text containing the data under the bar code') +63@click.option('-f', '--font', default='sans-serif', help='Font for the text underneath the bar code') +67@click.option('-f', '--font', default='sans-serif', help='Font for the text underneath the bar code') +64@click.option('-s', '--font-size', type=float, default=12, help='Font size for the text underneath the bar code in points (pt)') +68@click.option('-s', '--font-size', type=float, default=12, help='Font size for the text underneath the bar code in points (pt)') +65@click.option('-b', '--bar-width', type=float, default=1.0, help='Bar width in mm') +69@click.option('-b', '--bar-width', type=float, default=1.0, help='Bar width in mm') +66@click.option('-m', '--margin', type=float, default=3.0, help='Margin around bar code in mm') +70@click.option('-m', '--margin', type=float, default=3.0, help='Margin around bar code in mm') +67@click.option('-c', '--color', default='black', help='SVG color for the bar code') +71@click.option('-c', '--color', default='black', help='SVG color for the bar code') +68@click.option('--text-color', default=None, help='SVG color for the text (defaults to the bar code\'s color)') +72@click.option('--text-color', default=None, help='SVG color for the text (defaults to the bar code\'s color)') +69@click.option('--dpi', type=float, default=96, help='DPI value to assume for internal SVG unit conversions') +73@click.option('--dpi', type=float, default=96, help='DPI value to assume for internal SVG unit conversions') +70@click.argument('data') +74@click.argument('data') +71@click.argument('outfile', type=click.File('w'), default='-') +75@click.argument('outfile', type=click.File('w'), default='-') +72def cli(data, outfile, height, text, font, font_size, bar_width, margin, color, text_color, dpi): +76def cli(data, outfile, height, text, font, font_size, bar_width, margin, color, text_color, dpi): +73 data = int(data, 16) +77 data = int(data, 16) +74 text_color = text_color or color +78 text_color = text_color or color +75 +79 +76 NUM_BITS = 26 +80 NUM_BITS = 26 +77 +81 +78 data_bits = [bool(data & (1<<i)) for i in range(NUM_BITS)] +82 data_bits = [bool(data & (1<<i)) for i in range(NUM_BITS)] +79 data_encoded = itertools.chain(*[ +83 data_encoded = itertools.chain(*[ +80 (a, not a) for a in data_bits +84 (a, not a) for a in data_bits +81 ]) +85 ]) +82 data_encoded = [True, False, True, False, *data_encoded, False, True, True, False, True] +86 data_encoded = [True, False, True, False, *data_encoded, False, True, True, False, True] +83 +87 +84 width = len(data_encoded) * bar_width +88 width = len(data_encoded) * bar_width +85 # 1 px = 0.75 pt +89 # 1 px = 0.75 pt +86 pt_to_mm = lambda pt: pt / 0.75 /dpi * 25.4 +90 pt_to_mm = lambda pt: pt / 0.75 /dpi * 25.4 +87 font_size = pt_to_mm(font_size) +91 font_size = pt_to_mm(font_size) +88 total_height = height + font_size*2 +92 total_height = height + font_size*2 +89 +93 +90 tags = [] +94 tags = [] +91 for key, group in itertools.groupby(enumerate(data_encoded), key=lambda x: x[1]): +95 for key, group in itertools.groupby(enumerate(data_encoded), key=lambda x: x[1]): +92 if key: +96 if key: +93 group = list(group) +97 group = list(group) +94 x0, _key = group[0] +98 x0, _key = group[0] +95 w = len(group) +99 w = len(group) +96 tags.append(Tag('path', stroke=color, stroke_width=w, d=f'M {(x0 + w/2)*bar_width} 0 l 0 {height}')) +100 tags.append(Tag('path', stroke=color, stroke_width=w, d=f'M {(x0 + w/2)*bar_width} 0 l 0 {height}')) +97 +101 +98 if text: +102 if text: +99 tags.append(Tag('text', children=[f'{data:07x}'], +103 tags.append(Tag('text', children=[f'{data:07x}'], +100 x=width/2, y=height + 0.5*font_size, +104 x=width/2, y=height + 0.5*font_size, +101 font_family=font, font_size=f'{font_size:.3f}px', +105 font_family=font, font_size=f'{font_size:.3f}px', +102 text_anchor='middle', dominant_baseline='hanging', +106 text_anchor='middle', dominant_baseline='hanging', +103 fill=text_color)) +107 fill=text_color)) +104 +108 +
+105 outfile.write(str(Tag.setup_svg(tags, bounds=((0, 0), (width, total_height)), margin=margin))) +109 outfile.write(str(Tag.setup_svg(tags, bounds=((0, 0), (width, total_height)), margin=margin))) +106 +110 +107 +111 +108if __name__ == '__main__': +112if __name__ == '__main__': +109 cli() +113 cli() +
+
+
+ + + diff --git a/example.py b/example.py new file mode 100644 index 0000000..2e22e20 --- /dev/null +++ b/example.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 + +import math +import itertools +import textwrap + +import click +from reedmuller import reedmuller + + +class Tag: + """ Helper class to ease creation of SVG. All API functions that create SVG allow you to substitute this with your + own implementation by passing a ``tag`` parameter. """ + + def __init__(self, name, children=None, root=False, **attrs): + if (fill := attrs.get('fill')) and isinstance(fill, tuple): + attrs['fill'], attrs['fill-opacity'] = fill + if (stroke := attrs.get('stroke')) and isinstance(stroke, tuple): + attrs['stroke'], attrs['stroke-opacity'] = stroke + self.name, self.attrs = name, attrs + self.children = children or [] + self.root = root + + def __str__(self): + prefix = '\n' if self.root else '' + opening = ' '.join([self.name] + [f'{key.replace("__", ":").replace("_", "-")}="{value}"' for key, value in self.attrs.items()]) + if self.children: + children = '\n'.join(textwrap.indent(str(c), ' ') for c in self.children) + return f'{prefix}<{opening}>\n{children}\n' + else: + return f'{prefix}<{opening}/>' + + + @classmethod + def setup_svg(kls, tags, bounds, unit='mm', pagecolor='white', inkscape=False): + (min_x, min_y), (max_x, max_y) = bounds + + w, h = max_x - min_x, max_y - min_y + w = 1.0 if math.isclose(w, 0.0) else w + h = 1.0 if math.isclose(h, 0.0) else h + + if inkscape: + tags.insert(0, kls('sodipodi:namedview', [], id='namedview1', pagecolor=pagecolor, + inkscape__document_units=unit)) + namespaces = dict( + xmlns="http://www.w3.org/2000/svg", + xmlns__xlink="http://www.w3.org/1999/xlink", + xmlns__sodipodi='http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd', + xmlns__inkscape='http://www.inkscape.org/namespaces/inkscape') + + else: + namespaces = dict( + xmlns="http://www.w3.org/2000/svg", + xmlns__xlink="http://www.w3.org/1999/xlink") + + return kls('svg', tags, + width=f'{w}{unit}', height=f'{h}{unit}', + viewBox=f'{min_x} {min_y} {w} {h}', + style=f'background-color:{pagecolor}', + **namespaces, + root=True) + + +@click.command() +@click.option('-h', '--height', type=float, default=20, help='Bar height in mm') +@click.option('-t/-n', '--text/--no-text', default=True, help='Whether to add text containing the data under the bar code') +@click.option('-f', '--font', default='sans-serif', help='Font for the text underneath the bar code') +@click.option('-s', '--font-size', type=float, default=12, help='Font size for the text underneath the bar code in points (pt)') +@click.option('-b', '--bar-width', type=float, default=1.0, help='Bar width in mm') +@click.option('-m', '--margin', type=float, default=3.0, help='Margin around bar code in mm') +@click.option('-c', '--color', default='black', help='SVG color for the bar code') +@click.option('--text-color', default=None, help='SVG color for the text (defaults to the bar code\'s color)') +@click.option('--dpi', type=float, default=96, help='DPI value to assume for internal SVG unit conversions') +@click.argument('data') +@click.argument('outfile', type=click.File('w'), default='-') +def cli(data, outfile, height, text, font, font_size, bar_width, margin, color, text_color, dpi): + data = int(data, 16) + text_color = text_color or color + + NUM_BITS = 26 + + data_bits = [bool(data & (1<\n{children}\n' + else: + return f'{prefix}<{opening}/>' + + + @classmethod + def setup_svg(kls, tags, bounds, margin=0, unit='mm', pagecolor='white'): + (min_x, min_y), (max_x, max_y) = bounds + + if margin: + min_x -= margin + min_y -= margin + max_x += margin + max_y += margin + + w, h = max_x - min_x, max_y - min_y + w = 1.0 if math.isclose(w, 0.0) else w + h = 1.0 if math.isclose(h, 0.0) else h + + namespaces = dict( + xmlns="http://www.w3.org/2000/svg", + xmlns__xlink="http://www.w3.org/1999/xlink") + + return kls('svg', tags, + width=f'{w}{unit}', height=f'{h}{unit}', + viewBox=f'{min_x} {min_y} {w} {h}', + style=f'background-color:{pagecolor}', + **namespaces, + root=True) + + +@click.command() +@click.option('-h', '--height', type=float, default=20, help='Bar height in mm') +@click.option('-t/-n', '--text/--no-text', default=True, help='Whether to add text containing the data under the bar code') +@click.option('-f', '--font', default='sans-serif', help='Font for the text underneath the bar code') +@click.option('-s', '--font-size', type=float, default=12, help='Font size for the text underneath the bar code in points (pt)') +@click.option('-b', '--bar-width', type=float, default=1.0, help='Bar width in mm') +@click.option('-m', '--margin', type=float, default=3.0, help='Margin around bar code in mm') +@click.option('-c', '--color', default='black', help='SVG color for the bar code') +@click.option('--text-color', default=None, help='SVG color for the text (defaults to the bar code\'s color)') +@click.option('--dpi', type=float, default=96, help='DPI value to assume for internal SVG unit conversions') +@click.argument('data') +@click.argument('outfile', type=click.File('w'), default='-') +def cli(data, outfile, height, text, font, font_size, bar_width, margin, color, text_color, dpi): + data = int(data, 16) + text_color = text_color or color + + NUM_BITS = 26 + + data_bits = [bool(data & (1< Date: Mon, 28 Jul 2025 10:55:09 +0200 Subject: [PATCH 16/19] Fix collapse controls background color --- src/wsdiff.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/wsdiff.py b/src/wsdiff.py index 00096fc..b0cd80b 100644 --- a/src/wsdiff.py +++ b/src/wsdiff.py @@ -337,6 +337,7 @@ MAIN_CSS = r''' background-size: 10px 10px; background-position: center; background-repeat: repeat-x; + background-color: var(--c-bg-auxiliary) } .wsd-collapse-controls > label { From ed8285192edd551d3904b8670b8ce49ebfa94440 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 28 Jul 2025 10:55:45 +0200 Subject: [PATCH 17/19] Bump version to v0.3.1 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 269061b..a26ebe3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wsdiff" -version = "0.3.0" +version = "0.3.1" authors = [{name="jaseg", email="code@jaseg.de"}] description = "wsdiff is a tool that produces a syntax-highlighted, self-contained, static HTML file that will show a colored, syntax-highlighted diff of two files or folders without external dependencies or javascript." requires-python = ">=3.7" From 3bb0760be2c758cbaa4f21ad655b5311bbb40153 Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 28 Jul 2025 11:08:19 +0200 Subject: [PATCH 18/19] Fix leading newline --- src/wsdiff.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/wsdiff.py b/src/wsdiff.py index b0cd80b..bf004cc 100644 --- a/src/wsdiff.py +++ b/src/wsdiff.py @@ -515,8 +515,7 @@ DIFF_STYLE_SCRIPT = r''' document.getElementById('wsd-js-controls').style = 'display: flex'; ''' -HTML_TEMPLATE = r''' - +HTML_TEMPLATE = r''' From 64b5f981130bfa39a966a0b2c7a8ee56618da1bb Mon Sep 17 00:00:00 2001 From: jaseg Date: Mon, 28 Jul 2025 11:08:35 +0200 Subject: [PATCH 19/19] Bump version to v0.3.2 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index a26ebe3..92d5b94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "wsdiff" -version = "0.3.1" +version = "0.3.2" authors = [{name="jaseg", email="code@jaseg.de"}] description = "wsdiff is a tool that produces a syntax-highlighted, self-contained, static HTML file that will show a colored, syntax-highlighted diff of two files or folders without external dependencies or javascript." requires-python = ">=3.7"