home · contact · privacy
Edit (raw) edit page testing, improve templates. master
authorChristian Heller <c.heller@plomlompom.de>
Thu, 15 Jan 2026 23:52:17 +0000 (00:52 +0100)
committerChristian Heller <c.heller@plomlompom.de>
Thu, 15 Jan 2026 23:52:17 +0000 (00:52 +0100)
src/ledgplom/http.py
src/ledgplom/ledger.py
src/ledgplom/testing.py
src/templates/_macros.tmpl
src/templates/edit_raw.tmpl
src/templates/edit_structured.tmpl
src/tests/full.edit_raw.1 [new file with mode: 0644]
src/tests/full.edit_raw.4 [new file with mode: 0644]

index ce4bfdde10510f0a1201223a67a1661737617d15..e151b0fcbf3c655fe16041488e38999f409fbd25 100644 (file)
@@ -11,16 +11,16 @@ from ledgplom.ledger import DEFAULT_INDENT, Ledger
 PATH_TEMPLATES = Path('templates')
 _PREFIX_EDIT = 'edit_'
 _PREFIX_FILE = 'file_'
-_PREFIX_LEDGER = 'ledger_'
+PREFIX_LEDGER = 'ledger_'
 _SERVER_HOST = '127.0.0.1'
 _SERVER_PORT = 8084
-_TOK_RAW = 'raw'
-_TOK_STRUCTURED = 'structured'
+_SUFFIX_RAW = 'raw'
+_SUFFIX_STRUCTURED = 'structured'
 PAGENAME_BALANCE = 'balance'
-_PAGENAME_EDIT_RAW = f'{_PREFIX_EDIT}{_TOK_RAW}'
-_PAGENAME_EDIT_STRUCTURED = f'{_PREFIX_EDIT}{_TOK_STRUCTURED}'
-PAGENAME_LEDGER_RAW = f'{_PREFIX_LEDGER}{_TOK_RAW}'
-PAGENAME_LEDGER_STRUCTURED = f'{_PREFIX_LEDGER}{_TOK_STRUCTURED}'
+PAGENAME_EDIT_RAW = f'{_PREFIX_EDIT}{_SUFFIX_RAW}'
+PAGENAME_EDIT_STRUCTURED = f'{_PREFIX_EDIT}{_SUFFIX_STRUCTURED}'
+PAGENAME_LEDGER_RAW = f'{PREFIX_LEDGER}{_SUFFIX_RAW}'
+PAGENAME_LEDGER_STRUCTURED = f'{PREFIX_LEDGER}{_SUFFIX_STRUCTURED}'
 
 
 class Server(PlomHttpServer):
@@ -48,7 +48,7 @@ class _Handler(PlomHttpHandler):
         elif self.pagename.startswith(_PREFIX_EDIT)\
                 and self.postvars.first('apply'):
             redir_target = self.post_edit()
-        elif self.pagename.startswith(_PREFIX_LEDGER):
+        elif self.pagename.startswith(PREFIX_LEDGER):
             redir_target = self.post_ledger_action()
         self.redirect(redir_target)
 
@@ -63,7 +63,7 @@ class _Handler(PlomHttpHandler):
         'Based on postvars, edit targeted Booking.'
         old_id = int(self.path_toks[2])
         new_lines = []
-        if self.pagename == _PAGENAME_EDIT_STRUCTURED:
+        if self.pagename == PAGENAME_EDIT_STRUCTURED:
             line_keys = self.postvars.keys_prefixed('line_')
             lineno_to_inputs: dict[int, list[str]] = {}
             for key in line_keys:
@@ -92,8 +92,8 @@ class _Handler(PlomHttpHandler):
             return Path(self.path)
         if 'add_booking' in self.postvars.as_dict:
             id_ = self.server.ledger.add_empty_block()
