您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
adds a word counter with options to Google Docs
当前为
// ==UserScript== // @name Google Docs - Word Count with Options // @namespace https://zachhardesty.com // @author Zach Hardesty <[email protected]> (https://github.com/zachhardesty7) // @description adds a word counter with options to Google Docs // @copyright 2019, Zach Hardesty (https://zachhardesty.com/) // @license GPL-3.0-only; http://www.gnu.org/licenses/gpl-3.0.txt // @version 1.0.0 // @homepageURL https://github.com/zachhardesty7/tamper-monkey-scripts-collection/raw/master/google-docs-word-count.user.js // @homepageURL https://openuserjs.org/scripts/zachhardesty7/Google_Docs_-_Word_Count_(With_Options) // @supportURL https://openuserjs.org/scripts/zachhardesty7/Google_Docs_-_Word_Count_(With_Options)/issues // @match https://docs.google.com/document/* // ==/UserScript== // heavy inspiration from: // https://gf.zukizuki.org/en/scripts/22057-google-docs-wordcount/code // https://stackoverflow.com/questions/951021/what-is-the-javascript-version-of-sleep // strikingly complex (uses DOM bounding boxes) to get currently selected text: // may implement only necessary functions to save space, library size: (15.4 KB) // https://github.com/JensPLarsen/ChromeExtension-GoogleDocsUtil const displayCount = () => { // words not counted between these when true const BRACKETS = true const PARENTHESIS = true const QUOTES = true const MISC = true // skips works cited, personal titles const SELECTED = true // if selected text present, word count only counts it const display = document.createElement("div") display.id = "zh-display" display.setAttribute( "style", ` position: fixed; width: 100%; left: 0px; bottom: 0px; color: rgba(0,0,0,.7); height: 15px; background-color: #ededee; z-index: 100; font-family: Arial; font-size: 12px; padding-top: 5px; padding-left: 5px; border-top: 1px solid #d9d9d9; ` ) document.querySelector("body").append(display) /** * update the word count */ async function setCount() { const doc = getGoogleDocument() let selected = doc.selectedText console.log("selected", selected) const pages = document.querySelector(".kix-paginateddocumentplugin") .children[1].children let body = "" for (const page of pages) { // pages that are unloaded will appear to have no text // add a marker to the cumulative body to indicate that // a word count should not be displayed if (page.textContent === "") body += " ~~ " body += page.textContent } // clean extra spaces body = body.replace(/\u00A0/g, " ").trim() // generate regex from settings // must escape \'s in JS // in standard regex form: // /(“(.(?!“))+”)|(\((.(?!\())+\)|\[(.(?!\[))+\]) // |Works Cited(\n.*)*|(Unit \d (Primary Source Analysis|Exam: Part \d - #\d+))/g const regex = [] if (BRACKETS) regex.push("\\[(.(?!\\[))+\\]") if (PARENTHESIS) regex.push("\\((.(?!\\())+\\)") if (QUOTES) regex.push( "Works Cited(.|\\n.*)*|(Unit \\d (Primary Source Analysis|Exam: Part \\d( - #\\d+)*))" ) if (MISC) regex.push("(“(.(?!“))+”)") // apply regex filtering to body regex.forEach((reg) => { selected = selected.replace(new RegExp(reg, "g"), " ") }) // apply regex filtering to selected text if necessary let filtered = body regex.forEach((reg) => { filtered = filtered.replace(new RegExp(reg, "g"), " ") }) // remove extra spaces and line breaks and get counts const words = filtered .trim() .replace(/\u00A0/g, " ") .replace(/ {2,}/g, " ") .split(" ") if (words.includes("~~")) { // empty or unloaded pages present document.querySelector( "#zh-display" ).textContent = `Word Count: (scroll to bottom & remove empty pages) | Pages: ${pages.length}` } else if (selected.length > 0 && SELECTED) { selected = selected .trim() .replace(/\u00A0/g, " ") .replace(/ {2,}/g, " ") console.log("selected", selected) document.querySelector("#zh-display").textContent = `Word Count: ${ selected.split(" ").length } of ${words.length} (selected) | Pages: ${pages.length}` } else { document.querySelector( "#zh-display" ).textContent = `Word Count: ${words.length} | Pages: ${pages.length}` } } setInterval(setCount, 1000) } // #region - Google Docs Utils // - - - - - - - - - - - - - - - - - - - - // General // - - - - - - - - - - - - - - - - - - - - const classNames = { paragraph: ".kix-paragraphrenderer", line: ".kix-lineview", selectionOverlay: ".kix-selection-overlay", wordNode: ".kix-wordhtmlgenerator-word-node", cursor: ".kix-cursor", cursorName: ".kix-cursor-name", cursorCaret: ".kix-cursor-caret", } /** * Google Docs like to add \u200B, \u200C (&zwnj) and non breaking spaces to make sure * the browser shows the text correct. When getting the text, we would prefer to get * clean text. * * @param {string} text - ? * @returns {string} clean text */ function cleanDocumentText(text) { let cleanedText = text.replace(/[\u200B\u200C]/g, "") const nonBreakingSpaces = String.fromCharCode(160) const regex = new RegExp(nonBreakingSpaces, "g") cleanedText = cleanedText.replace(regex, " ") return cleanedText } // - - - - - - - - - - - - - - - - - - - - // Get Google Document // - - - - - - - - - - - - - - - - - - - - /** * Finds all the text and the caret position in the . * * @returns {GoogleDoc} google docs document */ function getGoogleDocument() { let caret, caretRect let caretIndex = 0 let caretLineIndex = 0 let caretLine = 0 const text = [] const nodes = [] let lineCount = 0 let globalIndex = 0 let selectedText = "" let exportedSelectionRect const paragraphRenderers = document.querySelectorAll(classNames.paragraph) if (containsUserCaretDom()) { caret = getUserCaretDom() caretRect = caret.getBoundingClientRect() } for (const paragraphRenderer of paragraphRenderers) { const lineViews = paragraphRenderer.querySelectorAll(classNames.line) for (const lineView of lineViews) { let lineText = "" const selectionOverlays = lineView.querySelectorAll( classNames.selectionOverlay ) const wordhtmlgeneratorWordNodes = lineView.querySelectorAll( classNames.wordNode ) for (const wordhtmlgeneratorWordNode of wordhtmlgeneratorWordNodes) { const wordhtmlgeneratorWordNodeRect = wordhtmlgeneratorWordNode.getBoundingClientRect() if ( caretRect && doesRectsOverlap(wordhtmlgeneratorWordNodeRect, caretRect) ) { const caretXStart = caretRect.left - wordhtmlgeneratorWordNodeRect.left const localCaretIndex = getLocalCaretIndex( caretXStart, wordhtmlgeneratorWordNode, lineView ) caretIndex = globalIndex + localCaretIndex caretLineIndex = lineText.length + localCaretIndex caretLine = lineCount } const nodeText = cleanDocumentText( wordhtmlgeneratorWordNode.textContent ) nodes.push({ index: globalIndex, line: lineCount, lineIndex: lineText.length, node: wordhtmlgeneratorWordNode, lineElement: lineView, text: nodeText, }) for (const selectionOverlay of selectionOverlays) { const selectionRect = selectionOverlay.getBoundingClientRect() if (selectionRect) exportedSelectionRect = selectionRect if ( doesRectsOverlap( wordhtmlgeneratorWordNodeRect, selectionOverlay.getBoundingClientRect() ) ) { const selectionStartIndex = getLocalCaretIndex( selectionRect.left - wordhtmlgeneratorWordNodeRect.left, wordhtmlgeneratorWordNode, lineView ) const selectionEndIndex = getLocalCaretIndex( selectionRect.left + selectionRect.width - wordhtmlgeneratorWordNodeRect.left, wordhtmlgeneratorWordNode, lineView ) selectedText += nodeText.slice( selectionStartIndex, selectionEndIndex ) } } globalIndex += nodeText.length lineText += nodeText } text.push(lineText) lineCount += 1 } } return { nodes, text, selectedText, caret: { index: caretIndex, lineIndex: caretLineIndex, line: caretLine, }, selectionRect: exportedSelectionRect, } } // http://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other /** * @param {DOMRect} RectA - ? * @param {DOMRect} RectB - ? * @returns {boolean} overlapping? */ function doesRectsOverlap(RectA, RectB) { return ( RectA.left <= RectB.right && RectA.right >= RectB.left && RectA.top <= RectB.bottom && RectA.bottom >= RectB.top ) } // The kix-cursor contain a kix-cursor-name dom, which is only set when it is not the users cursor /** * @returns {boolean} does the kix-cursor contain a kix-cursor-name dom */ function containsUserCaretDom() { const carets = document.querySelectorAll(classNames.cursor) for (const caret of carets) { const nameDom = caret.querySelectorAll(classNames.cursorName) const name = nameDom[0].textContent if (!name) return true } return false } // The kix-cursor contain a kix-cursor-name dom, which is only set when it is not the users cursor /** * @returns {Element} user caret */ function getUserCaretDom() { const carets = document.querySelectorAll(classNames.cursor) for (const caret of carets) { const nameDom = caret.querySelectorAll(classNames.cursorName) const name = nameDom[0].textContent if (!name) return caret.querySelectorAll(classNames.cursorCaret)[0] } throw new Error("Could not find the users cursor") } /** * @param {number} caretX - The x coordinate on where the element the caret is located * @param {Element} element - The element on which contains the text where in the caret position is * @param {Element} simulateElement - ?Doing the calculation of the caret position, we need to create a temporary DOM, the DOM will be created as a child to the simulatedElement. * @returns {number} caret index on the innerText of the element */ function getLocalCaretIndex(caretX, element, simulateElement) { // Creates a span DOM for each letter const text = cleanDocumentText(element.textContent) const container = document.createElement("div") const letterSpans = [] for (const ch of text) { const textNode = document.createElement("span") textNode.textContent = ch textNode.style.cssText = element.style.cssText // "pre" = if there are multiple white spaces, they will all be rendered. Default behavior is for them to be collapsed textNode.style.whiteSpace = "pre" letterSpans.push(textNode) container.append(textNode) } container.style.whiteSpace = "nowrap" simulateElement.append(container) // The caret is usually at the edge of the letter, we find the edge we are closest to. let index = 0 let currentMinimumDistance = -1 const containerRect = container.getBoundingClientRect() letterSpans.forEach((letterSpan, i) => { const rect = letterSpan.getBoundingClientRect() const left = rect.left - containerRect.left const right = left + rect.width if (currentMinimumDistance === -1) { currentMinimumDistance = Math.abs(caretX - left) } const leftDistance = Math.abs(caretX - left) const rightDistance = Math.abs(caretX - right) if (leftDistance <= currentMinimumDistance) { index = i currentMinimumDistance = leftDistance } if (rightDistance <= currentMinimumDistance) { index = i + 1 currentMinimumDistance = rightDistance } }) // Clean up container.remove() return index } displayCount()