Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix and rewrite markup anchor processing #29931

Merged
merged 5 commits into from
Mar 20, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 51 additions & 33 deletions web_src/js/markup/anchors.js
Original file line number Diff line number Diff line change
@@ -1,50 +1,68 @@
import {svg} from '../svg.js';

const headingSelector = '.markup h1, .markup h2, .markup h3, .markup h4, .markup h5, .markup h6';

// scroll to anchor while respecting the `user-content` prefix that exists on the target
function scrollToAnchor(hash, initial) {
function scrollToAnchor(encodedId, initial) {
// abort if the browser has already scrolled to another anchor during page load
if (initial && document.querySelector(':target')) return;
if (hash?.length <= 1) return;
const id = decodeURIComponent(hash.substring(1));
const el = document.getElementById(`user-content-${id}`);
if (el) {
el.scrollIntoView();
} else if (id.startsWith('user-content-')) { // compat for links with old 'user-content-' prefixed hashes
if (!encodedId || (initial && document.querySelector(':target'))) return;
const id = decodeURIComponent(encodedId);
let el = document.getElementById(`user-content-${id}`);

// check for matching user-generated `a[name]`
if (!el) {
const nameAnchors = document.getElementsByName(`user-content-${id}`);
if (nameAnchors.length) {
el = nameAnchors[0];
}
}

// compat for links with old 'user-content-' prefixed hashes
if (!el && id.startsWith('user-content-')) {
const el = document.getElementById(id);
if (el) el.scrollIntoView();
}

if (el) {
el.scrollIntoView();
}
}

export function initMarkupAnchors() {
if (!document.querySelector('.markup')) return;

// create link icons for markup headings, the resulting link href will remove `user-content-`
for (const heading of document.querySelectorAll(headingSelector)) {
const originalId = heading.id.replace(/^user-content-/, '');
const a = document.createElement('a');
a.classList.add('anchor');
a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
a.innerHTML = svg('octicon-link');
a.addEventListener('click', (e) => {
scrollToAnchor(e.currentTarget.getAttribute('href'), false);
});
heading.prepend(a);
}
const markupEls = document.querySelectorAll('.markup');
if (!markupEls.length) return;

for (const markupEl of markupEls) {
// create link icons for markup headings, the resulting link href will remove `user-content-`
for (const heading of markupEl.querySelectorAll(`:is(h1, h2, h3, h4, h5, h6`)) {
const originalId = heading.id.replace(/^user-content-/, '');
const a = document.createElement('a');
a.classList.add('anchor');
a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
a.innerHTML = svg('octicon-link');
heading.prepend(a);
}

// remove `user-content-` prefix from links so they don't show in url bar when clicked
for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
const href = a.getAttribute('href');
if (!href.startsWith('#user-content-')) continue;
const originalId = href.replace(/^#user-content-/, '');
a.setAttribute('href', `#${originalId}`);
}

// add `user-content-` prefix to user-generated `a[name]` link targets
// TODO: this prefix should be added in backend instead
for (const a of markupEl.querySelectorAll('a[name]')) {
const name = a.getAttribute('name');
if (!name) continue;
a.setAttribute('name', `user-content-${a.name}`);
}
silverwind marked this conversation as resolved.
Show resolved Hide resolved

// handle user-defined `name` anchors like `[Link](#link)` linking to `<a name="link"></a>Link`
for (const a of document.querySelectorAll('.markup a[href^="#"]')) {
const href = a.getAttribute('href');
if (!href.startsWith('#user-content-')) continue;
const originalId = href.replace(/^#user-content-/, '');
a.setAttribute('href', `#${encodeURIComponent(originalId)}`);
if (a.closest('.markup').querySelectorAll(`a[name="${originalId}"]`).length !== 1) {
for (const a of markupEl.querySelectorAll('a[href^="#"]')) {
a.addEventListener('click', (e) => {
scrollToAnchor(e.currentTarget.getAttribute('href'), false);
scrollToAnchor(e.currentTarget.getAttribute('href')?.substring(1), false);
});
}
}

scrollToAnchor(window.location.hash, true);
scrollToAnchor(window.location.hash.substring(1), true);
}