-            return Path('/', _PAGENAME_EDIT_STRUCTURED, f'{id_}')
-        keys_prefixed = self.postvars.keys_prefixed(_PREFIX_LEDGER)
+            return Path('/', PAGENAME_EDIT_STRUCTURED, f'{id_}')
+        keys_prefixed = self.postvars.keys_prefixed(PREFIX_LEDGER)
         action, id_str = keys_prefixed[0].split('_', maxsplit=2)[1:]
         id_ = int(id_str)
         if action.startswith('move'):
@@ -106,15 +106,15 @@ class _Handler(PlomHttpHandler):
         'Route GET requests to respective handlers.'
         if self.pagename == 'blocks':
             self.redirect(
-                Path('/', _PAGENAME_EDIT_STRUCTURED, self.path_toks[2]))
+                Path('/', PAGENAME_EDIT_STRUCTURED, self.path_toks[2]))
             return
         ctx = {'unsaved_changes': self.server.ledger.tainted,
                'path': self.path}
         if self.pagename == PAGENAME_BALANCE:
             self.get_balance(ctx)
         elif self.pagename.startswith(_PREFIX_EDIT):
-            self.get_edit(ctx, self.pagename == _PAGENAME_EDIT_RAW)
-        elif self.pagename.startswith(_PREFIX_LEDGER):
+            self.get_edit(ctx, self.pagename == PAGENAME_EDIT_RAW)
+        elif self.pagename.startswith(PREFIX_LEDGER):
             self.get_ledger(ctx, self.pagename == PAGENAME_LEDGER_RAW)
         else:
             self.get_ledger(ctx, False)
@@ -130,7 +130,7 @@ class _Handler(PlomHttpHandler):
     def get_edit(self, ctx, raw: bool) -> None:
         'Display edit form for individual Booking.'
         self._send_rendered(
-                _PAGENAME_EDIT_RAW if raw else _PAGENAME_EDIT_STRUCTURED,
+                PAGENAME_EDIT_RAW if raw else PAGENAME_EDIT_STRUCTURED,
                 ctx | self.server.ledger.view_ctx_edit(int(self.path_toks[2]),
                                                        raw))
 
index 6550f798522ce460c31b018ea7865c54340d21bf..9a1406357b62174f7f75a09330ff8f941c6b7595 100644 (file)
@@ -750,7 +750,7 @@ class Ledger:
                                  if not ac.parent],
                                 key=lambda root: root.basename)}
 
-    def view_ctx_edit(self, id_: int, raw: bool) -> dict[str, Any]:
+    def view_ctx_edit(self, id_: int, raw=True) -> dict[str, Any]:
         'All context data relevant for rendering an edit view.'
         accounts = self._calc_accounts()
         block = self._blocks[id_]
index 3c7d576d43f70af2e6d90c8fd8eaa268ad975dae..f06f15416b9d0e1a2fa025dea34d979e819dc171 100644 (file)
@@ -2,14 +2,15 @@
 # built-ins
 from sys import exit as sys_exit
 from pathlib import Path
-from typing import Any, Optional
+from typing import Optional
 # requirements.txt
 from jinja2 import (Environment as JinjaEnv,
                     FileSystemLoader as JinjaFSLoader)
 # ourselves
 from ledgplom.http import (
-        PAGENAME_BALANCE, PAGENAME_LEDGER_RAW, PAGENAME_LEDGER_STRUCTURED,
-        PATH_TEMPLATES)
+        PAGENAME_BALANCE, PAGENAME_EDIT_RAW, PAGENAME_EDIT_STRUCTURED,
+        PAGENAME_LEDGER_RAW, PAGENAME_LEDGER_STRUCTURED,
+        PATH_TEMPLATES, PREFIX_LEDGER)
 from ledgplom.ledger import Ledger
 
 
@@ -28,24 +29,33 @@ def run_tests() -> None:
     jinja = JinjaEnv(loader=JinjaFSLoader(PATH_TEMPLATES),
                      autoescape=True,
                      trim_blocks=True)
