Last active
June 18, 2023 08:12
-
-
Save unarist/e7ee9237764cc5cd2a3b4a531db8b5ee to your computer and use it in GitHub Desktop.
Hatena::Let - Show diff with linked (e.g. forked) Let
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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