| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Fork notice
This repository is a fork of the original HTML-Diff project. The upstream implementation has not seen updates since 2021, and this fork exists to keep the project maintained, compatible with newer Python / dependency versions, and open to fixes and improvements going forward. Unless explicitly stated, behavior aims to remain compatible with the original implementation.
Compares two HTML snippets strings and returns the diff as a valid HTML snippet with changes wrapped in <del> and <ins> tags.
Relies on BeautifulSoup4 with html.parser backend for HTML parsing and dumping.
HTML-Diff focusses on providing valid diffs, that is:
>>> from html_diff import diff
>>> diff("<em>ABC</em>", "<em>AB</em>C")
'<em><del>ABC</del><ins>AB</ins></em><ins>C</ins>'Example use case: having MathJax elements wrapped into <span class="math-tex">\(...\)</span> and wanting to avoid <del> and <ins> tags inside the \(...\) (which would be badly rendered):
>>> from html_diff.config import config
>>> config.tags_fcts_as_blocks.append(lambda tag: tag.name == "span" and "math-tex" in tag.attrs.get("class", []))Without it (does not render correctly with MathJax):
>>> diff(r'<span class="math-tex">\(\vec{v}\)</span>', r'<span class="math-tex">\(\vec{w}\)</span>')
'<span class="math-tex">\\(\\vec{<del>v</del><ins>w</ins>}\\)</span>'With it:
>>> from html_diff import clear_cache
>>> clear_cache()
>>> diff(r'<span class="math-tex">\(\vec{v}\)</span>', r'<span class="math-tex">\(\vec{w}\)</span>')
'<del><span class="math-tex">\\(\\vec{v}\\)</span></del><ins><span class="math-tex">\\(\\vec{w}\\)</span></ins>'The functions in config.config.tags_fcts_as_blocks should take a bs4.element.Tag as input and return a bool; the tags are tested against all functions in the list, and are considered insecable blocks if any call returns True.
HTML tags have a base score associated, which is added to there content score. This base score can be configured:
>>> config.EMPTY_ELEMENT_SCORE # default: 2
>>> config.OTHER_ELEMENT_SCORE # default: 2WARNING: some results are cached and changing the config does not invalidate the cache, thus the results may be wrong afterwards. Call clear_cache() to reset the cache.
>>> old = "old_string"
>>> new = "new_string"
>>> d = diff(old, new)
>>> from html_diff.check import new_from_diff
>>> new == new_from_diff(d)
True
>>> from html_diff.check import old_from_diff
>>> old == old_from_diff(d)
True
>>> from html_diff.check import is_diff_valid
>>> is_diff_valid(old, new, d)
TrueRun
python -m unittestat the root of the project.
Run
python -m html_diffand navigate to 127.0.0.1:8080 with your browser.
You can specify further options:
python -m html_diff -b 'lambda tag: tag.name == "span" and "math-tex" in tag.attrs.get("class", [])'The new implementation uses an algorithm that is closer to difflib.SequenceMatcher, although it does ironically not use it anymore.
The algorithm is similar to the legacy implementation with the UNCUTTABLE_PRECISE configuration, with the difference that it uses a Ratcliff-Obershelp-like procedure (best-matching subsequence) on all levels rather than testing all combinations to find the optimum. It is thus faster.
Parse the inputs with BeautifulSoup4; this yields two iterables of elements, either bs4.element.NavigableString or bs4.element.Tag.
On the top level of the HTML structure, split the bs4.element.NavigableString's in words (using re's \W pattern), then find the best-matching subsequence, using a score:
identical words: the length of the word,
bs4.element.Tag's where the name and attrs attributes match exactly:
On the left and the right of the best-matching subsequence, repeat 2. If non-matchable subsequences remain, assign them a score of 0 and treat them as completely deleted/inserted.
The legacy implementation is available in html_diff.legacy.
By default, the diff'ing algorithm for plain text parts does not care about words - if a word part is modified, that part gets <del>'ed and <ins>'ed, while the rest of the word remains untouched. It may however be more readable to have full words deleted and reinserted. To ensure this, switch config.config.cuttable_words_mode to either config.Config.CuttableWordsMode.UNCUTTABLE_SIMPLE or config.Config.CuttableWordsMode.UNCUTTABLE_PRECISE:
config.config.cuttable_words_mode == config.Config.CuttableWordsMode.CUTTABLE (default):
>>> from html_diff.legacy import diff as ldiff
>>> ldiff("OlyExams", "ExamTools")
'<del>Oly</del>Exam<ins>Tool</ins>s'
>>> ldiff("abcdef<br/>ghifjk", "abcdef ghifjk")
'abcdef<ins> ghifjk</ins><del><br/>ghifjk</del>'config.config.cuttable_words_mode == config.Config.CuttableWordsMode.UNCUTTABLE_SIMPLE (fast and gives acceptable results):
>>> ldiff("OlyExams", "ExamTools")
'<del>OlyExams</del><ins>ExamTools</ins>'
>>> ldiff("abcdef<br/>ghifjk", "abcdef ghifjk")
'abcdef<ins> ghifjk</ins><del><br/>ghifjk</del>'config.config.cuttable_words_mode == config.Config.CuttableWordsMode.UNCUTTABLE_PRECISE (quite slow, but uses early word breaking for better matching, in particular if plain string parts of the inputs were split or merged between old and new):
>>> ldiff("OlyExams", "ExamTools")
'<del>OlyExams</del><ins>ExamTools</ins>'
>>> ldiff("abcdef<br/>ghifjk", "abcdef ghifjk")
'abcdef<del><br/></del><ins> </ins>ghifjk'In uncuttable words modes, non-word characters correspond to re's \W pattern.
Parse the inputs with BeautifulSoup4; this yields two iterables of elements, either bs4.element.NavigableString or bs4.element.Tag.
Compare each element of the first iterable with each element of the second one. A match is only allowed in two cases:
Each match is temporarily stored, together with a "score":
The highest scoring match is kept and the algorithm recursively repeated on the subiterables before and after the matching elements. Each match thus gets (maximally) a match_before and a match_after assigned.
Regions without match are stored as "no-matches". With them, both iterables are completely covered by matches and no-matches.
| Back | FazBrowse Home | New Git URL |