-    templates = {item: jinja.get_template(f'{item}.tmpl') for item in (
-        PAGENAME_BALANCE, PAGENAME_LEDGER_RAW, PAGENAME_LEDGER_STRUCTURED)}
-    for path in [p for p in _PATH_TESTS.iterdir()
-                 if p.parts[-1].endswith(_EXT_DAT)]:
-        ledger = Ledger(path)
-        for key, template in templates.items():
-            test_path = Path(str(path)[:-len(_EXT_DAT)] + f'.{key}')
-            if not test_path.exists():
+    templates = {item: jinja.get_template(f'{item}.tmpl')
+                 for item in (PAGENAME_BALANCE,
+                              PAGENAME_EDIT_RAW,
+                              PAGENAME_EDIT_STRUCTURED,
+                              PAGENAME_LEDGER_RAW,
+                              PAGENAME_LEDGER_STRUCTURED)}
+    paths = tuple(_PATH_TESTS.iterdir())
+    for dat_path in [p for p in paths if p.parts[-1].endswith(_EXT_DAT)]:
+        ledger = Ledger(dat_path)
+        for test_path in [p for p in paths
+                          if p != dat_path
+                          and p.parts[-1].startswith(
+                              str(dat_path.parts[-1])[:-len(_EXT_DAT)] + '.')]:
+            tmpl_name, id_str = (str(test_path.parts[-1]).split('.')
+                                 + ['-1'])[1:3]
+            if tmpl_name not in templates.keys():
                 continue
+            tmpl_name_start = tmpl_name.split("_")[0]
+            lines_rendered = templates[tmpl_name].render(
+                **getattr(ledger, f'view_ctx_{tmpl_name_start}')(
+                    **({'raw': False} if tmpl_name == PAGENAME_EDIT_STRUCTURED
+                       else {}) | ({} if tmpl_name_start == PREFIX_LEDGER[:-1]
+                                   else {'id_': int(id_str)}))
+                       ).split('\n')
             with test_path.open('r', encoding='utf8') as f:
-                lines_expected = [line.rstrip('\n')
-                                  for line in f.readlines()]
-            ctx: dict[str, Any] = {}
-            if key == PAGENAME_BALANCE:
-                ctx |= ledger.view_ctx_balance(-1)
-            else:
-                ctx |= ledger.view_ctx_ledger()
-            lines_rendered = template.render(**ctx).split('\n')
+                lines_expected = tuple(line.rstrip('\n')
+                                       for line in f.readlines())
             msg_prefix = f'test for {test_path}:'
             for idx0, line in enumerate(lines_rendered):
                 idx1 = idx0 + 1
