diff options
Diffstat (limited to 'content/blog/wsdiff-static-html-diffs/index.rst')
-rw-r--r-- | content/blog/wsdiff-static-html-diffs/index.rst | 127 |
1 files changed, 0 insertions, 127 deletions
diff --git a/content/blog/wsdiff-static-html-diffs/index.rst b/content/blog/wsdiff-static-html-diffs/index.rst deleted file mode 100644 index 51f9175..0000000 --- a/content/blog/wsdiff-static-html-diffs/index.rst +++ /dev/null @@ -1,127 +0,0 @@ ---- -title: "wsdiff: Responsive diffs in plain HTML" -date: 2025-07-25T23:42:00+01:00 -summary: > - There's many tools that render diffs on the web, but almost none that work well on small screens such as phones. I - fixed this by publishing wsdiff, a diffing tool written in Python that produces diffs as beautiful, responsive, - static, self-contained HTML pages. wsdiffs wrap text to fit the window, and dynamically switch between unified and - split diffs based on screen size using only CSS. ---- - -Demo ----- - -First off, have a demo. Because of the width of this page, the output will show an unified diff. To try out the split -diff layout, make sure your browser window is wide enough and open the demo in a separate tab using `this link -</wsdiff-example.html>`__. - -wsdiff supports dark mode, try it out by toggling dark mode in your operating system! - -.. raw:: html - - <iframe src="/wsdiff-example.html" style="width: 100%; height: 30em; border: 1px #d0d0d0 solid" id="wsdiff example diff"></iframe> - -Core Features -------------- - -There's many tools that render diffs on the web, but almost none that work well on small screens such as phones. I fixed -this by publishing `wsdiff <https://pypi.org/project/wsdiff/>`__, a diffing tool written in Python that produces diffs -as beautiful, responsive, static, self-contained HTML pages. wsdiffs wrap text to fit the window, and dynamically switch -between unified and split diffs based on screen size using only CSS. - -Responsive Line Wrapping -........................ - -The first challenge I solved was wrapping source code lines to match the available screen space. Other tools often just -show horizontal scroll bars, which is an okay workaround when you're mostly working with hard-wrapped source code on a -laptop or desktop screen, but which results in catastrophic UX on any phone. - -I solved line breaking with a combination of CSS-controlled, web-standard word breaking rules: ``overflow-wrap: -anywhere`` for source code (`MDN link <https://developer.mozilla.org/en-US/docs/Web/CSS/overflow-wrap>`__) and -``white-space: pre-wrap`` to preserve whitespace accurately (`MDN link -<https://developer.mozilla.org/en-US/docs/Web/CSS/white-space>`__). To make both sides of the split diff align, and to -align line numbers with wrapped source code lines, the diff is laid out using a `CSS grid layout`_. In side-by-side -view, the layout has four columns: two for line numbers and two for the content. In unified view, the left ("old") -content column is dropped, and the deleted or modified lines that are highlighted in it in side-by-side view are slotted -into the remaining right column. - -When soft-wrapping source code, text editors will often display a little curved arrow marker to indicate that a line was -soft-wrapped, and that there is not actually a newline character in the file at that location. wsdiff solves this -using the same technique I used for the soft-wrapping code blocks in this blog, described `here <{{<ref -"blog/css-only-code-blocks/index.rst">}}>`__. It inserts a string of ``"\a↳\a↳\a↳\a↳\a↳..."`` into the line number -element's ``::after`` pseudo-element. This string evaluates to a sequence of unicode arrows separated by line breaks, -and starting with an empty line. The ``::after`` pseudo-element is positioned using ``position: absolute``, and the -parent ``<span class="lineno">`` has ``position: relative`` set. This way, the arrow pseudo-element gets placed on top -of the lineno span without affecting the layout at all. By setting ``overflow: clip`` on the parent ``<span -class="lineno">``, the arrow pseudo-element gets cut off vertically wherever the parent line number element naturally -ends. Since both the line and the line number element share a grid row, the line number element always matches the -height of the soft-wrapped line. - -Responsive Split/Unified Layout Selection -......................................... - -To dynamically change between unified and side-by-side views, wsdiff uses a web-standard `Media Query`_. By default, the -page is laid out for side-by-side view. In the HTML source, the diff is listed as it is displayed in side-by-side view, -with the old and new lines along with their line numbers interleaved. - -The magic happens when the media query gets triggered by a narrow screen width. The media query re-adjusts the layout in -four core steps: - - 1. All unchanged lines in the left (old) column are hidden. - 2. The left content column of the grid layout is hidden, so that now there are three columns: old line number, new line - number, and unified content. - 3. All deleted or changed lines from the left (old) column are re-located to the right column. They naturally slot in - in the right spot because they already appear in the right order in the HTML source. - 4. By slotting in the old lines in the right column, we have created gaps in the line number columns. Every deleted - line has an empty cell in the new line number column, and every inserted line has one in the old line number column. - The CSS adjusts the layout of these empty cells such that the border lines align nicely, and it overrides the - newline markers so that they only show in the right (new) line number column, not both. - -Since this is all CSS, it happens automatically and near-instantly. Since it is using only web standard features, it -works across browsers and platforms. - -Unchanged Line Folding in CSS -............................. - -When showing the diff of a large file, it is usually best to hide large runs of unchanged lines. wsdiff does this -similar to code folding in text editors. When a long run of unchanged lines is detected, a marker is placed spanning the -diff. This marker contains a checkbox that can be toggled to hide the unchanged lines. This feature is done completely -in CSS using a ``:has(input[type="checkbox"]:checked)`` selector. - -The actual mechanics are quite simple. To cleanly hide the lines, they must be placed in a container ``<div>``. That div -has a CSS subgrid layout using ``display: grid; grid-template-columns: subgrid;``, meaning that its contents align to -the surrounding diff grid. - -Dark Mode -......... - -Integrating a website with the OS-level dark mode is surprisingly easy. All you need is a `Media Query`_ that selects -for ``@media (prefers-color-scheme: dark)`` and you're good. wsdiff uses named colors using `CSS Custom Properties`_, so -the actual dark mode media query only needs to override these color properties, and the rest of the CSS will be -re-computed automatically. - -Limitations: Text selection -........................... - -A limitation in having a combined, single HTML source for both side-by-side and unified diffs is that text selection -only works naturally in either mode. You can't make text selection work in both simultaneously without re-sorting the -lines in the HTML source, since there is no way to override the text selection order from pure CSS. In wsdiff, I worked -around this issue by just disabling text selection on the unchanged lines in the left (old) column, so selecting text in -the right column copies the unified diff as one would expect. - -Try it yourself! ----------------- - -You can find the demo from above at `this link </wsdiff-example.html>`__. - -You can install wsdiff yourself `from PyPI <https://pypi.org/project/wsdiff/>`__: - -.. code:: sh - - $ pip install -U wsdiff - Successfully installed wsdiff-0.3.1 - $ wsdiff old.py new.py -o diff.html - -.. _`CSS grid layout`: https://css-tricks.com/snippets/css/complete-guide-grid/ -.. _`Media Query`: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_media_queries/Using_media_queries -.. _`CSS Custom Properties`: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_cascading_variables/Using_CSS_custom_properties |