Skip to content

Commit 889e20b

Browse files
authored
Merge pull request #2007 from git/copilot/fix-2002
Make anchor links in Git's documentation backwards-compatible again
2 parents e2acb19 + 007ecf9 commit 889e20b

File tree

3,167 files changed

+121226
-107775
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

3,167 files changed

+121226
-107775
lines changed

assets/js/application.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ $(document).ready(function() {
3838
Forms.init();
3939
Downloads.init();
4040
DownloadBox.init();
41+
PostelizeAnchor.init();
4142
});
4243

4344
function onPopState(fn) {
@@ -663,6 +664,120 @@ var DarkMode = {
663664
},
664665
}
665666

667+
/*
668+
* Respect Postel's Law when an invalid anchor was specified;
669+
* Try to find the most similar existing anchor and then use
670+
* that.
671+
*/
672+
var PostelizeAnchor = {
673+
init: function() {
674+
const anchor = window.location.hash;
675+
if (
676+
!anchor
677+
|| !anchor.startsWith("#")
678+
|| anchor.length < 2
679+
|| document.querySelector(CSS.escape(anchor)) !== null
680+
) return;
681+
682+
const id = anchor.slice(1);
683+
const maxD = id.length / 2;
684+
const ids = [...document.querySelectorAll('[id]')].map(e => e.id);
685+
const closestID = ids.reduce((a, e) => {
686+
const d = PostelizeAnchor.wuLevenshtein(id, e, maxD);
687+
if (d < a.d) {
688+
a.d = d;
689+
a.id = e;
690+
}
691+
return a;
692+
}, { d: maxD }).id;
693+
if (closestID) window.location.hash = `#${closestID}`;
694+
},
695+
/*
696+
* Wu's algorithm to calculate the "simple Levenshtein" distance, i.e.
697+
* the minimal number of deletions and insertions needed to transform
698+
* str1 to str2.
699+
*
700+
* The optional `maxD` parameter can be used to cap the distance (and
701+
* the runtime of the function).
702+
*/
703+
wuLevenshtein: function(str1, str2, maxD) {
704+
const len1 = str1.length;
705+
const len2 = str2.length;
706+
if (len1 === 0) return len2;
707+
if (len2 === 0) return len1;
708+
709+
/*
710+
* The idea is to navigate within the matrix that has len1 columns and len2
711+
* rows and which contains the edit distances d (the sum of
712+
* deletions/insertions) between the prefixes str1[0..x] and str2[0..y]. This
713+
* is done by looping over d, starting at 0, skipping along the diagonals
714+
* where str1[x] === str2[y] (which does not change d), storing the maximal x
715+
* value of each diagonal (characterized by k := x - y) in V[k + offset]. The
716+
* valid diagonals k range from -len2 to len1.
717+
*
718+
* Once x reaches the length of str1 and y the length of str2, the edit
719+
* distance between str1 and str2 has been found.
720+
*
721+
* Allocate a vector V of size (len1 + len2 + 1) so that index = k + offset,
722+
* with offset = len2 (since k can be negative, but JavaScript does not
723+
* support negative array indices).
724+
*
725+
* We can get away with a single array V because adjacent d values on
726+
* neighboring diagonals differ by 1, meaning that even k values correspond
727+
* to even d values, and odd k values to odd d values. Therefore, in loop
728+
* iterations where d is odd, V[k] is read out at even k values and modified
729+
* at odd k values.
730+
*/
731+
const size = len1 + len2 + 1;
732+
const V = new Array(size).fill(0);
733+
const offset = len2;
734+
735+
if (maxD === undefined) maxD = len1 + len2;
736+
// d is the edit distance (insertions/deletions)
737+
for (let d = 0; d < maxD; d++) {
738+
// k can only be between max(-len2, -d) and min(len1, d)
739+
// and we step in increments of 2.
740+
for (let k = Math.max(-len2, -d); k <= len1 && k <= d; k += 2) {
741+
const kIndex = k + offset;
742+
let x;
743+
744+
/*
745+
* Decide whether to use an insertion or a deletion:
746+
* - If k is -d, x (i.e. the offset in str1) must be 0 and nothing can be
747+
* deleted,
748+
* - If k is d, V[kIndex + 1] hasn't been calculated in the previous
749+
* loop iterations, therefore it must be a deletion,
750+
* - Otherwise, choose the direction that allows reaching furthest in
751+
* str1, i.e. maximize x (and therefore also y).
752+
*/
753+
if (k === -d || (k !== d && V[kIndex - 1] < V[kIndex + 1])) {
754+
// Insertion: from diagonal k+1 (i.e. we move down in str2)
755+
x = V[kIndex + 1];
756+
} else {
757+
// Deletion: from diagonal k-1 (i.e. we move right in str1)
758+
x = V[kIndex - 1] + 1;
759+
}
760+
761+
// Compute y based on the diagonal: y = x - k.
762+
let y = x - k;
763+
764+
// Follow the “snake” (i.e. match characters along the diagonal).
765+
while (x < len1 && y < len2 && str1[x] === str2[y]) {
766+
x++;
767+
y++;
768+
}
769+
V[kIndex] = x;
770+
771+
// If we've reached the ends of both strings, then we've found the answer.
772+
if (x >= len1 && y >= len2) {
773+
return d;
774+
}
775+
}
776+
}
777+
return maxD;
778+
},
779+
}
780+
666781
// Scroll to Top
667782
$('#scrollToTop').removeClass('no-js');
668783
$(window).scroll(function() {

0 commit comments

Comments
 (0)