Skip to content

Instantly share code, notes, and snippets.

@unarist
Last active June 18, 2023 08:12
Show Gist options
  • Select an option

  • Save unarist/e7ee9237764cc5cd2a3b4a531db8b5ee to your computer and use it in GitHub Desktop.

Select an option

Save unarist/e7ee9237764cc5cd2a3b4a531db8b5ee to your computer and use it in GitHub Desktop.
Hatena::Let - Show diff with linked (e.g. forked) Let
// ==UserScript==
// @name Hatena::Let - Show diff for forking
// @namespace https://github.com/unarist/
// @version 0.4.1
// @author unarist
// @downloadURL https://gist.github.com/unarist/e7ee9237764cc5cd2a3b4a531db8b5ee/raw/let-diff.user.js
// @match https://let.hatelabo.jp/*/let/*
// @require https://cdnjs.cloudflare.com/ajax/libs/jsdiff/3.4.0/diff.min.js#sha256=mXHDe/99FPqL1QnYUVFKhAAl45/l91tGCpRNkr4s9Mg=
// @grant none
// ==/UserScript==
/* globals JsDiff */
/* eslint-env es6 */
// やっぱりJsDiffイマイチというか、diff2html使いたいなー
(function() {
'use strict';
const tag = (name, props = {}, children = []) => {
const e = Object.assign(document.createElement(name), props);
if (typeof props.style === "object") Object.assign(e.style, props.style);
(children.forEach ? children : [children]).forEach(c => e.appendChild(c));
return e;
};
const currentCodeBlock = document.querySelector('.code-raw code');
// currentCodeBlock.innerHTML =
// currentCodeBlock.innerHTML.replace(/https?:\/\/let.hatelabo.jp\/[\w-]+\/let\/[\w-]+/g,
// url => `<a href="javascript:" class="let-url" title="show diff">${url}</a>`);
replaceTextNodes(currentCodeBlock, /https?:\/\/let.hatelabo.jp\/[\w-]+\/let\/[\w-]+/g,
url => Object.assign(document.createElement('a'), {
href: 'javascript:',
className: 'let-url',
title: 'show diff',
textContent: url
}));
for (let e of document.querySelectorAll('.let-url'))
e.addEventListener('click', onClickUrl);
function onClickUrl(e) {
fetchRawScript(e.target.textContent)
.then(text => showDiffFor(text, currentCodeBlock.textContent));
}
// ---- revlist ----
const addRadio = (refChild, name, value) => refChild.parentElement.insertBefore(
tag('input', { type: 'radio', style: 'margin-right: 3px', name, value }), refChild);
for (const revlink of document.querySelectorAll('.rev a')) {
const url = revlink.href;
addRadio(revlink, 'let-diff-newer', url).checked = revlink.parentElement.matches(':first-child') || url === location.href;
addRadio(revlink, 'let-diff-older', url);
}
const defaultDiffNewer = document.querySelector('input[name="let-diff-newer"]:checked').closest('li').nextElementSibling;
if (defaultDiffNewer) defaultDiffNewer.querySelector('input[name="let-diff-older"]').checked = true;
const rev_ol = document.querySelector('.rev ol');
for (const e of document.querySelectorAll('.let-url')) {
const url = e.textContent;
const revnode = rev_ol.appendChild(tag('li', {
style: 'list-style: none',
textContent: url
})).firstChild;
addRadio(revnode, 'let-diff-newer', url).disabled = true;
addRadio(revnode, 'let-diff-older', url);
}
document.querySelector('.rev').appendChild(tag('button', {
textContent: 'Open diff',
onclick: () =>
Promise.all([
fetchRawScript(document.querySelector('input[name="let-diff-older"]:checked').value),
fetchRawScript(document.querySelector('input[name="let-diff-newer"]:checked').value)
]).then(([oldsrc, newsrc]) => showDiffFor(oldsrc, newsrc))
}));
// ---- util ----
function fetchRawScript(letUrl) {
return fetch(letUrl + '.js')
.then(resp => {
if (!resp.ok) throw new Error(resp.statusText);
return resp.text();
})
.catch(reason => alert(`Fetching ${letUrl + '.js'} failed: ${reason}`));
}
function showDiffFor(oldstr, newstr) {
const cleaned_oldstr = oldstr.replace(/\r?\n/g, '\r\n');
const cleaned_newstr = newstr.replace(/\r?\n/g, '\r\n');
const popup = createPopup('div', '80%', '75%');
const pre = popup.appendChild(document.createElement('pre'));
pre.style = 'height: 100%; overflow: auto;';
const code = pre.appendChild(document.createElement('code'));
const fragment = document.createDocumentFragment();
JsDiff.diffLines(cleaned_oldstr, cleaned_newstr, {
newlineIsToken: false,
ignoreWhitespace: false
}).forEach(part => {
const color = part.added ? '#dfd' :
part.removed ? '#fee8e9' : 'inherit';
const div = document.createElement('div');
div.style.backgroundColor = color;
div.textContent = part.value;
fragment.appendChild(div);
});
code.appendChild(fragment);
}
function createPopup(tagName, w, h) {
const backdrop = document.body.appendChild(Object.assign(document.createElement('div'), {
style: `
background-color: rgba(0, 0, 0, 0.5);
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
z-index: ${Number.MAX_SAFE_INTEGER || Number.MAX_VALUE}
`,
onclick: e => {
if (e.target === backdrop)
backdrop.parentNode.removeChild(backdrop);
e.stopPropagation();
}
}));
return backdrop.appendChild(Object.assign(document.createElement(tagName), {
style: `
background-color: white;
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
width: ${w}; height: ${h};
margin: auto; text-align: left;
`
}));
}
/**
*
* @param {Node} target
* @param {string|RegExp} pattern
* @param {function(string): Node} replacer
*/
function replaceTextNodes(target, pattern, replacer) {
const nodeIterator = document.createNodeIterator(target, NodeFilter.SHOW_TEXT);
// RegExp has a state property "lastIndex", so we should create new instance even if it's already RegExp object.
const re = new RegExp(pattern);
let queue = [];
let currentNode;
while ((currentNode = nodeIterator.nextNode()) !== null ) {
let lastIndex = 0;
let items = [];
let match;
while ((match = re.exec(currentNode.textContent)) !== null) {
items.push({
index: match.index - lastIndex,
length: match[0].length,
replacement: replacer(match[0])
});
lastIndex = match.index + match[0].length;
}
if (items.length) {
queue.push([currentNode, items]);
}
}
for (const [node, items] of queue) {
let currentNode = node;
for (const item of items) {
currentNode = currentNode.splitText(item.index);
currentNode.textContent = currentNode.textContent.slice(item.length);
node.parentNode.insertBefore(item.replacement, currentNode);
}
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment