您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A userscript that adds a button to insert RTL text blocks in comments
当前为
// ==UserScript== // @name GitHub RTL Comment Blocks // @version 1.1.0 // @description A userscript that adds a button to insert RTL text blocks in comments // @license https://creativecommons.org/licenses/by-sa/4.0/ // @namespace http://github.com/Mottie // @include https://github.com/* // @run-at document-idle // @grant GM_addStyle // @connect github.com // @author Rob Garrison // ==/UserScript== /*jshint unused:true, esnext:true */ /* global GM_addStyle */ (function() { "use strict"; let targets, busy = false; const icon = ` <svg class="octicon" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14"> <path d="M14 3v8l-4-4m-7 7V6C1 6 0 5 0 3s1-3 3-3h7v2H9v12H7V2H5v12H3z"/> </svg>`, // maybe using ⁧ RTL text ⁦ (isolates) is a better combo? openRTL = "‏", // https://en.wikipedia.org/wiki/Right-to-left_mark closeRTL = "‎", // https://en.wikipedia.org/wiki/Left-to-right_mark regexOpen = /\u200f/ig, regexClose = /\u200e/ig, regexSplit = /(\u200f|\u200e)/ig; GM_addStyle(` .ghu-rtl-css { direction:rtl; text-align:right; unicode-bidi:isolate; } /* delegated binding; ignore clicks on svg & path */ .ghu-rtl > * { pointer-events:none; } /* override RTL on code blocks */ .js-preview-body pre, .markdown-body pre, .js-preview-body code, .markdown-body code { direction:ltr; text-align:left; unicode-bidi:normal; } `); // Add monospace font toggle function addRtlButton() { busy = true; let el, button, toolbars = $$(".toolbar-commenting"), indx = toolbars.length; if (indx) { button = document.createElement("button"); button.type = "button"; button.className = "ghu-rtl toolbar-item tooltipped tooltipped-n"; button.setAttribute("aria-label", "RTL"); button.setAttribute("tabindex", "-1"); button.innerHTML = icon; while (indx--) { el = toolbars[indx]; if (!$(".ghu-rtl", el)) { el.insertBefore(button.cloneNode(true), el.childNodes[0]); } } } checkRTL(); busy = false; } function checkContent(el) { // check the contents, and wrap in either a span or div let indx, // useDiv, html = el.innerHTML, parts = html.split(regexSplit), len = parts.length; for (indx = 0; indx < len; indx++) { if (regexOpen.test(parts[indx])) { // check if the content contains HTML // useDiv = regexTestHTML.test(parts[indx + 1]); // parts[indx] = (useDiv ? "<div" : "<span") + " class='ghu-rtl-css'>"; parts[indx] = "<div class='ghu-rtl-css'>"; } else if (regexClose.test(parts[indx])) { // parts[indx] = useDiv ? "</div>" : "</span>"; parts[indx] = "</div>"; } } el.innerHTML = parts.join(""); // remove empty paragraph wrappers (may have previously contained the mark) return el.innerHTML.replace(/<p><\/p>/g, ""); } function checkRTL() { let clone, indx = 0, div = document.createElement("div"), containers = $$(".js-preview-body, .markdown-body"), len = containers.length, // main loop loop = function() { let el, tmp, max = 0; while (max < 10 && indx < len) { if (indx > len) { return; } el = containers[indx]; tmp = el.innerHTML; if (regexOpen.test(tmp) || regexClose.test(tmp)) { clone = div.cloneNode(); clone.innerHTML = tmp; // now we can replace all instances el.innerHTML = checkContent(clone); max++; } indx++; } if (indx < len) { setTimeout(function() { loop(); }, 200); } }; loop(); } function $(selector, el) { return (el || document).querySelector(selector); } function $$(selector, el) { return Array.from((el || document).querySelectorAll(selector)); } function closest(el, selector) { while (el && el.nodeName !== "BODY" && !el.matches(selector)) { el = el.parentNode; } return el && el.matches(selector) ? el : []; } function addBindings() { $("body").addEventListener("click", function(event) { let textarea, target = event.target; if (target && target.classList.contains("ghu-rtl")) { textarea = closest(target, ".previewable-comment-form"); textarea = $(".comment-form-textarea", textarea); textarea.focus(); // add extra white space around the tags surroundSelectedText(textarea, ' ' + openRTL + ' ', ' ' + closeRTL + ' '); return false; } }); } targets = $$("#js-repo-pjax-container, #js-pjax-container, .js-preview-body"); Array.prototype.forEach.call(targets, function(target) { new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { // preform checks before adding code wrap to minimize function calls if (!busy && mutation.target === target) { addRtlButton(); } }); }).observe(target, { childList: true, subtree: true }); }); addBindings(); addRtlButton(); /* HEAVILY MODIFIED from https://github.com/timdown/rangyinputs code was unwrapped & unneeded code was removed */ /** * @license Rangy Inputs, a jQuery plug-in for selection and caret manipulation within textareas and text inputs. * * https://github.com/timdown/rangyinputs * * For range and selection features for contenteditable, see Rangy. * http://code.google.com/p/rangy/ * * Depends on jQuery 1.0 or later. * * Copyright 2014, Tim Down * Licensed under the MIT license. * Version: 1.2.0 * Build date: 30 November 2014 */ var UNDEF = "undefined"; var getSelection, setSelection, surroundSelectedText; // Trio of isHost* functions taken from Peter Michaux's article: // http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting function isHostMethod(object, property) { var t = typeof object[property]; return t === "function" || (!!(t == "object" && object[property])) || t == "unknown"; } function isHostProperty(object, property) { return typeof(object[property]) != UNDEF; } function isHostObject(object, property) { return !!(typeof(object[property]) == "object" && object[property]); } function fail(reason) { if (window.console && window.console.log) { window.console.log("RangyInputs not supported in your browser. Reason: " + reason); } } function adjustOffsets(el, start, end) { if (start < 0) { start += el.value.length; } if (typeof end == UNDEF) { end = start; } if (end < 0) { end += el.value.length; } return { start: start, end: end }; } function makeSelection(el, start, end) { return { start: start, end: end, length: end - start, text: el.value.slice(start, end) }; } function getBody() { return isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0]; } var testTextArea = document.createElement("textarea"); getBody().appendChild(testTextArea); if (isHostProperty(testTextArea, "selectionStart") && isHostProperty(testTextArea, "selectionEnd")) { getSelection = function(el) { var start = el.selectionStart, end = el.selectionEnd; return makeSelection(el, start, end); }; setSelection = function(el, startOffset, endOffset) { var offsets = adjustOffsets(el, startOffset, endOffset); el.selectionStart = offsets.start; el.selectionEnd = offsets.end; }; } else if (isHostMethod(testTextArea, "createTextRange") && isHostObject(document, "selection") && isHostMethod(document.selection, "createRange")) { getSelection = function(el) { var start = 0, end = 0, normalizedValue, textInputRange, len, endRange; var range = document.selection.createRange(); if (range && range.parentElement() == el) { len = el.value.length; normalizedValue = el.value.replace(/\r\n/g, "\n"); textInputRange = el.createTextRange(); textInputRange.moveToBookmark(range.getBookmark()); endRange = el.createTextRange(); endRange.collapse(false); if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) { start = end = len; } else { start = -textInputRange.moveStart("character", -len); start += normalizedValue.slice(0, start).split("\n").length - 1; if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) { end = len; } else { end = -textInputRange.moveEnd("character", -len); end += normalizedValue.slice(0, end).split("\n").length - 1; } } } return makeSelection(el, start, end); }; // Moving across a line break only counts as moving one character in a TextRange, whereas a line break in // the textarea value is two characters. This function corrects for that by converting a text offset into a // range character offset by subtracting one character for every line break in the textarea prior to the // offset var offsetToRangeCharacterMove = function(el, offset) { return offset - (el.value.slice(0, offset).split("\r\n").length - 1); }; setSelection = function(el, startOffset, endOffset) { var offsets = adjustOffsets(el, startOffset, endOffset); var range = el.createTextRange(); var startCharMove = offsetToRangeCharacterMove(el, offsets.start); range.collapse(true); if (offsets.start == offsets.end) { range.move("character", startCharMove); } else { range.moveEnd("character", offsetToRangeCharacterMove(el, offsets.end)); range.moveStart("character", startCharMove); } range.select(); }; } else { getBody().removeChild(testTextArea); fail("No means of finding text input caret position"); return; } // Clean up getBody().removeChild(testTextArea); function getValueAfterPaste(el, text) { var val = el.value, sel = getSelection(el), selStart = sel.start; return { value: val.slice(0, selStart) + text + val.slice(sel.end), index: selStart, replaced: sel.text }; } function pasteTextWithCommand(el, text) { el.focus(); var sel = getSelection(el); // Hack to work around incorrect delete command when deleting the last word on a line setSelection(el, sel.start, sel.end); if (text === "") { document.execCommand("delete", false, null); } else { document.execCommand("insertText", false, text); } return { replaced: sel.text, index: sel.start }; } function pasteTextWithValueChange(el, text) { el.focus(); var valueAfterPaste = getValueAfterPaste(el, text); el.value = valueAfterPaste.value; return valueAfterPaste; } var pasteText = function(el, text) { var valueAfterPaste = getValueAfterPaste(el, text); try { var pasteInfo = pasteTextWithCommand(el, text); if (el.value == valueAfterPaste.value) { pasteText = pasteTextWithCommand; return pasteInfo; } } catch (ex) { // Do nothing and fall back to changing the value manually } pasteText = pasteTextWithValueChange; el.value = valueAfterPaste.value; return valueAfterPaste; }; var updateSelectionAfterInsert = function(el, startIndex, text, selectionBehaviour) { var endIndex = startIndex + text.length; selectionBehaviour = (typeof selectionBehaviour == "string") ? selectionBehaviour.toLowerCase() : ""; if ((selectionBehaviour == "collapsetoend" || selectionBehaviour == "select") && /[\r\n]/.test(text)) { // Find the length of the actual text inserted, which could vary // depending on how the browser deals with line breaks var normalizedText = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); endIndex = startIndex + normalizedText.length; var firstLineBreakIndex = startIndex + normalizedText.indexOf("\n"); if (el.value.slice(firstLineBreakIndex, firstLineBreakIndex + 2) == "\r\n") { // Browser uses \r\n, so we need to account for extra \r characters endIndex += normalizedText.match(/\n/g).length; } } switch (selectionBehaviour) { case "collapsetostart": setSelection(el, startIndex, startIndex); break; case "collapsetoend": setSelection(el, endIndex, endIndex); break; case "select": setSelection(el, startIndex, endIndex); break; } }; surroundSelectedText = function(el, before, after, selectionBehaviour) { if (typeof after == UNDEF) { after = before; } var sel = getSelection(el); var pasteInfo = pasteText(el, before + sel.text + after); updateSelectionAfterInsert(el, pasteInfo.index + before.length, sel.text, selectionBehaviour || "select"); }; })();