CogTech Language Tool (CogWrite)

Extension-style floating LanguageTool checker for Google Docs and normal text fields.

Você precisará instalar uma extensão como Tampermonkey, Greasemonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Violentmonkey para instalar este script.

Você precisará instalar uma extensão como Tampermonkey ou Userscripts para instalar este script.

Você precisará instalar uma extensão como o Tampermonkey para instalar este script.

Você precisará instalar um gerenciador de scripts de usuário para instalar este script.

(Eu já tenho um gerenciador de scripts de usuário, me deixe instalá-lo!)

Advertisement:

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar uma extensão como o Stylus para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

Você precisará instalar um gerenciador de estilos de usuário para instalar este estilo.

(Eu já possuo um gerenciador de estilos de usuário, me deixar fazer a instalação!)

Advertisement:

// ==UserScript==
// @name         CogTech Language Tool (CogWrite)
// @namespace    https://greasyfork.org/scripts/languagetool-checker
// @version      1.2.0
// @description  Extension-style floating LanguageTool checker for Google Docs and normal text fields.
// @author       Phil 🥹👍 and CogTech
// @match        *://*/*
// @all-frames   true
// @grant        GM_xmlhttpRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_addStyle
// @grant        GM.addStyle
// @grant        GM_getValue
// @grant        GM.getValue
// @grant        GM_setValue
// @grant        GM.setValue
// @grant        GM_registerMenuCommand
// @grant        GM.registerMenuCommand
// @connect      api.languagetool.org
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  if (window.__cogwriteFloatingCheckerLoaded) return;
  window.__cogwriteFloatingCheckerLoaded = true;

  const API_URL = 'https://api.languagetool.org/v2/check';
  const MAX_TEXT_CHARS = 20000;
  const EDITABLE_SELECTOR = [
    'textarea',
    'input[type="text"]',
    'input[type="search"]',
    'input[type="email"]',
    'input[type="url"]',
    'input[type="tel"]',
    '[contenteditable="true"]',
    '[contenteditable="plaintext-only"]',
    '[role="textbox"]'
  ].join(',');

  const gm = {
    getValue(key, fallback) {
      try {
        if (typeof GM_getValue === 'function') return GM_getValue(key, fallback);
        if (typeof GM !== 'undefined' && typeof GM.getValue === 'function') return GM.getValue(key, fallback);
      } catch (_) {}
      return fallback;
    },
    setValue(key, value) {
      try {
        if (typeof GM_setValue === 'function') return GM_setValue(key, value);
        if (typeof GM !== 'undefined' && typeof GM.setValue === 'function') return GM.setValue(key, value);
      } catch (_) {}
      return undefined;
    },
    addStyle(css) {
      try {
        if (typeof GM_addStyle === 'function') return GM_addStyle(css);
        if (typeof GM !== 'undefined' && typeof GM.addStyle === 'function') return GM.addStyle(css);
      } catch (_) {}

      const style = document.createElement('style');
      style.textContent = css;
      (document.head || document.documentElement).appendChild(style);
      return style;
    },
    registerMenuCommand(name, fn) {
      try {
        if (typeof GM_registerMenuCommand === 'function') return GM_registerMenuCommand(name, fn);
        if (typeof GM !== 'undefined' && typeof GM.registerMenuCommand === 'function') return GM.registerMenuCommand(name, fn);
      } catch (_) {}
      return undefined;
    },
    request(details) {
      if (typeof GM_xmlhttpRequest === 'function') return GM_xmlhttpRequest(details);
      if (typeof GM !== 'undefined' && typeof GM.xmlHttpRequest === 'function') return GM.xmlHttpRequest(details);

      return fetch(details.url, {
        method: details.method || 'GET',
        headers: details.headers || {},
        body: details.data
      }).then(async (res) => {
        details.onload?.({ status: res.status, responseText: await res.text() });
      }).catch((err) => details.onerror?.(err));
    }
  };

  const settings = {
    enabled: gm.getValue('lt_enabled', true),
    language: gm.getValue('lt_language', 'auto'),
    preferredVariants: gm.getValue('lt_variants', 'en-US,en-GB,de-DE'),
    picky: gm.getValue('lt_picky', false)
  };

  let root;
  let panel;
  let button;
  let statusEl;
  let textBox;
  let resultsEl;
  let lastMatches = [];

  gm.addStyle(`
    #cogwrite-root {
      position: fixed !important;
      top: 86px !important;
      right: 18px !important;
      z-index: 2147483647 !important;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif !important;
      color: #f8fafc !important;
    }
    #cogwrite-root button,
    #cogwrite-root textarea {
      font: inherit !important;
    }
    #cogwrite-button {
      width: 48px !important;
      height: 48px !important;
      border-radius: 50% !important;
      border: 1px solid #334155 !important;
      background: #111827 !important;
      color: #ffffff !important;
      box-shadow: 0 10px 28px rgba(15, 23, 42, 0.32) !important;
      cursor: pointer !important;
      font-weight: 700 !important;
      font-size: 14px !important;
      display: flex !important;
      align-items: center !important;
      justify-content: center !important;
      user-select: none !important;
    }
    #cogwrite-button.cogwrite-has-issues {
      background: #991b1b !important;
      border-color: #fecaca !important;
    }
    #cogwrite-panel {
      display: none !important;
      position: absolute !important;
      right: 0 !important;
      top: 58px !important;
      width: 360px !important;
      max-width: calc(100vw - 36px) !important;
      max-height: 520px !important;
      overflow: hidden !important;
      border: 1px solid #334155 !important;
      border-radius: 8px !important;
      background: #0f172a !important;
      box-shadow: 0 18px 48px rgba(15, 23, 42, 0.38) !important;
    }
    #cogwrite-panel.cogwrite-open {
      display: block !important;
    }
    .cogwrite-header {
      display: flex !important;
      align-items: center !important;
      justify-content: space-between !important;
      padding: 10px 12px !important;
      border-bottom: 1px solid #334155 !important;
      font-weight: 700 !important;
    }
    .cogwrite-header button {
      background: transparent !important;
      border: 0 !important;
      color: #cbd5e1 !important;
      cursor: pointer !important;
      font-size: 16px !important;
      padding: 2px 6px !important;
    }
    .cogwrite-body {
      padding: 10px !important;
    }
    #cogwrite-status {
      color: #cbd5e1 !important;
      font-size: 12px !important;
      line-height: 1.4 !important;
      margin-bottom: 8px !important;
    }
    .cogwrite-actions {
      display: flex !important;
      gap: 8px !important;
      margin-bottom: 10px !important;
    }
    .cogwrite-action {
      border: 0 !important;
      border-radius: 6px !important;
      background: #2563eb !important;
      color: #ffffff !important;
      cursor: pointer !important;
      padding: 7px 10px !important;
      font-size: 12px !important;
      white-space: nowrap !important;
    }
    .cogwrite-action.secondary {
      background: #334155 !important;
    }
    #cogwrite-text {
      width: 100% !important;
      min-height: 92px !important;
      box-sizing: border-box !important;
      resize: vertical !important;
      border-radius: 6px !important;
      border: 1px solid #475569 !important;
      background: #020617 !important;
      color: #f8fafc !important;
      padding: 8px !important;
      margin-bottom: 10px !important;
      outline: none !important;
    }
    #cogwrite-results {
      max-height: 260px !important;
      overflow-y: auto !important;
    }
    .cogwrite-issue {
      border-top: 1px solid #334155 !important;
      padding: 9px 0 !important;
    }
    .cogwrite-message {
      font-weight: 700 !important;
      margin-bottom: 5px !important;
    }
    .cogwrite-context {
      color: #cbd5e1 !important;
      overflow-wrap: anywhere !important;
      margin-bottom: 7px !important;
    }
    .cogwrite-suggestions {
      display: flex !important;
      flex-wrap: wrap !important;
      gap: 6px !important;
    }
    .cogwrite-suggestion {
      border: 0 !important;
      border-radius: 6px !important;
      background: #334155 !important;
      color: #ffffff !important;
      cursor: pointer !important;
      padding: 4px 7px !important;
      font-size: 12px !important;
    }
  `);

  function isEditable(el) {
    return Boolean(
      el &&
      el.nodeType === Node.ELEMENT_NODE &&
      el.matches?.(EDITABLE_SELECTOR) &&
      !el.disabled &&
      !el.readOnly
    );
  }

  function getEditableText(el) {
    if (!isEditable(el)) return '';
    if (el instanceof HTMLTextAreaElement || el instanceof HTMLInputElement) return el.value || '';
    return el.innerText || el.textContent || '';
  }

  function getSelectedOrFocusedText() {
    const selection = String(window.getSelection?.() || '').trim();
    if (selection) return selection;

    const active = document.activeElement;
    const focusedText = getEditableText(active).trim();
    if (focusedText) return focusedText;

    return '';
  }

  function setStatus(message) {
    statusEl.textContent = message;
  }

  function setOpen(open) {
    panel.classList.toggle('cogwrite-open', open);
  }

  function setButtonState(matches, checking = false) {
    button.classList.toggle('cogwrite-has-issues', matches.length > 0);
    button.textContent = checking ? '...' : matches.length ? String(matches.length) : 'CW';
  }

  function checkText(text) {
    const value = String(text || '').trim();
    if (!value) {
      lastMatches = [];
      setButtonState(lastMatches);
      setStatus('Paste text here, select text in the page, or click into a normal text field.');
      renderResults('', []);
      return;
    }

    const body = new URLSearchParams();
    body.set('text', value.slice(0, MAX_TEXT_CHARS));
    body.set('language', settings.language);
    if (settings.language === 'auto') body.set('preferredVariants', settings.preferredVariants);
    if (settings.picky) body.set('level', 'picky');

    setStatus('Checking with LanguageTool...');
    setButtonState(lastMatches, true);

    gm.request({
      method: 'POST',
      url: API_URL,
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      data: body.toString(),
      onload(res) {
        if (res.status !== 200) {
          setStatus(`LanguageTool API error ${res.status}`);
          setButtonState(lastMatches);
          return;
        }

        try {
          const data = JSON.parse(res.responseText);
          lastMatches = data.matches || [];
          setStatus(`${lastMatches.length} issue${lastMatches.length === 1 ? '' : 's'} found.`);
          renderResults(value, lastMatches);
          setButtonState(lastMatches);
        } catch (error) {
          setStatus(error.message);
          setButtonState(lastMatches);
        }
      },
      onerror() {
        setStatus('Network error contacting LanguageTool.');
        setButtonState(lastMatches);
      }
    });
  }

  function renderResults(text, matches) {
    resultsEl.textContent = '';

    if (!matches.length) {
      const empty = document.createElement('div');
      empty.className = 'cogwrite-empty';
      empty.textContent = text ? 'No issues found.' : 'No text checked yet.';
      resultsEl.appendChild(empty);
      return;
    }

    for (const match of matches) {
      const issue = document.createElement('div');
      issue.className = 'cogwrite-issue';

      const ctxStart = Math.max(0, match.offset - 24);
      const ctxEnd = Math.min(text.length, match.offset + match.length + 24);

      const message = document.createElement('div');
      message.className = 'cogwrite-message';
      message.textContent = match.message;
      issue.appendChild(message);

      const context = document.createElement('div');
      context.className = 'cogwrite-context';
      context.appendChild(document.createTextNode(text.slice(ctxStart, match.offset)));
      const badText = document.createElement('b');
      badText.textContent = text.slice(match.offset, match.offset + match.length);
      context.appendChild(badText);
      context.appendChild(document.createTextNode(text.slice(match.offset + match.length, ctxEnd)));
      issue.appendChild(context);

      const suggestions = document.createElement('div');
      suggestions.className = 'cogwrite-suggestions';

      for (const replacement of (match.replacements || []).slice(0, 5)) {
        const suggestion = document.createElement('button');
        suggestion.type = 'button';
        suggestion.className = 'cogwrite-suggestion';
        suggestion.textContent = replacement.value;
        suggestion.addEventListener('click', () => {
          textBox.value = textBox.value.slice(0, match.offset) +
            replacement.value +
            textBox.value.slice(match.offset + match.length);
          checkText(textBox.value);
        });
        suggestions.appendChild(suggestion);
      }

      issue.appendChild(suggestions);
      resultsEl.appendChild(issue);
    }
  }

  function createUi() {
    if (!settings.enabled || root || !document.documentElement) return;

    root = document.createElement('div');
    root.id = 'cogwrite-root';

    panel = document.createElement('div');
    panel.id = 'cogwrite-panel';

    const header = document.createElement('div');
    header.className = 'cogwrite-header';

    const title = document.createElement('span');
    title.textContent = 'CogWrite';

    const closeButton = document.createElement('button');
    closeButton.type = 'button';
    closeButton.setAttribute('aria-label', 'Close');
    closeButton.textContent = 'x';

    header.append(title, closeButton);

    const body = document.createElement('div');
    body.className = 'cogwrite-body';

    statusEl = document.createElement('div');
    statusEl.id = 'cogwrite-status';
    statusEl.textContent = 'Paste text here or select text in the document.';

    const actions = document.createElement('div');
    actions.className = 'cogwrite-actions';

    const grabButton = document.createElement('button');
    grabButton.type = 'button';
    grabButton.className = 'cogwrite-action';
    grabButton.dataset.action = 'grab';
    grabButton.textContent = 'Use selected text';

    const checkButton = document.createElement('button');
    checkButton.type = 'button';
    checkButton.className = 'cogwrite-action';
    checkButton.dataset.action = 'check';
    checkButton.textContent = 'Check';

    const clearButton = document.createElement('button');
    clearButton.type = 'button';
    clearButton.className = 'cogwrite-action secondary';
    clearButton.dataset.action = 'clear';
    clearButton.textContent = 'Clear';

    actions.append(grabButton, checkButton, clearButton);

    textBox = document.createElement('textarea');
    textBox.id = 'cogwrite-text';
    textBox.placeholder = 'Paste or type text to check';

    resultsEl = document.createElement('div');
    resultsEl.id = 'cogwrite-results';

    body.append(statusEl, actions, textBox, resultsEl);
    panel.append(header, body);

    button = document.createElement('button');
    button.type = 'button';
    button.id = 'cogwrite-button';
    button.title = 'CogWrite';
    button.textContent = 'CW';

    root.append(panel, button);

    document.documentElement.appendChild(root);

    button.addEventListener('click', () => setOpen(!panel.classList.contains('cogwrite-open')));
    closeButton.addEventListener('click', () => setOpen(false));
    grabButton.addEventListener('click', () => {
      const text = getSelectedOrFocusedText();
      textBox.value = text;
      setStatus(text ? 'Loaded text. Press Check.' : 'No selectable or focused text found. Paste text below.');
    });
    checkButton.addEventListener('click', () => checkText(textBox.value));
    clearButton.addEventListener('click', () => {
      textBox.value = '';
      lastMatches = [];
      setButtonState(lastMatches);
      setStatus('Cleared.');
      renderResults('', []);
    });

    renderResults('', []);
  }

  gm.registerMenuCommand(settings.enabled ? 'Disable CogWrite' : 'Enable CogWrite', () => {
    settings.enabled = !settings.enabled;
    gm.setValue('lt_enabled', settings.enabled);
    location.reload();
  });

  gm.registerMenuCommand('CogWrite language: auto', () => {
    settings.language = 'auto';
    gm.setValue('lt_language', 'auto');
  });

  gm.registerMenuCommand('CogWrite language: en-US', () => {
    settings.language = 'en-US';
    gm.setValue('lt_language', 'en-US');
  });

  gm.registerMenuCommand('Toggle CogWrite picky mode', () => {
    settings.picky = !settings.picky;
    gm.setValue('lt_picky', settings.picky);
    alert(`Picky mode: ${settings.picky ? 'on' : 'off'}`);
  });

  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', createUi, { once: true });
  } else {
    createUi();
  }
})();