index c2495c5b29beda12e28bc6abb923b72e6d80704a..2f01776616bed84fe3afac2816029c6b63beeb9d 100644 (file)
@@ -178,70 +178,88 @@ function taint() {
 
 
 {% macro edit_bar(block, here, there) %}
-<form action="/edit_{{here}}/{{block.id_}}" method="POST">
+<form action="/edit_{{ here }}/{{ block.id_ }}" method="POST">
 <span class="disable_on_change">
-{{conditional_block_nav('/blocks/','prev',block)}}
-{{conditional_block_nav('/blocks/','next',block)}}
+{{ conditional_block_nav('/blocks/', 'prev', block) -}}
+{{ conditional_block_nav('/blocks/', 'next', block) -}}
 </span>
-<input class="enable_on_change" type="submit" name="apply" value="apply" disabled />
-<input class="enable_on_change" type="submit" name="revert" value="revert" disabled />
+<input class="enable_on_change"
+       type="submit"
+       name="apply"
+       value="apply"
+       disabled
+       />
+<input class="enable_on_change"
+       type="submit"
+       name="revert"
+       value="revert"
+       disabled
+       />
 <span class="disable_on_change">
-<a href="/edit_{{there}}/{{block.id_}}">switch to {{there}}</a>
+<a href="/edit_{{ there }}/{{ block.id_ }}">switch to {{ there }}</a>
 ·
-<a href="/balance?up_incl={{block.id_}}">balance after</a>
+<a href="/balance?up_incl={{ block.id_ }}">balance after</a>
 ·
-<a href="/ledger_{{here}}/#block_{{block.id_}}">in ledger</a>
+<a href="/ledger_{{ here }}/#block_{{ block.id_ }}">in ledger</a>
 </span>
 <hr />
 {% if block.date_error or (block.booking and block.booking.sink_error) %}
-    <div class="critical">block-wide errors:
-    {{block.date_error}}
-    {% if block.booking %}
-        {% if block.date_error %}– and:{% endif %}
-        {{block.booking.sink_error}}
-    {% endif %}
-    </div>
-    <hr />
+<div class="critical">
+    block-wide errors:
+    {{ block.date_error }}
+{##}{% if block.booking %}
+    {{ "– and:" if block.date_error -}}
+    {{ block.booking.sink_error }}
+{##}{% endif %}
+</div>
+<hr />
 {% endif %}
 {% endmacro %}
 
 
 
-{% macro booking_balance(roots, valid) %}
-{% macro booking_balance_account_with_children(node) %}
-    {% macro td_wealth(wealth_dict) %}
+{% macro booking_balance(roots, valid) -%}
+
+{##}{% macro booking_balance_account_with_children(node) %}
+{######}{% macro td_wealth(wealth_dict) %}
         <td>
-        <table>
-        {% for curr, amt in wealth_dict.items() %}
-            <tr>
-            <td class="balance amount">{{amt}}</td>
-            <td class="balance currency">{{ currency_short(curr) }}</td>
-            </tr>
-        {% endfor %}
-        </table>
+            <table>
+{##########}{% for curr, amt in wealth_dict.items() %}
+                <tr>
+                    <td class="balance amount">
+                        {{- amt -}}
+                    </td>
+                    <td class="balance currency">
+                        {{- currency_short(curr) -}}
+                    </td>
+                </tr>
+{##########}{% endfor %}
+            </table>
+        </td>{##}
+{######}{% endmacro %}
+    <tr>
+        <td{{ ' class="direct_target"'|safe if node.direct_target }}>
+            {{- node.name }}{{ ':' if node.children -}}
         </td>
-    {% endmacro %}
+{#    #}{{ td_wealth(node.wealth_before) }}
+{#    #}{{ td_wealth(node.wealth_diff) }}
+{#    #}{{ td_wealth(node.wealth_after) }}
+    </tr>
+{######}{% for child in node.children %}
+    {{- booking_balance_account_with_children(child) -}}
+{######}{% endfor %}
+{##}{% endmacro -%}
+
+<table class="alternating{{ ' critical' if not valid }}">
     <tr>
-    <td {% if node.direct_target %}class="direct_target"{% endif %}>
-    {{node.name}}{% if node.children %}:{% endif %}
-    </td>
-    {{ td_wealth(node.wealth_before) }}
-    {{ td_wealth(node.wealth_diff) }}
-    {{ td_wealth(node.wealth_after) }}
+        <th>account</th>
+        <th>before</th>
+        <th>diff</th>
+        <th>after</th>
     </tr>
-    {% for child in node.children %}
-        {{ booking_balance_account_with_children(child) }}
-    {% endfor %}
-{% endmacro %}
-<table class="alternating {% if not valid %}critical{% endif %}">
-<tr>
-<th>account</th>
-<th>before</th>
-<th>diff</th>
-<th>after</th>
-</tr>
 {% for root in roots %}
-    {{ booking_balance_account_with_children(root) }}
+    {{- booking_balance_account_with_children(root) -}}
 {% endfor %}
 </table>
-{% endmacro %}
+
+{%- endmacro %}
index ac689266e561bfa4156fa4102d43e83bb3f7a59b..af80ee0c7a306690e866c75ba0be8c8142bf1abc 100644 (file)
@@ -5,21 +5,27 @@
 {% block css %}
 {{ macros.css_tabular_money() }}
 {{ macros.css_booking_balance() }}
-{% endblock %}
+{%- endblock %}
 
 
 
 {% block script %}
-{{macros.js_taint()}}
+{{ macros.js_taint() }}
 {% endblock %}
 
 
 
 {% block content %}
-{{macros.edit_bar(block,'raw','structured')}}
-<textarea name="raw_lines" cols=100 rows={{block.lines|length + 1}} oninput="taint()">
-{% for dat_line in block.lines %}{{ dat_line.raw }}
-{% endfor %}</textarea>
+{{ macros.edit_bar(block, 'raw', 'structured') }}
+<textarea name="raw_lines"
+          cols=100
+          rows={{ block.lines|length + 1 }}
+          oninput="taint()"
+          >
+{% for dat_line in block.lines %}
+{{ dat_line.raw }}
+{% endfor %}
+</textarea>
 </form>
 {{ macros.booking_balance(roots, valid) }}
 {% endblock %}
index dca7e0a4251dab823e3fe574a240dba9d1be2d04..9921fbda8d593e174065d29c9777e20b67bdf3a7 100644 (file)
@@ -18,9 +18,9 @@ input.amount {
 
 
 {% block script %}
-{{macros.js_taint()}}
-var raw_gap_lines = {{raw_gap_lines|tojson|safe}};
-var booking_lines = {{booking_lines|tojson|safe}};
+{{ macros.js_taint() }}
+var raw_gap_lines = {{ raw_gap_lines|tojson|safe }};
+var booking_lines = {{ booking_lines|tojson|safe }};
 
 function new_booking_line(account='', amount='None', currency='') {
     return {
@@ -253,7 +253,7 @@ window.onload = update_form;
 
 
 {% block content %}
-{{macros.edit_bar(block,'structured','raw')}}
+{{ macros.edit_bar(block, 'structured', 'raw') }}
 <span class="disable_on_change">
 <input type="button" onclick="mirror()" value="mirror">
 <input type="button" onclick="fill_sink()" value="fill sink">
@@ -268,12 +268,17 @@ to
 <table id="booking_lines" class="alternating">
 </table>
 <div>Gap:</div>
-<textarea id="gap_lines" name="raw_lines" cols=100 rows={{raw_gap_lines|length + 1}} oninput="taint()">
+<textarea id="gap_lines"
+          name="raw_lines"
+          cols=100
+          rows={{ raw_gap_lines|length + 1 }}
+          oninput="taint()"
+          >
 </textarea>
 </form>
 <datalist id="all_accounts">
 {% for acc in all_accounts %}
-<option value="{{acc}}">{{acc}}</a>
+<option value="{{ acc }}">{{ acc }}</a>
 {% endfor %}
 </datalist>
 <hr />
diff --git a/src/tests/full.edit_raw.1 b/src/tests/full.edit_raw.1
new file mode 100644 (file)
index 0000000..8c2a3b1
--- /dev/null
@@ -0,0 +1,217 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+var suppress_beforeunload = false;
+
+function taint() {
+    // activate buttons "apply", "revert"
+    Array.from(document.getElementsByClassName('enable_on_change')).forEach((el) => {
+        el.disabled = false;
+        el.addEventListener('click', function(e) {
+           suppress_beforeunload = true;
+        });
+    });
+    // deactivate "disable_on_change" span contents
+    function recursive_span_disable(el) {
+        old_nodes = Array.from(el.childNodes);
+        el.innerHTML = '';
+        old_nodes.forEach((node) => {
+            if (node.tagName == 'SPAN') {
+                recursive_span_disable(node);
+                el.appendChild(node);
+            } else if (node.tagName == 'INPUT') {
+                node.disabled = true;
+                el.appendChild(node);
+            } else if (node.tagName == 'A') {
+                const del = document.createElement('del');
+                del.textContent = node.textContent;
+                el.appendChild(del);
+            } else {
+                el.appendChild(node);
+            };
+        });
+    }
+    Array.from(document.getElementsByClassName('disable_on_change')).forEach((el) => {
+        recursive_span_disable(el);
+    });
+    // try to catch user closing or reloading window
+    window.addEventListener('beforeunload', function(e) {
+        if (!suppress_beforeunload) {
+            e.preventDefault();
+            e.returnValue = true;
+        }
+    });
+    // remove oninput handlers no longer needed (since we only ever go one way)
+    Array.from(document.querySelectorAll('*')
+        ).filter(el => (el.oninput !== null)
+        ).forEach(el => el.oninput = null);
+}
+
+</script>
+<style>
+html {
+    scroll-padding-top: 2em;
+}
+body {
+    background: #ffffff;
+    font-family: sans-serif;
+    text-align: left;
+    margin: 0;
+    padding: 0;
+}
+#header {
+    background: #ffffff;
+    position: sticky;
+    top: 0;
+    padding-left: 0.5em;
+    padding-bottom: 0.25em;
+    border-bottom: 1px solid black;
+}
+table.alternating > tbody > tr:nth-child(odd) {
+    background-color: #dcdcdc;
+}
+table.alternating > tbody > tr:nth-child(even) {
+    background: #ffffff;
+}
+td {
+    vertical-align: top;
+}
+.critical {
+    background: #ff6666 !important;
+}
+td.amount {
+    text-align: right;
+}
+td.amount,
+td.currency {
+    font-family: monospace;
+    font-size: 1.25em;
+}
+table.alternating.critical > tbody > tr:nth-child(even) {
+    background-color: #ff8a8a;
+}
+td.balance.amount {
+    width: 10em;
+}
+td.balance.currency {
+    width: 3em;
+}
+td.direct_target {
+    font-weight: bold;
+}
+</style>
+</head>
+<body>
+<div id="header">
+    <form action="" method="POST">
+        <span class="disable_on_change">
+            ledger <a href="/ledger_structured">structured</a>
+            / <a href="/ledger_raw">raw</a>
+            · <a href="/balance">balance</a>
+            · <input type="submit"name="file_load" value="reload" />
+        </span>
+    </form>
+</div>
+<form action="/edit_raw/1" method="POST">
+<span class="disable_on_change">
+<a href="/blocks/0">prev</a>
+<a href="/blocks/2">next</a>
+</span>
+<input class="enable_on_change"
+       type="submit"
+       name="apply"
+       value="apply"
+       disabled
+       />
+<input class="enable_on_change"
+       type="submit"
+       name="revert"
+       value="revert"
+       disabled
+       />
+<span class="disable_on_change">
+<a href="/edit_structured/1">switch to structured</a>
+<a href="/balance?up_incl=1">balance after</a>
+<a href="/ledger_raw/#block_1">in ledger</a>
+</span>
+<hr />
+
+<textarea name="raw_lines"
+          cols=100
+          rows=5
+          oninput="taint()"
+          >
+2001-01-01 test ; foo
+  foo  10 €
+  bar  -10 €
+
+</textarea>
+</form>
+<table class="alternating">
+    <tr>
+        <th>account</th>
+        <th>before</th>
+        <th>diff</th>
+        <th>after</th>
+    </tr>
+    <tr>
+        <td class="direct_target">bar</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr>
+        <td class="direct_target">foo</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+</table>
+</body>
+</html>
diff --git a/src/tests/full.edit_raw.4 b/src/tests/full.edit_raw.4
new file mode 100644 (file)
index 0000000..6e793ee
--- /dev/null
@@ -0,0 +1,369 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+var suppress_beforeunload = false;
+
+function taint() {
+    // activate buttons "apply", "revert"
+    Array.from(document.getElementsByClassName('enable_on_change')).forEach((el) => {
+        el.disabled = false;
+        el.addEventListener('click', function(e) {
+           suppress_beforeunload = true;
+        });
+    });
+    // deactivate "disable_on_change" span contents
+    function recursive_span_disable(el) {
+        old_nodes = Array.from(el.childNodes);
+        el.innerHTML = '';
+        old_nodes.forEach((node) => {
+            if (node.tagName == 'SPAN') {
+                recursive_span_disable(node);
+                el.appendChild(node);
+            } else if (node.tagName == 'INPUT') {
+                node.disabled = true;
+                el.appendChild(node);
+            } else if (node.tagName == 'A') {
+                const del = document.createElement('del');
+                del.textContent = node.textContent;
+                el.appendChild(del);
+            } else {
+                el.appendChild(node);
+            };
+        });
+    }
+    Array.from(document.getElementsByClassName('disable_on_change')).forEach((el) => {
+        recursive_span_disable(el);
+    });
+    // try to catch user closing or reloading window
+    window.addEventListener('beforeunload', function(e) {
+        if (!suppress_beforeunload) {
+            e.preventDefault();
+            e.returnValue = true;
+        }
+    });
+    // remove oninput handlers no longer needed (since we only ever go one way)
+    Array.from(document.querySelectorAll('*')
+        ).filter(el => (el.oninput !== null)
+        ).forEach(el => el.oninput = null);
+}
+
+</script>
+<style>
+html {
+    scroll-padding-top: 2em;
+}
+body {
+    background: #ffffff;
+    font-family: sans-serif;
+    text-align: left;
+    margin: 0;
+    padding: 0;
+}
+#header {
+    background: #ffffff;
+    position: sticky;
+    top: 0;
+    padding-left: 0.5em;
+    padding-bottom: 0.25em;
+    border-bottom: 1px solid black;
+}
+table.alternating > tbody > tr:nth-child(odd) {
+    background-color: #dcdcdc;
+}
+table.alternating > tbody > tr:nth-child(even) {
+    background: #ffffff;
+}
+td {
+    vertical-align: top;
+}
+.critical {
+    background: #ff6666 !important;
+}
+td.amount {
+    text-align: right;
+}
+td.amount,
+td.currency {
+    font-family: monospace;
+    font-size: 1.25em;
+}
+table.alternating.critical > tbody > tr:nth-child(even) {
+    background-color: #ff8a8a;
+}
+td.balance.amount {
+    width: 10em;
+}
+td.balance.currency {
+    width: 3em;
+}
+td.direct_target {
+    font-weight: bold;
+}
+</style>
+</head>
+<body>
+<div id="header">
+    <form action="" method="POST">
+        <span class="disable_on_change">
+            ledger <a href="/ledger_structured">structured</a>
+            / <a href="/ledger_raw">raw</a>
+            · <a href="/balance">balance</a>
+            · <input type="submit"name="file_load" value="reload" />
+        </span>
+    </form>
+</div>
+<form action="/edit_raw/4" method="POST">
+<span class="disable_on_change">
+<a href="/blocks/3">prev</a>
+<del>next</del>
+</span>
+<input class="enable_on_change"
+       type="submit"
+       name="apply"
+       value="apply"
+       disabled
+       />
+<input class="enable_on_change"
+       type="submit"
+       name="revert"
+       value="revert"
+       disabled
+       />
+<span class="disable_on_change">
+<a href="/edit_structured/4">switch to structured</a>
+<a href="/balance?up_incl=4">balance after</a>
+<a href="/ledger_raw/#block_4">in ledger</a>
+</span>
+<hr />
+<div class="critical">
+    block-wide errors:
+    date &lt; previous date
+    – and:
+</div>
+<hr />
+
+<textarea name="raw_lines"
+          cols=100
+          rows=7
+          oninput="taint()"
+          >
+2001-01-01 test
+  foo:x  10 €
+  foo:x  1 USD
+  bar:x:y  -10 €
+  bar:z  -1 USD
+
+</textarea>
+</form>
+<table class="alternating critical">
+    <tr>
+        <th>account</th>
+        <th>before</th>
+        <th>diff</th>
+        <th>after</th>
+    </tr>
+    <tr>
+        <td>bar:</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">1</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">-1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-9</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">-1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr>
+        <td>bar:x:</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr>
+        <td class="direct_target">bar:x:y</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr>
+        <td class="direct_target">bar:z</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">-1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr>
+        <td>foo:</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">20</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr>
+        <td class="direct_target">foo:x</td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">0</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+        <td>
+            <table>
+                <tr>
+                    <td class="balance amount">10</td>
+                    <td class="balance currency">€</td>
+                </tr>
+                <tr>
+                    <td class="balance amount">1</td>
+                    <td class="balance currency">USD</td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+</table>
+</body>
+</html>