diff --git a/ferris.css b/ferris.css index 513efa1431..f7a7daf084 100644 --- a/ferris.css +++ b/ferris.css @@ -19,6 +19,11 @@ body.ayu .not_desired_behavior { background: #501f21; } +:root { + --ferris-large-width: 4.5em; + --ferris-small-width: 2.3em; +} + .ferris-container { position: absolute; z-index: 99; @@ -33,11 +38,19 @@ body.ayu .not_desired_behavior { } .ferris-large { - width: 4.5em; + width: var(--ferris-large-width); } .ferris-small { - width: 2.3em; + width: var(--ferris-small-width); +} + +.ferris-buffer-large { + margin-right: calc(var(--ferris-large-width) + 1.0em); +} + +.ferris-buffer-small { + margin-right: calc(var(--ferris-small-width) + 0.5em); } .ferris-explain { diff --git a/ferris.js b/ferris.js index 13b1ceb9a5..cdff574159 100644 --- a/ferris.js +++ b/ferris.js @@ -52,7 +52,9 @@ function attachFerrises(type) { continue; } - container.appendChild(createFerris(type, size)); + let ferris = createFerris(type, size) + container.appendChild(ferris); + giveFerrisSpace(codeBlock, ferris, size); } } @@ -100,3 +102,72 @@ function createFerris(type, size) { return a; } + +/** + * Put each line ending in a span. For each of those spans, + * if Ferris might hide it, give it a safety buffer. + * @param {HTMLElement} codeBlock + * @param {HTMLAnchorElement} ferris + * @param {'small' | 'large'} size + */ +function giveFerrisSpace(codeBlock, ferris, size) { + // sanity checking + lint awareness + const ferrisImage = ferris.firstChild; + if (!(ferrisImage instanceof HTMLImageElement)) { + console.error("ferris should be containing ", ferris); + return; + } + + /** @type {HTMLSpanElement[]} */ + const lineEndings = []; // line endings which might be hidden by Ferris + + const walker = document.createTreeWalker(codeBlock, NodeFilter.SHOW_TEXT); + const re = /^(.*?)\n(.*)$/s + + while (walker.nextNode()) { + const current = walker.currentNode; + const parent = current.parentNode; + + // sanity checking + lint awareness + if (!(current instanceof Text) || !parent) { + continue; + } + + let re_results; + while (re_results = current.textContent.match(re)) { + // text node contains newline + const [_, beforeNewline, afterNewline] = re_results; + + // line ending gets a span + const lineEnd = document.createElement("span"); + lineEnd.textContent = beforeNewline; + lineEndings.push(lineEnd); + parent.insertBefore(lineEnd, current); + + // newline now stands alone + parent.insertBefore(document.createTextNode("\n"), current); + + // rest of the text + current.textContent = afterNewline; + // current might still contain newlines, so we go again until it doesn't + } + } + + codeBlock.normalize(); // not strictly necessary, but good practice to leave the DOM normalized + + // setTimeout so getBoundingClientRect returns valid results + setTimeout(() => { + const f = ferrisImage.getBoundingClientRect(); + lineEndings.forEach((s) => { + const {bottom, top} = s.getBoundingClientRect(); + if ( // vertical overlap between ferris and span + (bottom >= f.top && bottom <= f.bottom) + || (top >= f.top && top <= f.bottom) + || (f.top >= top && f.top <= bottom) + ) { + // buffer needed! + s.classList.add("ferris-buffer-" + size); + } + }); + }); +}