CogTech Language Tool (CogWrite)

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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();
  }
})();