summaryrefslogtreecommitdiff
path: root/content/blog/wsdiff-static-html-diffs/index.rst
diff options
context:
space:
mode:
Diffstat (limited to 'content/blog/wsdiff-static-html-diffs/index.rst')
-rw-r--r--content/blog/wsdiff-static-html-diffs/index.rst127
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